In 2014, Oleg Smolsky proposed to add default member functions defining comparisons. That re-ignited a decades-old debate. I once asked Dennis Richie why C didn’t provide a built-in == (equality) the way it provides a built-in = (copy). The answer was that until 1978 C didn’t have = for structs either, and copying could be efficiently implemented by something like memcpy(), but comparison couldn’t because the layout of structs included “holes” caused by alignment that had to be ignored. The days where compilers could handle individual members one by one and still generate optimal code for them were still far in the future, so I postponed dealing with comparisons. If you wanted one, you could write it yourself. That’s true, but many people, including Alex Stepanov, have objected to the inconvenience.
The killer argument for default comparisons is not actually convenience, but the fact that people get their equality operators wrong. They forget to compare all elements (especially when updating a struct with a new member). Also, given inheritance, a X::operator==(const X&) is easy to misuse (whether virtual or not). We get slicing (b==d will work even if b and d are of different classes in a hierarchy, and usually give a wrong answer). If people define operator==() as a member; then the left- and right-hand operand obey different conversion rules. Also, defining the semantics for == and != is relatively simple, but there are subtleties about other operators, such as <=; does its meaning involve < and == or < and !? Contrary to “common knowledge” there seems to be little difference in performance.
Lots of people joined the discussions and the discussions went all over the place. How do we handle pointers? (we don’t), how do we handle mutable members?, should we use static reflection?, etc., etc., At times, I thought that we would get nothing. That wouldn’t be a tragedy because default comparisons are not on my (mostly mythical) top-20 list of desirable improvements to C++. However:
- I started trying to design == in analogy to = as if Dennis and I had been doing it in 1980 or so.
- Jens Maurer helped us with wording, making sure the rules were consistent.
- Herb Sutter helped ensure that lookup rules were sane from a programmer’s point of view.
Using namespaces and operator==(), you can (today) construct programs for which a==b is true in one place and false in another. That’s unavoidable if you treat == as an ordinary function. But == is not ordinary! Its semantics is fixed by the rules of logic (we cannot have both a==b and a!=b), and its meaning must match that of copying (e.g., after a=b we must have a==b). Secondly, slicing is evil. Defining = in terms of references was a mistake. As far as I can remember, this is the only place in C++ where I made a change based solely on the advice of a theoretician: “OO is based on references, so for uniformity == should be based on references.” But there is nothing OO about ==! Each class (in a hierarchy or not) has its own ==, just like it has its own =. You can define “odd” assignments and comparisons yourself. That’s your problem, but the default must be sane and safe.
Jens suggested the obvious solution: just ban slicing for both == and =. I had shied away from this incompatible change, but the result is beautiful. If the committee approves, we have a coherent set of rules and we eliminate some subtle nasty bugs! To preserve the techniques of tag dispatching, the rule is that you cannot slice if the derived class has added a non-static data member. That is, the derived to base conversion works unless it produces a sliced object.
- Oleg Smolsky: Default Comparison Operators. And several follow-up papers.
- Bjarne Stroustrup: Default Comparisons.
- A. Tomazos, M. Spertus: Defaulted Comparison Using Reflection.
- Michael Price: Defaulted comparison operator semantics should be uniform.
- Jens Maurer: Proposed wording for default comparisons, revision 2.