Document number: | N3950 |
Date: | 2014-02-19 |
Project: | Programming Language C++, Language Evolution Working Group |
Reply-to: | Oleg Smolsky oleg.smolsky@gmail.com |
Provide means of generating default equality, inequality and comparison member operators for user-defined types. This is strictly an "opt in" feature so that semantics of existing code remain intact.
This feature would be useful for modern C++ code that operates with types composed of "regular" members. The definition of equality is trivial in such cases - member-wise comparison. Inequality can then be generated as an inverse.
This proposal is based on the notion of "regular" types that naturally compose. Such cases are becoming more prevalent as people program more with value types and writing (in)equality manually becomes tiresome. This is especially true when trying to lexicographically compare members.
Consider the following trivial example where a C++ type represents some kind of a user record:
struct user { uint32_t id, rank, position; std::string first_name, last_name; std::string address1, address2, city, state, country; uint32_t us_zip_code; bool operator==(const user &) const; bool operator!=(const user &) const; bool operator<(const user &) const; bool operator>=(const user &) const; };
bool user::operator==(const user &r) const { return id == r.id && rank == r.rank && position = r.position && address1 == r.address1 && address2 == r.address2 && city == r.city && state == r.state && country == r.country && us_zip_code == r.us_zip_code; } bool user::operator<(const user &r) const { // Can implement a full lexicographical comparison of members, but can // also cheat by using standard libraries return std::tie(id, rank, position, address1, address2, city, state, country, us_zip_code) < std::tie(r.id, r.rank, r.position, r.address1, r.address2, r.city, r.state, r.country, r.us_zip_code); }Specifically, this code, while technically required, suffers from the following issues:
It is vital that equal/unequal, less/more-or-equals and more/less-or-equal pairs
behave as boolean negations of each other. After all, the world would make no sense
if both operator==()
and operator!=()
returned false!
As such, it is common to implement these operators in terms of each other:
bool user::operator!=(const user &r) const { return !(*this == r); } bool user::operator!=(const user &r) const { return !(*this == r); } bool user::operator>=(const user &r) const { return !(*this < r); } bool user::operator>(const user &r) const { return r < *this; } bool user::operator<=(const user &r) const { return !(*this > r); }Specifically:
operator<()
must remain transitive in its nature.
Member-wise generation of special functions is already present in the Standard (see Section 12), so it seems natural to extend the scope of generation and reuse the existing syntax.
The proposed syntax for generating the new "explicitly defaulted" member functions is as follows:
struct Thing { int a, b, c; std::string d; bool operator==(const Thing &) const = default; bool operator<(const Thing &) const = default; bool operator!=(const Thing &) const = default; bool operator>=(const Thing &) const = default; bool operator>(const Thing &) const = default; bool operator<=(const Thing &) const = default; };
I feel this is a natural choice because:
It is possible to mandate that every explicitly defaulted operator is to be implemented in a member-wise fashion. In fact, it would we consistent with copy construction, assignment and equality. However:
I have a working prototype implementation using Clang that does the following:
operator<()
is implemented via a call to std::tie()
The following additional work is needed to get closer to production quality:
The following operator member functions can be explicitly defaulted:
operator==()
and operator!=()
[class.equality]
operator<()
, operator>()
,
operator<=()
and operator>=()
[class.comparison]
The default constructor (12.1), copy constructor and copy assignment operator (12.8), move constructor and move assignment operator (12.8) and destructor (12.4) are special member functions. These, together with equality operators (12.10) and comparison operators (12.11) can be explicitly defaulted as per [dcl.fct.def.default]
12.10 Equality operators [class.equality]
= default
notation as these member functions can be explicitly
defalted as per [dcl.fct.def.default].
operator==()
is generated if and only if all
sub-objects and base classes are intergal types or provide operator==()
operator==()
with
the subobject as the object expression and the corresponding subobject of x as a
single function argument (as if by explicit qualification; that is, ignoring any
possible virtual overriding functions in more derived classes);
operator==()
and returns a boolean negation of the result
12.11 Comparison operators [class.comparison]
= default
notation as these member functions can be explicitly defaulted
as per [dcl.fct.def.default].
operator<()
is generated if and only if all
sub-objects and base classes are integral types or provide operator<()
operator<()
for a non-union class X performs
lexicographical comparison of member values in a manner compatible to
std::tie()
.
Direct base classes of X are compared first, in the order of their declaration in the
base-specifier-list, and then the immediate non-static data members of X are compared,
in the order in which they were declared in the class definition.operator<()
with
the subobject as the object expression and the corresponding subobject of x as a
single function argument (as if by explicit qualification; that is, ignoring any
possible virtual overriding functions in more derived classes);
operator>=()
for a non-union class X performs a call
to operator<()
and returns a boolean negation of the result
operator>()
for a non-union class X performs a call
to operator<()
but reverses the arguments
operator<=()
for a non-union class X performs a call
to operator>()
and returns a boolean negation of the result
operator==()
implicitly? How do we
deal with previously defined non-member operators? (Perhaps we can allow non-member
operators to hide implicitly generated member ones?).
operator<()
performs
member comparisons in a manner compatible to std::tie()
. Such a
statement is easy to write and prototype, but, if taken literarily, puts an
unusual dependency between the core language and the standard library. It may be
better to spell out what a "lexicographical comparison" is.
The fundamental idea comes from Alex Stepanov as his work revolves around "regular" types. Such types should be automatically copied, assigned and compared. The first two points have been in the C++ language from the beginning and this proposal attempts to address the last one.
I want to thank Andrew Sutton for early feedback and guidance as well as Daniel Krügler for detailed corrections and suggestions.