Implementation divergence with a moved-from set comparator

Which 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:

Posted 2023-05-10