This paper assumes that the reader is familar with N4475 "Default comparisons (R2)" by Bjarne Stroustrup. In particular, default comparisons are assumed to be implicit (i.e. require no extra syntax to be available).
P0221R0 as amended by a clarification for template specializations was approved by EWG during the Jacksonville (2016-03) meeting of WG21. Blue text in the proposed wording indicates changes compared to P0221R1.
Given aim 3, we're 30+ years late in mandating the ideal world, which is too late. In particular, that means we cannot enforce the important principle that x==y appearing in different corners of the source code has the same semantics everywhere. We can, however, make sure that the default (if used) has the same semantics everywhere.
The following rules reflect the aims above. These rules are given in informal language; for a precise wording of the rules, refer to the "wording" section below.
[Rule R1 removed per EWG guidance in Oulu.]Second, we introduce declarations (but not definitions) of the default comparisons for each class that is defined (aim 1). Note that a class template specialization is a class that is defined when the class template is instantiated or explicitly specialized. A friend declaration is only visible to argument-dependent lookup, thus we can only find it for function calls, but not for taking the address.
R2: For each class that is defined, equality (5.10 expr.eq) and relational (5.9 expr.rel) operator functions according to the patternThird, we allow a user declaration of one of those operator functions to hide the implicitly-declared one (aim 3):bool operator op(const C&, const C&);are implicitly declared (unless there was a preceding user declaration with a similar signature), but not yet defined. (A member declaration of op is transformed into the equivalent non-member form by introducing the implicit object parameter (13.3.1 over.match.funcs) for the check.) The declaration is as-if by a friend declaration immediately before the closing brace of the class definition.
R3: A user-declared comparison function hides the corresponding implicitly-declared one for class C with a similar signature (if any). Hiding means that if both appear in a lookup set, only the user-declared comparison function is considered in overload resolution.Fourth, the definition of a default comparison (aim 2, aim 6):
R4: An implicitly-declared comparison function for a class C is defined if it is odr-used (3.2 basic.def.odr). It performs subobject decomposition and then subobject comparisons (see "wording" below). The subobject comparisons are performed in the context of class C as-if immediately before the closing brace of the class definition. If subobject comparison would be ill-formed, the comparison is defined as deleted.Fifth, using both a default comparison and the corresponding user-declared comparison is a syntax error (aim 2):
R5: If an expression or the definition of a default comparison uses a default comparison, but would use a user-declared comparison if the former appeared later in the translation unit, the program is ill-formed. No diagnostic is required if the use and the competing declaration are in different translation units.
[Rule R6 removed per EWG guidance in Oulu.]
Seventh, define "similar signature". This could be extended to (some kinds of) function templates if desired.Two operator functions have a similar signature if theyNote that the wording below takes a slightly different approach by reusing the intermediate results of overload resolution to determine whether a default comparison should be generated.
- have the same name and,
- given a class type C and the types T1 and T2 of corresponding parameters, each of T1 and T2 is either C or reference to cv C
struct B { int x; }; B b; bool result = B() == b; // ok, default== for B
struct S { bool operator==(const S&); // #1; note: non-const (user oversight) }; S s; bool b1 = s == s; // calls #1 bool b2 = S() == S(); // error, can't call #1 and no default== struct S2 { }; bool operator==(const S2&, const S2&); // #2 bool b3 = S2() == S2(); // calls #2 struct S3 { }; S operator==(const S3&, S3&); // #3 (strange) S3 s3; S b4 = s3 == s3; // calls #3; no default== S b5 = S3() == S3(); // error, can't call #3 and no default==
struct S { }; namespace N { bool operator==(const S&, const S&); // #1 bool operator==(const S&, S&); // #2 bool b = S() == S(); // calls #1 (note: some prefer default== or error) S s; bool b2 = S() == s; // calls #2 (note: some prefer default== or error) }
struct S { };
bool b1 = S() == S(); // ok, use default==
namespace N {
bool operator==(S,S); // #1
bool b2 = S() == S(); // ok, use N::operator==
}
struct S { }; // #1 struct S2 { S s; }; namespace N { bool operator==(S,S); // #2 bool b = S2() == S2(); // #3 ok, does not use #2 }For #3, we use the default== on S2. Its definition uses the default== on S introduced at #1.
struct S { operator int() const; }; bool b = S() == S(); // ok, using built-in "int == int"
struct C { }; template<class T> bool operator==(const T&, const T&); bool b = C() == C(); // ok, use operator== template
template<class T> class S { }; template<class T> bool operator==(const S<T>&, const S<T>&); S<int> s; bool b = s == s; // use user-declared operator==template<class T> class S2 { friend bool operator==(const S2<T>&, const S2<T>&) { ... } }; S<int> s; bool b2 = s == s; // uses user-declared operator==
Case 1: User-written == appears as a member of C, or in the same namespace as C and mentions C specifically. All of these cases call the user-written ==. Varying the parameter types between C, const C&, and C& does not change the outcome, except that the rules preventing binding prvalues to lvalue references remain in force.// case 1a: obvious case struct C1 { bool operator==(const C1&) const; }; bool b = C1() == C1(); // uses user-declared == for C1 // case 1b: obvious case, non-member struct C2 { }; bool operator==(const C2&, const C2&); bool b = C2() == C2(); // uses user-declared == for C2 // case 1c: class template member template<class T> struct C3 { bool operator==(const C3&) const; }; bool b = C3() == C3(); // uses user-declared == for C3 // case 1d: function template for a class template template<class T> struct C4 { }; template<class T> bool operator==(const C4<T>&, const C4<T>&); bool b = C4() == C4(); // uses user-declared == for C4 // case 2a: fully general template struct C5 { }; template<class T> bool operator==(const T&, const T&); C5 c5; bool b = C5() == C5(); // calls operator function template // case 2b: same with concept struct C6 { }; template<My_concept T> bool operator==(const T&, const T&); bool b = C6() == C6(); // calls operator function template // case 2c: conversion struct C7 { }; struct X { X(const C7&){} }; // convertible from C7 bool operator==(const X&, const X&); bool b = C7() == C7(); // uses user-declared == for X
struct S { int i = 0; }; bool operator==(const S&, const S&) { return true; } int f() { S() != S(); // well-formed, yields false }
struct S { int a[3]; }
will acquire generated
comparison functions, but top-level (standalone) int[3]
(i.e. an array not wrapped in a struct) will not.struct A { }; void operator==(const A&, const A&); // yes void operator==(const A&, int); // no void operator==(A&, const A&); // yes void operator==(A&&, volatile A&); // no void operator==(const volatile A&&, const A&); // no void operator==(A, const A&); // yes template<class T> void operator==(const T&, A); // yes template<class T> void operator==(const T&, const T&); // yes template<class T> void operator==(T&&, T&&); // no
struct A { int x, y; } a; namespace N { void *operator<(A, A); auto q = a > a; }Consistent with aim 2, we cannot use N::operator<. This is ill-formed to avoid silent surprises.
struct S { int x; }; bool operator==(const S&, const S&); S s; bool b = s < s; // error
A type cv T supports comparison by subobject if T is an array type, or is a non-union class type that satisfies the following constraints:Change in 5.9 [expr.rel] paragraph 2:T supports operator op in a given context if overload resolution (13.3.1.2 [over.match.oper]) finds a viable function for the expression
- T is not a closure type (5.1.5 [expr.prim.lambda]),
- T has no virtual base class (clause 10 [class.derived]),
- T has no virtual member function (10.3 [class.virtual]),
- T has no direct mutable member (7.1.1 [dcl.stc]),
- T has no direct non-static data member of pointer type (9.2 [class.mem]),
- T has no user-provided or deleted copy/move constructor
or copy/move assignment operator(12.8 [class.copy]), and- T has no user-provided copy/move assignment operator.
x op y
, where x and y are lvalues of type T.T supports equality comparison by subobject in a given context if T supports comparison by subobject and does not support operator== and operator!= in that context.
T supports relational comparison by subobject in a given context if T supports equality comparison by subobject in that context and does not support operator <, operator <=, operator >, and operator >= in that context.
The operands shall have arithmetic, enumeration, or pointer type. [ Note: For operands of class type, see 9.10 [class.oper]. ] ...Change in 5.10 [expr.eq] paragraph 2:
The == (equal to) and the != (not equal to) operators group
left-to-right. The operands shall have arithmetic, enumeration,
pointer, or pointer to member type, or
type std::nullptr_t
. [ Note: For operands of class
type, see 9.10 [class.oper]. ] ...
Change in 9 [class] paragraph 7 bullet 6:
Change in 9.2 [class.mem] paragraph 1:
- ...
- has all non-static data members and bit-fields in the class and its base classes
first declared inas direct members of the same class, and- ...
The member-specification in a class definition declares the full set of members of the class; no member can be added elsewhere. A direct member of a class is a member of the class that was first declared within the class's member-specification. ...Add a new section 9.10 [class.oper]:
9.10 Operators [class.oper]Add a paragraph after 13.3.1.2 [over.match.oper] paragraph 7:[ Note: This section specifies the meaning of relational (5.9 [expr.rel]) or equality (5.10 [expr.eq]) operators applied to values of class type. ]
The result of comparing two objects x and y that have the same class or array type T (ignoring cv-qualification) is defined by decomposing x and y into a sequence of corresponding subobjects and comparing the pairs of corresponding subobjects. [ Note: For the original x-y pair, x and y are considered subobjects of themselves, respectively. ] The context for applying a comparison operator to such a pair is defined as follows: If the original x-y pair is compared, the context is where the comparison appears. Otherwise, the context is as if in a member function of class T defined at the point where the comparison appears. [ Note: The context determines operator function lookup and access control. ]
Comparing x and y for equality is defined as follows:
- A sequence of corresponding subobjects of x and y is formed by starting with a sequence comprising x corresponding to y, and repeatedly replacing each element whose type supports equality comparison by subobject (3.9.2 [basic.compound]) with a sequence of the corresponding subobjects of the direct base classes and direct members of that type, in order of declaration or order of increasing array subscript.
- If the resulting sequence of corresponding subobjects has no elements,
x == y
yields true andx != y
yields false.- Otherwise, for each element in the sequence of corresponding subobjects, the corresponding subobjects are compared using ==. If a subobject comparison, when contextually converted to
bool
, yields false, no further subobjects are compared, andx == y
yields false andx != y
yields true. If all such comparisons yield true, thenx == y
yields true andx != y
yields false.Comparing x and y with a relational operator is defined as follows:
[ Example:
- For x > y, the result is y < x. For x >= y, the result is y <= x.
- A sequence of corresponding subobjects of x and y is formed by starting with a sequence comprising x corresponding to y, and repeatedly replacing each element whose type supports relational comparison by subobject (3.9.2 [basic.compound]) with a sequence of the corresponding subobjects of the direct base classes and direct members of that type, in order of declaration or order of increasing array subscript.
- If the resulting sequence of corresponding subobjects has no elements,
x < y
yields false andx <= y
yields true.- Otherwise, for each element in the sequence of corresponding subobjects, the corresponding subobjects are compared using ==. If a subobject comparison, when contextually converted to bool, yields false, that element is the first differing one. If the final element is reached for a < comparison, that element is the first differing one, and is not compared with ==. Otherwise, if all such comparisons yield true, there is no first differing element.
- If there is no first differing element, the result of the <= comparison is true. Otherwise, the corresponding subobjects for the first differing element are compared using <. The result of this comparison, when contextually converted to bool, is the result of the overall comparison.
struct B { int i = 0; }; struct S : B { int j = 1; }; struct T : S { bool operator>(T) = delete; }; void f() { B b; S s1, s2; s2.j = 2; s1 == s1; // yields true s1 != s2; // yields true s1 < s1; // yields false s1 < s2; // yields true s1 == b; // error: not the same class type T() == T(); // yields true T() < T(); // error: operator> is user-declared } template<class T> struct V { T x; }; struct E { }; bool operator==(E,E); V<E> v; bool b = v == v; // ok, default== finds operator== for E struct Q { int x; }; bool operator==(Q&&, Q&&); Q q1, q2; bool bq = q1 == q2; // ok, compares q1.x == q2.x-- end example ]
If overload resolution yields no viable function (13.3.2 [over.match.viable]) for a relational (5.9 [expr.rel]) or equality (5.10 [expr.eq]) operator and the operands are of the same complete class type T (ignoring cv-qualification), then:If a comparison operator is treated as a built-in operator in a context whose nearest enclosing namespace is N, and a comparison with the same operator and the same operand types in another context whose nearest enclosing namespace is also N is not treated as a built-in operator, the program is ill-formed. No diagnostic is required if the two comparisons appear in different translation units. [ Example:
- If the operator is == and T does not support equality comparison by subobject, the program is ill-formed.
- If the operator is < and T does not support relational comparison by subobject, the program is ill-formed.
- Otherwise, the operator is treated as a built-in operator and interpreted according to 9.10 [class.oper].
struct S { }; bool b = S() == S(); // built-in == bool operator==(const S&, const S&); S s; bool b2 = s == s; // error: not using built-in ==-- end example ]