Defaulted comparison operators (original) (raw)
Document number: | N3950 |
---|---|
Date: | 2014-02-19 |
Project: | Programming Language C++, Language Evolution Working Group |
Reply-to: | Oleg Smolsky oleg.smolsky@gmail.com |
I. Introduction
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.
II. Motivation and scope
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;
};
Verbosity
The structure consists of regular members and the implementation of the equality operator is trivial yet verbose:
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:
- needlessly verbose - every member is already equality comparable
- error prone - the author could miss a member, or the implementation may become stale when a new member is added
- not in the spirit of Modern C++: correct things should be intuitive and easy
Correctness
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:
- Based on my experience, this template is applicable to the vast majority of types that define comparison operators
- The users should have "short hand" for stating what is common, correct and simple
- These relations are algebraic in nature and must be considered carefully. For example,
operator<()
must remain transitive in its nature.
III. Design decisions
The proposed syntax
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:
- C++11 already has member function specifiers such as "default" and "delete"
- Users can "opt in" to get the new behavior
- Member functions are explicitly declared and are, hence, visible in the source code. This is generally helpful when humans read the code.
Other points of consideration
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:
- Such an implementation is not useful. It would generate uselss code which may be sizeable due to the number of members.
- The most logical choice for inequality is the boolean inverse of equality. The same applies to the other operators - they can all be derived.
- Users are still free to implement the operators in any way the see fit
IV. Implementation
I have a working prototype implementation using Clang that does the following:
- parses the proposed syntax
- declares and defines the new member functions for an arbitrary number of built-in and composite members
- Equality is generated in a member-wise fashion. Both built-in and user-defined types are supported.
operator<()
is implemented via a call tostd::tie()
The following additional work is needed to get closer to production quality:
- add support for base classes
- enhance diagnostics pertinent to non-regular members
- test against a large body of code
V. Technical specifications
Correction for 8.4.2 "Explicitly-defaulted functions [dcl.fct.def.default]"
- A function definition of the form:
attribute-specifier-seqopt decl-specifier-seqopt declarator virt-specifier-seqopt = default ;
is called an explicitly-defaulted definition. A function that is explicitly defaulted shall
— be a special member function, or an explicitly defaultable operator member function. See [defaultable]
New section in 8.4
8.4.4 Explicitly defaultable operator member functions [defaultable]
The following operator member functions can be explicitly defaulted:
- Equality operators:
operator==()
andoperator!=()
[class.equality] - Comparison operators:
operator<()
,operator>()
,operator<=()
andoperator>=()
[class.comparison]
Correction for "12 Special member function [special]"
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]
New section in 12
12.10 Equality operators [class.equality]
- A non-union class can provide overloaded equality and inequality operators as per [over.oper]. A default implementation can be generated via the
= default
notation as these member functions can be explicitly defalted as per [dcl.fct.def.default]. - The defaulted
operator==()
is generated if and only if all sub-objects and base classes are intergal types or provideoperator==()
Alternative: IFF they all satisfy the requirements of the EqualityComparable concept (17.6.3.1). - The implicitly-defined equality operator for a non-union class X performs memberwise equality comparison of its subobjects. 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.
Let x be either the parameter of the function or, for the move operator, an xvalue referring to the parameter. Each subobject is compared in the manner appropriate to its type:- if the subobject is of class type, as if by a call to
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); - if the subobject is an array, each element is compared, in the manner appropriate to the element type;
- if the subobject is of trivial type, the built-in "equality" operator is used.
- if the subobject is of class type, as if by a call to
- The implicitly-defined inequality operator for a non-union class X performs a call to
operator==()
and returns a boolean negation of the result
12.11 Comparison operators [class.comparison]
- A non-union class can provide overloaded comparison operators as per [over.oper]. A a default implementation via the
= default
notation as these member functions can be explicitly defaulted as per [dcl.fct.def.default]. - The defaulted
operator<()
is generated if and only if all sub-objects and base classes are integral types or provideoperator<()
Alternative: IFF they all satisfy the requirements of the LessThanComparable concept (17.6.3.1). - The implicitly-defined
operator<()
for a non-union class X performs lexicographical comparison of member values in a manner compatible tostd::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.
Let x be either the parameter of the function or, for the move operator, an xvalue referring to the parameter. Each subobject is compared in the manner appropriate to its type:- if the subobject is of class type, as if by a call to
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); - if the subobject is an array, each element is compared, in the manner appropriate to the element type;
- if the subobject is of trivial type, the built-in "less than" operator is used.
- if the subobject is of class type, as if by a call to
- The implicitly-defined
operator>=()
for a non-union class X performs a call tooperator<()
and returns a boolean negation of the result - The implicitly-defined
operator>()
for a non-union class X performs a call tooperator<()
but reverses the arguments - The implicitly-defined
operator<=()
for a non-union class X performs a call tooperator>()
and returns a boolean negation of the result
VI. Related ideas and discussion
The following related ideas need consideration for the future:
- It is possible to generate definitions in terms of the operators being used, instead of the "key" operator? Would it make sense? Such specification introduces even more variance into the generated code.
- Is it possible to generate these
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?). - Is it useful to support non-member comparison functions? This is not technically hard, but introduces additional (somewhat unusual) syntax.
- The current specification states that
operator<()
performs member comparisons in a manner compatible tostd::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.
VII. Acknowledgments
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.