libc++'s implementation of std::map/set::equal_range gives unexpected results
Asked Answered
H

1

6

I noticed std::set::equal_range (same for std::map) in clang's libc++ gives different result than libstdc++. I always assumed equal_range should return equivalent of std::make_pair(set.lower_bound(key), set.upper_bound(key)) which is what cppreference says and libstdc++ does. In libc++ however I have a code which gives a different result.

#include <set>
#include <iostream>
#include <iterator>

struct comparator {
    using range_t = std::pair<int, int>;
    using is_transparent = std::true_type;
    bool operator()(int lhs, int rhs) const
    {
        return lhs < rhs;
    }
    bool operator()(int lhs, range_t rhs) const
    {
        return lhs < rhs.first;
    }
    bool operator()(range_t lhs, int rhs) const
    {
        return lhs.second < rhs;
    }
};

using range_set = std::set<int, comparator>;

int main()
{
    range_set set = { 1, 3, 6, 10 };
    auto range = comparator::range_t{2, 7};
    auto eq = set.equal_range(range);
    auto low = set.lower_bound(range);
    auto high = set.upper_bound(range);
    std::cout << "equal_range returned " << std::distance(eq.first, eq.second) << " elem(s): ";
    std::copy(eq.first, eq.second, std::ostream_iterator<int>(std::cout, " "));
    std::cout << "\nlower/upper returned " << std::distance(low, high) << " elem(s): ";
    std::copy(low, high, std::ostream_iterator<int>(std::cout, " "));
    std::cout << '\n';
    return 0;
}

As you can see at https://rextester.com/CLTS82056 this gives

equal_range returned 1 elem(s): 3 
lower/upper returned 2 elem(s): 3 6 

With libstdc++ as well as with boost::container::set I get 2 elements (which I expect) in both cases.

Of course I can manually call lower_bound and upper_bound in my code but using equal_range is shorter, clearly shows my intention (I want to find all elements which fall into the given range) and might be faster (because equal_range method could be coded as one tree traversal without actual calls to lower_bound and upper_bound).

Interestingly enough, changing container type to std::multiset in this case makes equal_range return 2 elements.

Now is this a bug in libc++ or does cppreference give wrong hint in https://en.cppreference.com/w/cpp/container/set/equal_range:

Alternatively, the first iterator may be obtained with lower_bound(), and the second with upper_bound().

Hochheimer answered 18/8, 2019 at 14:29 Comment(1)
It looks to me like the effective result your range comparator's strict weak ordering is implemented in a way that results in 3 and 6 in the std::set compare equal to the range value that gets passed in to equal_range. Given that std::set is supposed to contain unique values only, the libc implementation might be assuming that once it finds a value that compares equal, that's all the set has. I find nothing in a brief review of what N4659 says that supports this. The only thing I see is that comparator should define is_transparent. Not sure, but I'll point the finger at libc.Beem
C
5

Your code is fine.

You are testing on an outdated libc++. This is bug 30959, fixed in 2018 and available in clang 7.

Rextester's clang is apparently 3.8.

Cooker answered 18/8, 2019 at 15:43 Comment(2)
Great to hear that, I was using clang 6 on my side and seems it is finally time to upgradeHochheimer
Heh, I could've sworn I scribbled down something about partitioning being the only requirement for transparent comparisons, a long time ago. Must've been a figment of my imagination...Beem

© 2022 - 2024 — McMap. All rights reserved.