28

I have always assumed that std::lower_bound() runs in logarithmic time if I pass a pair of red-black tree iterators (set::iterator or map::iterator) to it. I had to burn myself twice to notice that std::lower_bound() runs in O(n) time in this case, at least with the libstdc++ implementation. I understand that the standard doesn't have the concept of red-black tree iterators; std::lower_bound() will treat them as bidirectional iterators and advance them in linear time. I still don't see any reason why the implementation couldn't create an implementation specific iterator tag for red-black tree iterators and call a specialized lower_bound() if the passed in iterators happen to be red-black tree iterators.

Is there any technical reason why std::lower_bound() is not specialized for red-black tree iterators?


UPDATE: Yes, I am aware of the find member functions but it is so not the point. (In a templated code I may not have access to the container or work only on a part of container.)


After the bounty has expired: I find Mehrdad's and Yakk's answers the most convincing. I couldn't decide between the too; I am giving the bounty to Mehrdad and accepting Yakk's answer.

Ali
  • 56,466
  • 29
  • 168
  • 265
  • Tried the container method instead? – Yakk - Adam Nevraumont Jan 05 '14 at 14:30
  • 1
    Not the point. Beside, in templated code you may not work access the container / work on the whole container. – Ali Jan 05 '14 at 14:31
  • 1
    I think it's possible to supply a predicate that is not equivalent to the one supplied to `std::set` and still fulfil the requirement of partially sorted (for special `set`s). So you can only replace the `lower_bound` algorithm by a special red-black version if the predicate is equivalent to the `std::set` ordering. – dyp Jan 05 '14 at 14:34
  • 1
    @dyp technically it just needs to agree on the elements in the given range. – Yakk - Adam Nevraumont Jan 05 '14 at 14:49
  • @Yakk Indeed, but I guess that's much harder to check. – dyp Jan 05 '14 at 14:52
  • You could add an iterator type -- say "skipable" -- that given two iterators can go to one approximetally halfway between them in amortized constant time. – Yakk - Adam Nevraumont Jan 05 '14 at 17:10
  • 2
    +1 You have gained enough upvotes - so we approve you: **go and patch the library! :-)** Because the only real reason is that nobody did it yet. – Tomas Jan 07 '14 at 16:17
  • That and backward compatibility, which was an important goal of libstdc++. – Alice Jan 08 '14 at 20:39
  • It could be done, but not with SCARY iterators. – Marc Glisse Jan 09 '14 at 13:18

5 Answers5

12

There are multiple reasons:

  1. When using the non-member version a different predicate can be used. In fact, a different predicate has to be used when using, e.g., a std::map<K, V> as the map predicate operates on Ks while the range operates on pairs of K and V.
  2. Even if the predicate is compatible, the function has an interface using a pair of nodes somewhere in the tree rather than the root node which would be needed for an efficient search. Although it is likely that there are parent pointers, requiring so for the tree seems inappropriate.
  3. The iterators provided to the algorithm are not required to be the t.begin() and the t.end() of the tree. They can be somewhere in the tree, making the use of the tree structure potentially inefficient.
  4. The standard library doesn't have a tree abstraction or algorithms operating on trees. Although the associative ordered containers are [probably] implemented using trees the corresponding algorithms are not exposed for general use.

