Why is heterogenous std::*map lookup opt-in in C++?

Viewed 96

To support heterogeneous key lookup for a std::map one has to be a bit more verbose that in the olden days: (taken from the question on how to do it)

int main()
{
    {
        puts("The C++11 way makes a copy...");
        std::map<std_string, int> m;
        auto it = m.find("Olaf");
    }
    {
        puts("The C++14 way doesn't...");
        std::map<std_string, int, std::less<>> m;
        auto it = m.find("Olaf");
    }
}

Also see: https://www.cppstories.com/2021/heterogeneous-access-cpp20/ for an explanation.

From this link and from the linked question (and also from N3657) there a a few scattered reasons given as to why this is opt-in. Since I had to do quite some scrolling and I didn't find a succinct summary of the reasons, I would like to put together a summary here that every junior dev understands, as to

Why stupid C++ makes me write that extra std::less<> ??! Why doesn't it just allow transparent heterogeneous comparison with the default type we always used?

;-)

2 Answers

The map template has a default comparison

template<
    class Key,
    class T,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<std::pair<const Key, T> >
> class map;

Suppose you wanted a map<my_type, int> and enabled that by specializing std::less<my_type> (which is then used by the default).

Then C++14 couldn't just start using unspecialized std::less<> instead. So you have to explicitly ask for it!

If std::map would simple support this silently, that is if std::map<KeyT, ValT> would support find(LookupT) for "any compatible" type then:

implicitly supporting heterogeneous lookup can be dangerous, as the relationship between values might not be maintained after conversions. For example, 1.0 < 1.1, but static_cast<int>(1.0) == static_cast<int>(1.1). Thus, using a double to look up a value in a std::set<int> could lead to incorrect results.

  • Performance(!) - old code with types that do not support heterogeneous comparison would start to run slower: dyp wrote:

Consider there exists for some type stupid_string only a converting constructor from char const* but not a comparison operator operator<(stupid_string const&, char const*), only operator<(stupid_string const&, stupid_string const&). Since std::less<> ((would)) forward to the comparison operator, each comparison will create a new stupid_string.

((instead of just creating the extra comparison object once on the call site of std::find))

Related