Defaulted comparison operators

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:

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:

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 to std::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

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]

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 provide operator==()

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.

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 provide operator<()

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 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.
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.

The implicitly-defined operator>=() for a non-union class X performs a call
to operator<() and returns a boolean negation of the result

The implicitly-defined operator>() for a non-union class X performs a call
to operator<() but reverses the arguments

The implicitly-defined operator<=() for a non-union class X performs a call
to operator>() 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 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.

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.