The part I consider questionable is the use of a generic name for an algorithm which has linear complexity with bidirectional iterators and logarithmic complexity with random access iterators (I understand that the number of comparisons has logarithmic complexity in both cases and that the movements are considered to be fast).

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • "The part I consider questionable" -- even worse, `std::advance` uses the same name for an algorithm that has linear complexity with bidirectional iterators and *constant* complexity with random-access ;-p – Steve Jessop Jan 05 '14 at 16:57
  • for map, `V` is a junior oartner in the ordering: and as `K` must be unique, they are usually compatible if `V` is comparable. – Yakk - Adam Nevraumont Jan 05 '14 at 17:05
  • 1
    @SteveJessop `std::advance()` is not confusing for me: It is always as fast as possible. `std::lower_bound()` is different as it is possible to implement it in logarithmic time with red-black tree iterators, yet it runs in linear time. Ouch! – Ali Jan 05 '14 at 17:09
  • 2
    @Ali: Not really, `std::advance` is *not* "always as fast as possible". If you used ADL to call it (i.e. `using std::advance; advance(i, d);`) *then* it might have been. But at the moment, `std::advance` does **not** use `operator+=` unless the iterator is random-access. That's inefficient: consider a `concat_iterator`, which iterators over the concatenation of a bunch of ranges. `operator++` would be *much* slower there than `operator+=` because the latter can skip entire ranges, but the iterator could not possibly offer the random-access guarantee so `std::advance` would be inefficient. – user541686 Jan 05 '14 at 22:22
  • 1
    @Ali: I personally think ADL should be used for calling *all* C++ algorithms for reasons like this one -- but I feel like I'm the only one who believe this is necessary or a good idea, so I don't actually end up doing it in practice either. :( I think C++ would provide much better abstractions if ADL was used properly for algorithms, though. – user541686 Jan 05 '14 at 22:24
  • @Mehrdad Hmm. Interesting. What is this mysterious `concat_iterator`? Do you mean that there are other useful iterator types that the standard library is not aware of, and as a consequence the algorithms doesn't play well with them? In any case, what I meant is that `std::advance()` is as fast as possible *on the iterators in the standard library.* – Ali Jan 05 '14 at 22:30
  • @Ali: Yes, that's what I mean. [Here](http://p-stade.sourceforge.net/oven/doc/html/oven/range_adaptors.html#oven.range_adaptors.concatenated)'s an example of one (it's a range, but it's the same issue). – user541686 Jan 05 '14 at 22:33
  • @Mehrdad: Interesting. `concat_iterator` is an iterator falling between categories. So for performance you want to customize `advance`. But this desire is in conflict with the STL-inspired principle that algorithms are generic, and "shouldn't" need to be customized per-iterator. Algorithms aren't separate from data if you go around re-implementing them per iterator type. Now, `swap` is so clearly non-generic that everyone knows it's part of the object and needs to be ADL-called. I think it's less obvious that the same is (or should be) true of `advance`, `lower_bound`, `copy`, `sort`... – Steve Jessop Jan 06 '14 at 18:00
  • @SteveJessop: To me `advance`, `swap`, `iter_swap`, etc. aren't even "algorithms" in the first place! :) Frankly, `advance` shouldn't even *exist* -- `operator+=` would be sufficient, if only it was required to be defined for all iterator types. Same deal with `swap` -- it should have been an operator, which is of course a proposal. – user541686 Jan 06 '14 at 19:08
  • @SteveJessop: As for the rest of the algorithms -- there's simply *no way* to keep algorithms 100% orthogonal to iterators. The entire *point* of data structures is to affect their associated algorithms. The abstraction is very leaky; it's simply naive of C++ to think that algorithms apply equally to all data structures, because that implies data structures are pointless! For example, consider that `std::sort` should be a no-op for a binary search tree using the default predicate -- of *course* you should detect it as a special situation. Algorithms just *aren't* orthogonal to data structures. – user541686 Jan 06 '14 at 19:11
  • @SteveJessop: On a side note, this is also why ranges are usually better than iterators. If `lower_bound` accepted a range then the user could just pass the container itself and we wouldn't have problems with trying to find the predicate... – user541686 Jan 06 '14 at 19:14
  • @Mehrdad: right, if what you say is generally agreed, then a switch from iterator pairs to ranges would be a good opportunity to change (what I called) the principles of algorithms. Say that *all* standard algorithms can be ADL-implemented by ranges (including containers) and implementations in `std::` are just defaults for containers that are happy with them. Which is most but not necessarily all combinations of standard algorithms with standard containers. Btw I don't think `sort` on a binary search tree should be a no-op, it's a logical error. That doesn't undermine your points though. – Steve Jessop Jan 06 '14 at 23:41
  • 1
    @SteveJessop: I don't think considering algorithms customization points is the right approach. There are a couple of customization pointers, e.g., the operators, `swap()`, `advance()`, `distance()`, and maybe a handful of others. Not everything which is a function template is a suitable customization point, though. In fact, I'd rather go the opposite way and nail certain algorithms to _not_ be candidates looked up via ADL by making them function objects (which would have the added advantage that you could easily bind them even if you don't know the arguments, yet). – Dietmar Kühl Jan 06 '14 at 23:46
  • @DietmarKühl: OK, well we may be forming an answer to "if what Mehrdad says is generally agreed". It's generally agreed over your dead body, for one ;-) I'm kind of staying out of that because I don't have firm opinions which things currently in `` actually benefit from customization for real non-standard range types out there in the wild. `swap` was a sure-fire example in C++03 but to be honest is kind of marginal with C++11. – Steve Jessop Jan 06 '14 at 23:47
  • @SteveJessop: nah, I wouldn't stand in the way of a misguided mob! The times where I would sacrifice myself for the benefit of society are long gone: the crowd will do what the crowd thinks is right which isn't necessarily the best solution. It won't stop me from using my own library (if it ever emerges...) doing things correctly. – Dietmar Kühl Jan 06 '14 at 23:50
  • lol I can't tell who's agreeing with whom but this is an interesting discussion. – user541686 Jan 07 '14 at 00:32
  • @DietmarKühl The bounty is expiring soon. Although I find Mehrdad's and Yakk's answer more convincing at the moment (probably because they say what I wanted to hear), I would like to ask you to please somehow move your comment *"I don't think considering algorithms customization points is the right approach. [...]"* into your answer and please expand on it with examples. Maybe your approach is better / less twisted and I would like to understand what you mean, preferably with examples. Many thanks! – Ali Jan 12 '14 at 12:03
6

(Elaborating on a comment)

I think it's possible to supply a predicate that is not equivalent to the one supplied to std::set and still fulfil the requirement of partially sorted (for special sets). So you can only replace the lower_bound algorithm by a special red-black version if the predicate is equivalent to the std::set ordering.

Example:

#include <utility>
#include <algorithm>
#include <set>
#include <iostream>

struct ipair : std::pair<int,int>
{
    using pair::pair;
};

bool operator<(ipair const& l, ipair const& r)
{  return l.first < r.first;  }

struct comp2nd
{
    bool operator()(ipair const& l, ipair const& r) const
    {  return l.second > r.second; /* note the > */ }
};

std::ostream& operator<<(std::ostream& o, ipair const& e)
{  return o << "[" << e.first << "," << e.second << "]";  }

int main()
{
    std::set<ipair, comp2nd> my_set = {{0,4}, {1,3}, {2,2}, {3,1}, {4,0}};
    for(auto const& e : my_set) std::cout << e << ", ";

    std::cout << "\n\n";

    // my_set is sorted wrt ::operator<(ipair const&, ipair const&)
    //        and       wrt comp2nd
    std::cout << std::is_sorted(my_set.cbegin(), my_set.cend()) << "\n";
    std::cout << std::is_sorted(my_set.cbegin(), my_set.cend(),
                                comp2nd()) << "\n";

    std::cout << "\n\n";

    // implicitly using operator<
    auto res = std::lower_bound(my_set.cbegin(), my_set.cend(), ipair{3, -1});
    std::cout << *res;

    std::cout << "\n\n";

    auto res2 = std::lower_bound(my_set.cbegin(), my_set.cend(), ipair{-1, 3},
                                 comp2nd());
    std::cout << *res2;
}

Output:

[0,4], [1,3], [2,2], [3,1], [4,0], 

1
1

[3,1]

[1,3]
dyp
  • 38,334
  • 13
  • 112
  • 177
  • 1
    Of course, you can still provide some mechanism to dispatch the `lower_bound` to a red-black version if the used predicate is equivalent to the `std::set::key_comp()` – dyp Jan 05 '14 at 14:52
  • Thanks. I should have made it clear in the question that I assume that the `lower_bound()` and the `set` use the same predicate. – Ali Jan 05 '14 at 14:53
  • 1
    @Ali You'd still had to check that, I think. So you can't just use a general tag-dispatch for red-black iterators (in my edited example, you'll see that the default predicate chosen by `lower_bound` can be different from the predicate of the set as well; even if you don't supply a non-default predicate to `set`, name lookup might differ). – dyp Jan 05 '14 at 14:57
  • 1
    I think it is the user's responsibility to provide `lower_bound` with the correct predicate. If the user messes up, there is nothing we can do. I simply can't decide whether your example isn't an example of a user's mistake. – Ali Jan 05 '14 at 15:01
  • I am not sure I fully understand your example. Is it possible to make `lower_bound` run in logarithmic time if the `set` and `lower_bound` use the same predicate ? Is it possible to decide whether the predicates are the same if you don't have access to the container itself? – Ali Jan 05 '14 at 22:55
  • 1
    @Ali As Dietmar Kühl says, it might be possible to get O(logN) runtime if there are parent pointers. Checking the equivalence of the predicate *does* require access to the container if the comparison is non-trivial (imagine a predicate function object with data members). The type of the predicate can be embedded in the type of iterator, though (that should be sufficient for `std::less` and other stateless predicates). It might also be impossible to check for equivalence (predicate needed to be EqualityComparable). – dyp Jan 05 '14 at 23:35
  • Mehrdad says that all the implementations that he is aware of use parent pointers. So no, it is not a real issue. A quick google search shows that there are RB trees out there without parent pointer but their superiority is questionable, see for example [Left-Leaning Red-Black Trees Considered Harmful](http://www.read.seas.harvard.edu/~kohler/notes/llrb.html). OK, so the only real issue I see is the case when the predicate has data members. – Ali Jan 05 '14 at 23:58
  • Red Black trees are not the only data structure which satisfies set and map; many data structures, and more than a few in common use, satisfy the constraints. Skip lists were used in many projects, due to the thought they required less space on average. Various other forms of BST's have been used, notably the AA tree which is widely used in personal implementations because it is simpler than a RB tree, and the AVL tree because it is more strict and easier to reason about. Don't assume a red black tree; the standard doesn't. – Alice Jan 08 '14 at 20:35
  • @Alice Although you're right that red-black trees are not mandated, the argument I want to point out in my answer is related to the *ordering* used by `std::set`, which is independent of the data structure used to implement `set`. Ali points out that libstdc++ could provide a special implementation of `lower_bound` for red-black tree iterators (via dispatch), as libstdc++ uses red-black trees for its `std::set` (i.e. an implementation-specific optimization). This is what the comments relating to red-black trees are referring to. – dyp Jan 08 '14 at 20:55
6

Great question. I honestly think there's no good/convincing/objective reason for this.

Almost all the reasons I see here (e.g. the predicate requirement) are non-issues to me. They might be inconvenient to solve, but they're perfectly solvable (e.g. just require a typedef to distinguish predicates).

The most convincing reason I see in the topmost answer is:

Although it is likely that there are parent pointers, requiring so for the tree seems inappropriate.

However, I think it's perfectly reasonable to assume parent pointers are implemented.

Why? Because the time complexity of set::insert(iterator, value) is guaranteed to be amortized constant time if the iterator points to the correct location.

Consider that:

  1. The tree must stay self-balancing.
  2. Keeping a tree balanced requires looking at the parent node at every modification.

How can you possibly avoid storing parent pointers here?

Without parent pointers, in order to ensure the tree is balanced after the insertion, the tree must be traversed starting from the root every single time, which is certainly not amortized constant time.

I obviously can't mathematically prove there exists no data structure that can provide this guarantee, so there's clearly the possibility that I'm wrong and this is possible.
However, in the absence of such data structures, what I'm saying is that this is a reasonable assumption, given by the fact that all the implementations of set and map I've seen are in fact red-black trees.


Side note, but note that we simply couldn't partially-specialize functions (like lower_bound) in C++03.
But that's not really a problem because we could have just specialized a type instead, and forwarded the call to a member function of that type.

user541686
  • 205,094
  • 128
  • 528
  • 886
  • A quick google search shows that there are RB trees out there without parent pointer but their superiority is questionable, see for example [Left-Leaning Red-Black Trees Considered Harmful](http://www.read.seas.harvard.edu/~kohler/notes/llrb.html). So assuming parent pointers is quite reasonable. OK, so the only real issue I see is the case when the predicate has data members: DyP says that in this case we need access to the container. Any thoughts on that? – Ali Jan 06 '14 at 00:01
  • @Ali: Oh I certainly did *not* mean that *all* RB trees I'd seen use parent pointers! That's certainly not the case -- in fact, I've seen that link before. What I meant was that all implementations of `std::map` or `std::set`s I had seen use parent pointers *because they need to provide the O(1) amortized time complexity guarantee* when the iterator is given, and I don't think they can do so otherwise. A generic RB tree need not satisfy this. Regarding the predicate -- yes, that requires the nodes to hold pointers back to their containers, but there's nothing (?) preventing that is there? – user541686 Jan 06 '14 at 00:39
  • @Ali: Alternatively the comparator could be stored elsewhere on the heap to avoid issues when containers are `swap`ped, if necessary. – user541686 Jan 06 '14 at 00:41
  • It can be done otherwise; the iterator itself could maintain a store of the nodes it's already gone through, similar to a naive recursive tree walk. I've seen this method used several times when the tree is large and the number of iterators is small. I would not be surprised to see such an implementation in a high performance STL-like library, as it makes lock-free implementations easier and optimizes for the normal case of low iterators. – Alice Jan 08 '14 at 20:30
  • @Alice: Wait what do you mean? Doesn't that require O(n) storage inside each iterator which is pretty expensive? Also, doesn't that mean the iterators will get invalidated once the parent of a right-child node is erased? – user541686 Jan 10 '14 at 19:00
  • O(log n), depth of the current branch of the tree. And no, if the nodes are stored through a weak pointer, then as long as at least one node is still valid it can skip the erased ones. These tactic is mainly used in lock free maps; the iterators are reused so the cost is not high, and the cost of additional parent pointers is that nodes can only be lazily removed. By moving all views into the data structure to "heavy" iterators, it can be deduced who points to what, and true freeing of node memory can be done. – Alice Jan 10 '14 at 21:27
  • @Alice: Interesting... Though that implies log n cost for creating and destroying iterators. I expected the standard to give time bounds on iterators but I guess it doesn't? – user541686 Jan 10 '14 at 22:18
  • [Threaded trees](https://en.wikipedia.org/wiki/Threaded_binary_tree) can also give constant-size iterators with constant-time next and previous queries, and don't require parent pointers (they reuse the space of NIL child pointers). I think they can also provide the time guarantees of `insert` with a hint. – Fred Foo Jan 14 '14 at 15:55
  • @larsmans: Can they really? In order to re-balance the tree you need access to the parent, don't you? You can't get parent access in constant time... – user541686 Jan 14 '14 at 20:52
  • You're right, the insertion routine I had in mind would unbalance the tree. Maybe in a splay tree it would be acceptable, but the rest of the `set`/`map` specification seems to rule that out. – Fred Foo Jan 14 '14 at 21:38
  • @Mehrdad Creating and destroying the iterator would both be constant time (if one assumes the allocator is O(1) which, while not always or even usually true, is usually assumed), but the constant would be large. The height of the tree is predictably bounded (if you have X nodes in the tree, and it's arranged with Y branching, the size is some function log Y of X), so there is no implication of log n iterators. – Alice Jan 15 '14 at 16:43
  • @Alice: I'm confused, what is "n" in your explanation? – user541686 Jan 15 '14 at 19:49
  • @Mehrdad Same as in any data structure; all items inserted into the data structure. – Alice Jan 16 '14 at 01:16
  • @Alice: But I thought you just said there are *X* nodes in your data structure, not *n*? – user541686 Jan 16 '14 at 01:20
  • That was just an example. X = n. The point was that you can predict the depth of the tree or make worst case assumptions about it using constant time data, such as the known number of nodes in the tree. – Alice Jan 16 '14 at 02:17
  • @Alice: Uh what?? that's kinda silly... The whole *point* of big-O is to analyze what happens when n increases. If you're bounding n then everything in the world runs in constant time, and that's a completely useless statement to make. – user541686 Jan 16 '14 at 02:28
  • What are you talking about? I didn't bind anything to n at all. The point is that each iterator creation is constant time; it makes a single call to the allocator, allocating enough memory to store nodes as it progresses around the tree. It only needs to store as many nodes as the tree is high, which is a constant time calculation. None of that is binding; the amount needed changes as the tree gets bigger. If your allocator is O(1) (probably with some high constant C), then the iterator operations are still within the STL bounds. – Alice Jan 16 '14 at 03:05
  • @Alice: I'm not talking about the allocator, I'm talking about your statement "it only needs to store as many nodes as the tree is high". There are n nodes in the tree. Storing those nodes takes log(n) time and space. How is this constant-time!? – user541686 Jan 16 '14 at 03:23
  • @Mehrdad it can allocate all of the storage space for the nodes in constant time, since it's known. That's O(1) time, O(log n) storage. It uses that as a stack. Each time it goes down, it pushes a parent node onto it, which is an O(1) operation. At no time does it invoke an O(log n) operation; it's mere an O(1) on top of the traditional traversal operations. You are confusing when it does the storage with allocation for the storage. – Alice Jan 16 '14 at 04:21
  • @Alice: No I'm not confusing the two, I'm saying there's no reason why the traversal would begin in the root of the tree. It may very well begin somewhere else, in which case it would need to push log n nodes onto the stack. – user541686 Jan 16 '14 at 04:35
6

There is no technical reason why this could not be implemented.

To demonstrate, I will sketch out a way to implement this.

We add a new Iterator category, SkipableIterator. It is a subtype of BiDirectionalIterator and a supertype of RandomAccessIterator.

SkipableIterators guarantee that the function between done in a context where std::between is visible works.

template<typeanme SkipableIterator>
SkipableIterator between( SkipableIterator begin, SkipableIterator end )

between returns an iterator between begin and end. It returns end if and only if ++begin == end (end is right after begin).

Conceptually, between should efficiently find an element "about half way between" begin and end, but we should be careful to allow a randomized skip list or a balanced red black tree to both work.

Random access iterators have a really simple implementation of between -- return (begin + ((end-begin)+1)/2;

Adding a new tag is also easy. Derivation makes existing code work well so long as they properly used tag dispatching (and did not explicitly specialize), but there is a small concern of breakage here. We could have "tag versions" where iterator_category_2 is a refinement of iterator_category (or soemthing less hacky), or we could use a completely different mechanism to talk about skipable iterators (an independent iterator trait?).

Once we have this ability, we can write a fast ordered searching algorithms that works on map/set and multi. It would also work on a skip list container like QList. It might be even the same implementation as the random access version!

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • What asymptotic runtime does `between` have for tree iterators? (And how is it used to implement a binary search? Is is just a replacement for `distance`+`advance`?) – dyp Jan 06 '14 at 12:42
  • @DyP: Probably amortized or expected O(`log(distance(begin, end))`). – user541686 Jan 06 '14 at 19:23
  • @DyP All that matters is that `between` is faster than `distance`+`advance` amortized, which should be easy in `O` notation scale for a balanced binary bidirectional iterable tree. Ideally, you'd mimic `set::lower_bound`'s search order. The point is that sometimes `between` is *much* faster than `advance`, as `advance` says "I want to move exactly so far", when all we want to do is move *about* half way to the `end`. (Note that if the tree was fast indexable (each node tracked count of children, say), a log-speed `advance` could be implemented) – Yakk - Adam Nevraumont Jan 06 '14 at 19:32
  • Hmm... To me that sounds like `lower_bound` with `SkipableIterator`s is still asymptotically slower than `set::find`, something like O(logN * logN), as each step in the binary search requires a `between`, which is O(logN) (that N is halfed for each step, but that seems not to matter asymptotically). That's better than `lower_bound` with a O(N) `advance`, but still wouldn't make the member function obsolete. Or did you have a different algorithm in mind? – dyp Jan 06 '14 at 20:08
  • @DyP you might be able to reach amortized constant time on recursive calls to zero size: track how you'd walk a (somewhat) balanced binary tree. There may be a constant factor that a member could pull off (because it knows it starts with the entire tree). (The note about log-speed `advance` was a side note, not how I'd expect `between` to be implemented: `between` explicitly does not care about where it ends up exactly, while `advance` may have to descend needlessly down the tree to find the exact spot it wants to be) – Yakk - Adam Nevraumont Jan 06 '14 at 20:40
4

Here's a very simple non-technical reason: It's not required by the standard, and any future change will break backwards compatibility with existing compiled code for no reason.

Wind the clock back to the early 2000's, during the transition between GCC and GCC 3, and later, during minor revisions of GCC 3. Many of the projects I worked on were meant to be binary compatible; we could not require the user to recompile our programs or plugins, and neither could we be certain of the version of GCC they were compiled on or the version of the STL they were compiled against.

The solution: don't use the STL. We had in-house strings, vectors, and tries rather than using the STL. The solution to the dependency hell introduced by an ostensibly standard part of the language was so great, that we abandoned it. Not in just one or two projects either.

This problem has largely gone away, thankfully, and libraries such as boost have stepped in to provide include only versions of the STL containers. In GCC 4, I would see no issue with using standard STL containers, and indeed, binary compatibility is much easier, largely due to standardization efforts.

But your change would introduce a new, unspoken, dependency

Suppose tomorrow, a new data structure comes out, which substantially beats red black trees, but does not provide the guarantee that some specialized iterators are available. One such implementation that was very popular just a few years ago was the skip list, which offered the same guarantees at a possibly substantially smaller memory footprint. The skip list didn't seem to pan out, but another data structure very well could. My personal preference is to use tries, which offer substantially better cache performance and more robust algorithmic performance; their iterators would be substantially different from a red black trees, should someone in the libstdc++ decide that these structures offer better all around performance for most usages.

By following the standard strictly, binary backwards compatibility can be maintained even in the face of data structure changes. This is a Good Thing (TM) for a library meant to be used dynamically. For one that would be used statically, such as the Boost Container library, I would not bat an eye if such optimizations were both well implemented and well used.

But for a dynamic library such as libstdc++, binary backwards compatibility is much more important.

Alice
  • 3,958
  • 2
  • 24
  • 28
  • Would a change in an algorithm (template) make binaries incompatible? – dyp Jan 08 '14 at 20:58
  • It depends on the implementation of the dynamic link. Some parts of the library (the template, for example) will be in the linking program, while others (the implementation of the underlying structure) will not be. Suppose the template is a wrapper, which wraps generic calls to an underlying datastructure that uses void*'s in the shared library (a common way to reduce bloat allowed by the standard). If the compiled in template refers to iterators that are now not used in the shared library, we have a breaking change. The standard is policy, not implementation, for this very reason. – Alice Jan 08 '14 at 21:22
  • I should point out, this is a fundamental conflict between generics and objects, which is solved through a primitive form of type erasure; either vector and vector are fundamentally different classes and thus no code can be shared between them, or some form of type erasure must occur and "vector" of any instantiate can share code. The first is clearly what happens in a header include only static library (like boost container), and the second is what we would prefer in a dynamic library, like libstdc++. – Alice Jan 08 '14 at 21:30