C++ Algorithms – Why Do All Functions Take Only Ranges, Not Containers?

clanguage-designstandard-library

There are many useful functions in <algorithm>, but all of them operate on "sequences" – pairs of iterators. E.g., if I have a container and like to run std::accumulate on it, I need to write:

std::vector<int> myContainer = ...;
int sum = std::accumulate(myContainer.begin(), myContainer.end(), 0);

When all I intend to do is:

int sum = std::accumulate(myContainer, 0);

Which is a bit more readable and clearer, in my eyes.

Now I can see that there might be cases where you'd only want to operate on parts of a container, so it's definitely useful to have the option of passing ranges. But at least in my experience, that's a rare special case. I'll usually want to operate on whole containers.

It's easy to write a wrapper function which takes a container and calls begin() and end() on it, but such convenience functions are not included in the standard library.

I'd like to know the reasoning behind this STL design choice.

Best Answer

... it's definitely useful to have the option of passing ranges. But at least in my experience, that's a rare special case. I'll usually want to operate on whole containers

It may be a rare special case in your experience, but in reality the whole container is the special case, and the arbitrary range is the general case.

You've already noticed that you can implement the whole container case using the current interface, but you can't do the converse.

So, the library-writer had a choice between implementing two interfaces up front, or only implementing one which still covers all cases.


It's easy to write a wrapper function which takes a container and calls begin() and end() on it, but such convenience functions are not included in the standard library

True, especially since the free functions std::begin and std::end are now included.

So, let's say the library provides the convenience overload:

template <typename Container>
void sort(Container &c) {
  sort(begin(c), end(c));
}

now it also needs to provide the equivalent overload taking a comparison functor, and we need to provide the equivalents for every other algorithm.

But we at least covered every case where we want to operate on a full container, right? Well, not quite. Consider

std::for_each(c.rbegin(), c.rend(), foo);

If we want to handle operating backwards on containers, we need another method (or pair of methods) per existing algorithm.


So, the range-based approach is more general in the simple sense that:

  • it can do everything the whole-container version can
  • the whole-container approach doubles or triples the number of overloads required, while still being less powerful
  • the range-based algorithms are also composable (you can stack or chain iterator adaptors, although this is more commonly done in functional languages and Python)

There's another valid reason, of course, which is that it was already a lot of work to get the STL standardized, and inflating it with convenience wrappers before it had been widely used wouldn't be a great use of limited committee time. If you're interested, you can find Stepanov & Lee's technical report here

As mentioned in comments, Boost.Range provides a newer approach without requiring changes to the standard.

Related Topic