Implementation divergence with a moved-from set
comparator
set
comparatorWhich of the Big Three C++ vendors wrote your standard library? This code snippet will tell you (Godbolt):
struct C : std::less<> {
std::shared_ptr<int> s_;
explicit C(std::shared_ptr<int> s) : s_(s) {}
};
int main() {
auto p = std::make_shared<int>(42);
auto s = std::set<int, C>(C(p));
auto t = std::move(s);
t = std::move(s);
const char *libraries[] = {
"unknown",
"libc++",
"libstdc++",
"Microsoft STL",
};
std::cout << libraries[p.use_count()] << '\n';
}
That’s because vendors disagree as to what should happen when you move-from
a std::set
: should you move-from its comparator as well, or copy the comparator?
If you move-from the comparator, it’ll be left in a moved-from state, which means
that the set
is now radioactive (Godbolt):
using C = std::function<bool(int,int)>;
std::set<int, C> s({1,2,3}, std::less());
decltype(s) t;
t = std::move(s);
// leaves s.key_comp() in a moved-from state
s.insert({1,2,3});
// throws std::bad_function_call
The three vendors do three different things with set
’s special members:
- libc++: Move in the move constructor; move in the move-assignment operator
- libstdc++: Copy in the move constructor (for historical reasons?); move in the move-assignment operator
- Microsoft: Copy in the move constructor; copy in the move-assignment operator
This is a particularly bad scene because the C++ standard actually defines
a noexcept-specification for set::operator=
—
set& operator=(set&& x)
noexcept(allocator_traits<Allocator>::is_always_equal::value &&
is_nothrow_move_assignable_v<Compare>);
So Microsoft’s implementation will never enter a radioactive state,
but will do a “rogue std::terminate
”
if copying the comparator throws. It’s unclear if this behavior is conforming.
This area is the subject of LWG issue 2227, which has been
open since 2012. I’ve been interested in it since 2019 — see my CppCon talk
“Mostly Invalid: flat_map, Exception Guarantees, and the STL” —
but it’s back on my front burner this year because C++23 gives us flat_set
, which has exactly
the same problems in this area as C++98 set
. Will each vendor’s flat_set
choose to match the
behavior of their own set
? Or will we see consistency among vendors’ flat_set
implementations,
at the cost of consistency between each vendor’s flat_set
and set
?
See also:
- “Fetishizing class invariants” (2019-02-24), the blog post that led to that CppCon 2019 talk