Document number: N3950
Date: 2014-02-19
Project: Programming Language C++, Language Evolution Working Group
Reply-to: Oleg Smolsky oleg.smolsky@gmail.com

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:

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:

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:

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:

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:

IV. Implementation

I have a working prototype implementation using Clang that does the following:

The following additional work is needed to get closer to production quality:

V. Technical specifications

Correction for 8.4.2 "Explicitly-defaulted functions [dcl.fct.def.default]"

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

  1. Equality operators: operator==() and operator!=() [class.equality]
  2. Comparison operators: operator<(), operator>(), operator<=() and operator>=() [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]

  1. 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].
  2. 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).
  3. 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:
  4. 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]

  1. 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].
  2. 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).
  3. 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:
  4. The implicitly-defined operator>=() for a non-union class X performs a call to operator<() and returns a boolean negation of the result
  5. The implicitly-defined operator>() for a non-union class X performs a call to operator<() but reverses the arguments
  6. 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:
  1. 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.
  2. 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?).
  3. Is it useful to support non-member comparison functions? This is not technically hard, but introduces additional (somewhat unusual) syntax.
  4. 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.