std::map Requirements for Keys (Design Decision)

半世苍凉 提交于 2020-01-01 08:25:31

问题


When I make a std::map<my_data_type, mapped_value>, what C++ expects from me is that my_data_type has its own operator<.

struct my_data_type
{
    my_data_type(int i) : my_i(i) { }

    bool operator<(const my_data_type& other) const { return my_i < other.my_i; }

    int my_i;
};

The reason is that you can derive operator> and operator== from operator<. b < a implies a > b, so there's operator>. !(a < b) && !(b < a) means that a is neither less than b nor greater than it, so they must be equal.

The question is: Why hasn't the C++ designer require operator== to be explicitly defined? Obviously, operator== is inevitable for std::map::find() and for removing duplicates from the std::map. Why implement 5 operations and call a method twice in order not to compel me to explicitly implement operator==?


回答1:


operator== is inevitable for std::map::find()

This is where you go badly wrong. map does not use operator== at all, it is not "inevitable". Two keys x and y are considered equivalent for the purposes of the map if !(x < y) && !(y < x).

map doesn't know or care whether you've implemented operator==. Even if you have, it need not be the case that all equivalent keys in the order are equal according to operator==.

The reason for all this is that wherever C++ relies on orders (sorting, maps, sets, binary searches), it bases everything it does on the well-understood mathematical concept of a "strict weak order", which is also defined in the standard. There's no particular need for operator==, and if you look at the code for these standard functions you won't very often see anything like if (!(x < y) && !(y < x)) that does both tests close together.

Additionally, none of this is necessarily based on operator<. The default comparator for map is std::less<KeyType>, and that by default uses operator<. But if you've specialized std::less for KeyType then you needn't define operator<, and if you specify a different comparator for the map then it may or may not have anything to do with operator< or std::less<KeyType>. So where I've said x < y above, really it's cmp(x,y), where cmp is the strict weak order.

This flexibility is another reason why not to drag operator== into it. Suppose KeyType is std::string, and you specify your own comparator that implements some kind of locale-specific, case-insensitive collation rules. If map used operator== some of the time, then that would completely ignore the fact that strings differing only by case should count as the same key (or in some languages: with other differences that are considered not to matter for collation purposes). So the equality comparison would also have to be configurable, but there would only be one "correct" answer that the programmer could provide. This isn't a good situation, you never want your API to offer something that looks like a point of customization but really isn't.

Besides, the concept is that once you've ruled out the section of the tree that's less than the key you're searching for, and the section of the tree for which the key is less than it, what's left either is empty (no match found) or else has a key in it (match found). So, you've already used current < key then key < current, leaving no other option but equivalence. The situation is exactly:

if (search_key < current_element)
    go_left();
else if (current_element < search_key)
    go_right();
else
    declare_equivalent();

and what you're suggesting is:

if (search_key < current_element)
    go_left();
else if (current_element < search_key)
    go_right();
else if (current_element == search_key)
    declare_equivalent();

which is obviously not needed. In fact, it's your suggestion that's less efficient!




回答2:


Your assumptions aren't correct. Here's what's really happening:

std::map is a class template which takes four template parameters: key type K, mapped type T, comparator Comp and allocator Alloc (the names are immaterial, of course, and only local to this answer). What matters for this discussion is that an object Comp comp; can be called with two key refrences, comp(k1, k2), where k1 and k2 are K const &, and the result is a boolean which imlpements a strict weak ordering.

If you do not specify the third argument, then Comp is the default type std::less<K>, and this (stateless) class imlpements the binary operation as k1 < k2. It does not matter whether this <-operator is a member of K, or a free function, or a template, or whatever.

And that wraps up the story, too. The comparator type is the only datum required to implement an ordered map. Equality is defined as !comp(a, b) && !comp(b,a), and the map only stores one unique key according to this definition of equality.

There is no reason to make additional requirements on the key type, and also there is no logical reason that a user-defined operator== and operator< should at all be compatible. They could both exist, independently, and serve entirely different and unrelated purpose.

A good library imposes the minimal necessary requirements and offers the greatest possible amount of flexibility, and this is precisely what std::map does.




回答3:


In order to find the element i within the map, we have traversed to element e the tree search will already have tested i < e, which would have returned false.

So either you call i == e or you call e < i, both of which imply the same thing given the prerequisite of finding e in the tree already. Since we already had to have an operator< we don't rely on operator==, since that would increase the demands of the key concept.




回答4:


You have a faulty assumption:

!(a < b) && !(b < a) means that a is neither less than b nor greater than it, so they must be equal.

It means that they are equivalent, but not necessarily equal. You are free to implement operator< and operator== in such a way that two objects can be equivalent but not equal.

Why hasn't the C++ designer require operator== to be explicitly defined?

To simplify the implementation of types that can be used as keys, and to allow you to use a single custom comparator for types without overloaded operators. The only requirement is that you supply a comparator (either operator< or a custom functor) that defines a partial ordering. Your suggestion would require both the extra work of implementing an equality comparison, and the extra restriction of requiring equivalent objects to compare equal.




回答5:


The reason why a comparison operator is needed is the way map is implemented: as a binary search tree, which allows you to look up, insert and delete elements in O(log n). In order to build this tree, a strict weak order must be defined for the set of keys. That's why only one operator definition is needed.



来源:https://stackoverflow.com/questions/9413777/stdmap-requirements-for-keys-design-decision

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!