5

I know that you should never use std::find(some_map.begin(), some_map.end()) or std::lower_bound, because it will take linear time instead of logarithmic provided by some_map.lower_bound. Similar thing happens with std::list: there is std::list::sort function for sorting, but you're unable to call std::sort(some_list.begin(), some_list.end()), because iterators are not random-access.

However, std::swap, for instance, has overloads for standard containers, so that call of swap(some_map, other_map) takes O(1), not O(n). Why doesn't C++ standard give us specialized versions of lower_bound and find for maps and sets? Is there are deep reason?

yeputons
  • 8,478
  • 34
  • 67

4 Answers4

5

I don't think there's any deep reason, but it's more philosophical than anything. The free function forms of standard algorithms, including the ones you mention, take iterator pairs indicating the range over which they'll traverse. There's no way for the algorithm to determine the type of the underlying container from these iterators.

Providing specializations, or overloads, would be a departure from this model since you'd have to pass the container itself to the algorithm.

swap is different because it takes instances of the types involved as arguments, and not just iterators.

Praetorian
  • 106,671
  • 19
  • 240
  • 328
3

The rule followed by the STL is that the free-function algorithms operate on iterators and are generic, not caring about the type of range the iterators belong to (other than the iterator category). If a particular container type can implement an operation more efficiently than using std::algo(cont.begin(), cont.end()) then that operation is provided as a member function of the container and called as cont.algo().

So you have std::map<>::lower_bound() and std::map<>::find() and std::list<>::sort().

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
2

The simple fact that such functions should work in terms of iterator pairs would produce a quite significant overhead, even if specialized for map/set iterators.

Keep in mind that:

  • Such functions can be called with any pair of iterators, not just a begin/end.

  • Iterators of map/sets are usually implemented as a pointer to a leaf node, while the starting node of the member find/lower_bound is the root node of the tree.

Having a member find and lower_bound is better, because a pointer to the root node is directly stored as a member of the map/set object.

An hypothetical non-member find would have to traverse the tree to find the lowest common ancestor between the two input nodes, and then do a dfs - while still being careful to search only in the [first,last) range - which is significantly more expensive.

Yes, you could keep track of the root node inside the iterator, then optimize if the function is called with a begin/end pair... just to avoid a member function?

sbabbi
  • 11,070
  • 2
  • 29
  • 57
2

There are good "design philosophy" reasons for not providing those overloads. In particular, the whole STL containers / algorithms interaction is designed to go through the different iterator concepts such that those two parts remain independent, so that you can define new container types without having to overload for all the algorithms, and so that you can define new algorithms without having to overload for all the containers.

Beyond those design choices, there is a very good technical reason why these algorithms (sort, lower-bound, etc..) cannot be overloaded for the special containers like list or map / set. The reason is that there is generally no reason for iterators to be explicitly connected to their parent container. In other words, you can get list iterators (begin / end) from the list container, but you cannot obtain (a reference to) the list container from a list iterator. The same goes for map / set iterators. Iterators can be implemented to be "blind" to their parent container. For example, a container like list would normally contain, as data members, pointers to the head and tail nodes, and the allocator object, but the iterators themselves (usually just pointers to a node) don't need to know about the head / tail or allocator, they just have to walk the previous / next link pointers to move back / forth in the list. So, if you were to implement an overload of std::sort for the list container, there would be no way to retrieve the list container from the iterators that are passed to the sort function. And, in all the cases you mentioned, the versions of the algorithms that are appropriate for those special containers are "centralized" in the sense that they need to be run at the container level, not at the "blind" iterator level. That's why they make sense as member functions, and that's why you cannot get to them from the iterators.

However, if you need to have a generic way to call these algorithms regardless of the container that you have, then you can provide overloaded function templates at the container level if you want to. It's as easy as this:

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

template <typename T, typename Alloc>
void sort_container(std::list<T, Alloc>& c) {
  c.sort();
};

template <typename Key, typename T, typename Compare, typename Alloc>
void sort_container(std::map<Key, T, Compare, Alloc>&) { /* nothing */ };

//... so on..

The point here is that if you are already tied in with STL containers, then you can tie in with STL algorithms in this way if you want to... but don't expect the C++ standard library to force you to have this kind of inter-dependence between containers and algorithms, because not everyone wants them (e.g., a lot of people really like STL algorithms, but have no love for the STL containers (which have numerous problems)).

Mikael Persson
  • 18,174
  • 6
  • 36
  • 52