Why does the C++ standard not change std::set to use std::less<> as its default template argument?

Viewed 418
#include <set>
#include <string>
#include <string_view>

using namespace std::literals;

int main()
{
    auto v1 = std::set<std::string, std::less<>>{"abc"s};
    v1.contains("abc"s);  // ok
    v1.contains("abc"sv); // ok
    
    auto v2 = std::set{"abc"s};
    v2.contains("abc"s);  // ok
    v2.contains("abc"sv); // error
}

v1.contains("abc"sv); is more efficient than v1.contains("abc"s);, because it needn't to construct a string object.

However, the C++ standard uses std::less<T>, rather than std::less<>, as std::set's default template argument. So, CTAD (Class Template Argument Deduction) doesn't work on std::less<>, I have to write ugly std::set<std::string, std::less<>>{"abc"s}, rather than std::set{"abc"s}.

Why does the C++ standard not change std::set to use std::less<> as its default template argument? Just for backward compatibility?

3 Answers

Moving from std::set<T, std::less<T>> to std::set<T, std::less<>> can make key-finding algorithms more efficient, if the search-key never has to be converted.

Conversely, it can make them less efficient if the conversion occurrs on every call to the comparator, instead of once in the caller on starting the algorithm. Those conversions can be quite expensive.

Especially if the conversion from search-key to T is lossy, there isn't even a guarantee both would yield the same result!

For these reasons, such a change is not a straight upgrade, but a breaking change. And the committee is quite loath to introduce those.

Why does the C++ standard not change std::set to use std::less<> as its default template argument? Just for backward compatibility?

This would be an ABI break.

// a.cpp
void f(std::set<int> const&) { ... }

// b.cpp
void g() {
    std::set<int> s = /* ... */;
    f(s);
}

If a.cpp is compiled on C++11, f takes a std::set<int, std::less<int>>.

If the standard library changed the default comparison from std::less<T> to std::less<void> in C++14 (N3421 was only written in 2012), and b.cpp was compiled on C++14, then s would be a std::set<int, std::less<void>>. And now we fail to link.

My guess is because they thought it wasn't so great improvement to break backward compatibility.

Another reason is because std::set with std::less<Key> existed even before C++11 (starting from C++03 I guess) and std::less<> appeared only in C++14. So if they move to std::set with std::less<> then you have to force C++14 when using set, or you have to make two kinds of sets - one for C++03 and one for C++14.

Also if you start from C++14 making set being std::less<>-based then your pre-C++14 code will start to behave differently sometimes. For example your code relied on calling Key's constructor for some reason when adding to set, then suddenly if you add option -std=c++14 your old code starts doing other things.

Usually STD people make such changes that switching from C++11 to C++14 doesn't break code's behaviour. Only downswitching from C++14 to C++11 can break something(usually non-compiling). In other words it is backward compatibility breaking change to use std::less<>. Such changes are usually only done by introducing new class name for such specializations.

Related