1. Changelog
-
R1:
-
Add discussion of
, and of triviality, in § 3.5 Other return types are still forbidden.void operator = ( X && ) = default
-
2. Motivation
Current C++ permits
to appear only on certain signatures, with certain return types.
The current wording prohibits the use of placeholder types such as
to express these return
types, with the single exception of C++20’s
. This leads to redundant repetition,
such as in this real code from libc++'s test suite:
Today | After P2952 |
---|---|
|
|
The comparison operators are inconsistent among themselves:
can deduce
, but the others cannot deduce
.
Today | After P2952 |
---|---|
|
|
The status quo is inconsistent between non-defaulted and defaulted functions,
making it unnecessarily tedious to upgrade to
:
Today | After P2952 |
---|---|
|
|
|
|
The ill-formedness of these declarations comes from overly restrictive wording in the standard,
such as [class.eq]/1 specifically requiring that a defaulted equality
operator must have a declared return type of
, instead of simply specifying that its return type
must be
.
We believe each of the examples above has an intuitively clear meaning: the placeholder return type
correctly matches the type which the defaulted body will actually return. We propose to loosen the
current restrictions and permit these declarations to be well-formed.
This proposal does not seek to change the set of valid return types for these functions.
We propose a purely syntactic change to expand the range of allowed declaration syntax, not semantics.
(But we do one drive-by clarification which we believe matches EWG’s original intent:
if an empty class’s defaulted
returns a non-comparison-category type,
it should be defaulted as deleted.)
3. Proposal
We propose that a defaulted function declaration with a placeholder return type should have its type deduced ([dcl.spec.auto.general]) as if from a fictional return statement that returns:
-
a prvalue of type
, in the case ofbool
,operator ==
,operator !=
,operator <
,operator >
, oroperator <= operator >= -
a prvalue of type
, in the case ofQ
; whereoperator <=>
is the common comparison type of R0, R1,... Rn-1Q -
an lvalue of type
, in the case ofC
for a class or union typeoperator = C
Then, the deduced return type is compared to the return type(s) permitted by the standard. If the types match, the declaration is well-formed. Otherwise it’s ill-formed.
struct MyClass { auto & operator = ( const MyClass & ) = default ; // Proposed OK: deduces MyClass& decltype ( auto ) operator = ( const MyClass & ) = default ; // Proposed OK: deduces MyClass& auto && operator = ( const MyClass & ) = default ; // Proposed OK: deduces MyClass& const auto & operator = ( const MyClass & ) = default ; // Still ill-formed: deduced const MyClass& is not MyClass& auto operator = ( const MyClass & ) = default ; // Still ill-formed: deduced MyClass is not MyClass& auto * operator = ( const MyClass & ) = default ; // Still ill-formed: deduction fails void operator = ( const MyClass & ) = default ; // Still ill-formed: void is not MyClass& };
For
, our proposal gives the following behavior:
struct MyClass { auto operator == ( const MyClass & ) const = default ; // Proposed OK: deduces bool decltype ( auto ) operator == ( const MyClass & ) const = default ; // Proposed OK: deduces bool auto && operator == ( const MyClass & ) const = default ; // Still ill-formed: deduced bool&& is not bool auto & operator == ( const MyClass & ) const = default ; // Still ill-formed: deduction fails };
3.1. "Return type" versus "declared return type"
Today, vendors unanimously reject
.
But we can’t find any wording in [class.copy.assign] or [dcl.fct.def.default] that directly justifies
this behavior. It seems that vendors are interpreting e.g. [dcl.fct.def.default]/2.5’s
"[if] the return type of F1 differs from the return type of F2"
to mean "the declared return type of F1," even though newer sections such as [class.compare] consistently distinguish
the "declared return type" from the (actual) return type.
We tentatively propose to leave [dcl.fct.def.default] alone, and simply add an example that indicates the (new) intent of the (existing) wording: that it should now be interpreted as talking about the assignment operator’s actual return type, not its declared (placeholder) return type.
3.2. "Defaulted as deleted"
The current wording for comparison operators is crafted so that the following
is well-formed.
Its
is defaulted as deleted (so that operator is unusable), but the instantiation
of class
itself is OK. We need to preserve this in our rewriting.
(Godbolt.)
template < class T > struct Container { T t ; auto operator <=> ( const Container & ) const = default ; }; Container < std :: mutex > cm ; // OK, <=> is deleted struct Weird { int operator <=> ( Weird ) const ; }; Container < Weird > cw ; // OK, <=> is deleted because Weird’s operator<=> // returns a non-comparison-category type
Similarly for dependent return types:
template < class R > struct C { int i ; R operator <=> ( const C & ) const = default ; }; static_assert ( std :: three_way_comparable < C < std :: strong_ordering >> ); static_assert ( ! std :: three_way_comparable < C < int >> ); // OK, C<int>'s operator<=> is deleted
Therefore we can’t just say "
shall have a return type which is a comparison category type";
we must say that if the return type is not a comparison category type then the operator is defaulted as deleted.
3.3. "Deducing this
" and CWG2586
The resolution of [CWG2586] (adopted for C++23) permits defaulted functions to have explicit object parameters.
This constrains the wordings we can choose for
: we can’t say “the return type is deduced
as if from
” because there might not be a
.
There’s a quirk with rvalue-ref-qualified assignment operators — not move assignment, but assignment where the destination object is explicitly rvalue-ref-qualified.
-
deduces an lvalue reference; butauto && operator = ( const B & ) && { return * this ; } -
deduces an rvalue reference.auto && operator = ( this B && self , const B & ) { return self ; }
Nonetheless, a defaulted assignment operator always returns an lvalue reference ([class.copy.assign]/6, [dcl.fct.def.default]/2.5), regardless of whether it’s declared using explicit object syntax.
struct A { A & operator = ( const A & ) && = default ; // OK today A && operator = ( const A & ) && = default ; // Ill-formed, return type isn’t A& decltype ( auto ) operator = ( const A & ) && { return * this ; } // OK, deduces A& decltype ( auto ) operator = ( const A & ) && = default ; // Proposed OK, deduces A& }; struct B { B & operator = ( this B && self , const B & ) { return self ; } // Error, self can’t bind to B& B && operator = ( this B && self , const B & ) { return self ; } // OK decltype ( auto ) operator = ( this B && self , const B & ) { return self ; } // OK, deduces B&& B & operator = ( this B & self , const B & ) = default ; // OK B & operator = ( this B && self , const B & ) = default ; // OK B && operator = ( this B && self , const B & ) = default ; // Ill-formed, return type isn’t B& decltype ( auto ) operator = ( this B && self , const B & ) = default ; // Proposed OK, deduces B& };
Defaulted rvalue-ref-qualified assignment operators are weird; Arthur is bringing another paper to forbid them entirely ([P2953]). However, P2952 doesn’t need to treat them specially. Defaulted assignment operators invariably return lvalue references, so we invariably deduce as-if-from an lvalue reference, full stop.
3.4. Burden on specifying new defaultable operators
We propose to leave [dcl.fct.def.default] alone and reinterpret its term "return type" to mean the actual return type, not the declared return type. This will, by default, permit the programmer to use placeholder return types on their defaulted operators. So there is a new burden on the specification of the defaultable operator, to specify exactly how return type deduction works for the implicitly defined operator.
operator ++ ( int )
defaultable in the same way as a
secondary comparison operator. It would presumably have done this by
adding wording to [over.inc]. After P2952, this
added wording would need to include a sentence like:
A defaulted postfixfor class
operator ++ shall have a return type that is
X or
X . If its declared return type contains a placeholder type, its return type is deduced as if from
void
where
return X ( r ); is an lvalue reference to the function’s object parameter, if
r is a well-formed expression;
X ( r )
otherwise.
return ;
[P0847] §5.2’s
example of
("CRTP without the C, R, or even T") involves an
with return
type
; but that
is not defaulted, and probably couldn’t be defaulted even after [P1046R2], firstly
because it is a template and secondly because its deduced return type is
instead of
.
struct add_postfix_increment { template < class Self > auto operator ++ ( this Self & , int ) = default ; // Today: ill-formed, can’t default operator++ // After P1046R2: presumably still not OK, can’t default a template }; struct S : add_postfix_increment { int i ; auto & operator ++ () { ++ i ; return * this ; } using add_postfix_increment :: operator ++ ; }; S s = { 1 }; S t = s ++ ;
3.5. Other return types are still forbidden
Notice that returning any type other than
from a defaulted
remains ill-formed.
For example, we don’t propose to accept:
struct X { void operator = ( X && ) = default ; // still ill-formed };
Any paper that did propose to permit defaulting
would have to consider very carefully
whether to call that operator "trivial." Today,
is true only when
is
. It’s just barely conceivable that existing
user code might rely on that fact, and break when presented with a trivial assignment
operator that returned
instead. And certainly the return-by-copy
shouldn’t be considered "trivial" for non-trivially-copy-constructible
.
But none of this is a problem for this proposal P2952, because we propose to continue rejecting
a defaulted
that returns any type but
. We merely propose to allow the programmer
to spell that return type using a placeholder such as
.
3.6. Existing corner cases
There is vendor divergence in some corner cases. Here is a table of the divergences we found, plus our opinion as to the conforming behavior, and our proposed behavior.
URL | Code | Clang | GCC | MSVC | EDG | Correct |
---|---|---|---|---|---|---|
link |
| ✗ | ✗ | ✗ | ✓ | ✗ |
link |
| ✓ | ✗ | ✓ | ✓ | ✓ |
link |
| ✗ | ✓ | ✗ | ✗ | Today: ✗ Proposed: ✓ |
link |
| ✗ | ✗ | ✓ | ✓ | Today: ✗ Proposed: ✗ |
link |
| ✗ | ✓ | ✓ | ✓ | Today: ✗ Proposed: ✓ |
link |
| ✗ | unmet | ✓ | ✓ | Today: ✗ Proposed: unmet |
link |
| ✓ | ✓ | ✗ | deleted | Today: ✓ Proposed: deleted |
link |
| ✓ | ✓ | ✗ | deleted | deleted |
link |
| ✓ | deleted | ✓ | deleted | deleted |
link |
| ✓ | ✓ | ✓ | deleted | Today: ✓ Proposed: deleted |
link |
| ✓ | ✗ | ✓ | deleted | deleted |
link |
| ✓ | ✓ | ✓ | ✓ | deleted |
link |
| ✓ | ✓ | noexcept | incon- sistent | ✓ |
link |
| ✓ | ✓ | ✓ | ✓ | ✓ |
link |
| deleted | ✗ | ✗ | deleted | deleted |
link |
| deleted | ✗ | ✗ | deleted | deleted |
link |
| ✓ | ✓ | ✓ | ✓ | ✓ ([P2953]: deleted) |
link |
| ✗ | ✗ | ✗ | ✗ | ✗ |
3.7. Impact on existing code
There should be little effect on existing code, since this proposal mainly allows syntax that was ill-formed before. As shown in § 3.6 Existing corner cases, we do propose to change some very arcane examples, e.g.
struct C { const std :: strong_ordering & operator <=> ( const C & ) const = default ; // Today: Well-formed, non-deleted // Tomorrow: Well-formed, deleted };
4. Implementation experience
None yet.
5. Proposed wording
5.1. [class.eq]
Note: The phrase "equality operator function" ([over.binary])
covers both
or
. But
is not covered by [class.eq]; it’s covered by [class.compare.secondary] below.
Modify [class.eq] as follows:
1․ A defaulted equality operator function ([over.binary]) shall have a declared return type.
bool 2․ A defaulted
operator function for a class
== is defined as deleted unless, for each
C i in the expanded list of subobjects for an object
x of type
x ,
C i
x i is usable ([class.compare.default]).
== x 3․ The return value
x․ A defaultedof a defaulted
V operator function with parameters
== and
x is determined by comparing corresponding elements
y i and
x i in the expanded lists of subobjects for
y and
x (in increasing index order) until the first index i where
y i
x
== i
y yields a result value which,whencontextually converted to, yields
bool false
. If no such index exists,is
V true
. Otherwise,is
V false
.operator function shall have the return type
== . If its declared return type contains a placeholder type, its return type is deduced as if from
bool .
return true; 4․ [Example 1:
struct D { int i ; friend bool operator == ( const D & x , const D & y ) = default ; // OK, returns x.i == y.i }; — end example]
5.2. [class.spaceship]
Note: There are only three "comparison category types" in C++, and
is implicitly convertible
to all three of them. The status quo already effectively forbids
to return a non-comparison-category type,
since either
is deduced as a common comparison type (which is a comparison category type by definition), or else
a synthesized three-way comparison of type
must exist (which means
must be a comparison category type),
or else the sequence
i must be empty (in which case there are no restrictions on
except
that it be constructible from
). We strengthen the wording to directly mandate that the
return type be a comparison category type, even in the empty case.
Modify [class.spaceship] as follows:
[...]2․ Let
be the declared return type of a defaulted three-way comparison operator function, and let
R i be the elements of the expanded list of subobjects for an object
x of type
x .
C — (2.1) If
R iscontains a placeholder type, then let cvi,
auto i be the type of the expression
R i
x i. The operator function is defined as deleted if that expression is not usable or if
<=> x i is not a comparison category type ([cmp.categories.pre]) for any i. The return type is deduced as if from
R , where
return Q ( std :: strong_ordering :: equal ); is the common comparison type (see below) of
Q 0,
R 1, ...,
R n-1.
R — (2.2) Otherwise,
if the synthesized three-way comparison of typeshall not contain a placeholder type. If
R between any objects
R i and
x i is not defined, the operator function is defined as deleted.
x 3․ The return value
x․ A defaulted three-way comparison operator function which is not deleted shall have a return type which is a comparison category type ([cmp.categories.pre]).
V of typeof the defaulted three-way comparison operator function with parameters
R and
x
y of the same typeis determined by comparing corresponding elementsi and
x i in the expanded lists of subobjects for
y and
x (in increasing index order) until the first index i where the synthesized three-way comparison of type
y between
R i and
x i yields a result value
y i where
v i
v , contextually converted to
!= 0 , yields
bool true
;is
V a copy ofi. If no such index exists,
v is
V
static_cast < R > (
std :: strong_ordering :: equal .
) 4․ The common comparison type
of a possibly-empty list of n comparison category types
U 0,
T 1, ...,
T n-1 is defined as follows:
T [...]
5.3. [class.compare.secondary]
Modify [class.compare.secondary] as follows:
1․ A secondary comparison operator is a relational operator ([expr.rel]) or theoperator.
!= A defaulted operator function ([over.binary]) for a secondary comparison operator@
shall have a declared return type.
bool 2․
TheA defaulted secondary comparison operator function with parametersand
x is defined as deleted if
y — (2.1) overload resolution ([over.match]), as applied to
, does not result in a usable candidate, or
x @y — (2.2) the candidate selected by overload resolution is not a rewritten candidate.
Otherwise, the operator function yields
x․ A defaulted secondary comparison operator function shall have the return type. The defaulted operator function is not considered as a candidate in the overload resolution for the
x @y @
operator.. If its declared return type contains a placeholder type, its return type is deduced as if from
bool .
return true; 3․ [Example 1:
struct HasNoLessThan { }; struct C { friend HasNoLessThan operator <=> ( const C & , const C & ); bool operator < ( const C & ) const = default ; // OK, function is deleted }; — end example]
5.4. [class.copy.assign]
Note: [class.copy.assign]/6 already clearly states that "The implicitly-declared copy/move assignment operator for class
has the return type
."
But we need this new wording to ensure that an explicitly-defaulted copy/move assignment operator
will deduce that same type. (If it didn’t deduce that type, then the explicitly-defaulted operator
would be deleted, as in example
below.)
Note: Arthur initially proposed that [class.copy.assign]/14 should say "...returns an lvalue reference to the object for which...", but Jens Maurer thought that wouldn’t be an improvement from CWG’s point of view. Maybe it should say "...returns the assignment operator’s object parameter ([dcl.fct])" instead.
Modify [class.copy.assign] as follows:
14․ The implicitly-defined copy/move assignment operator for a class returns the object for which the assignment operator is invoked, that is, the object assigned to.15․ If a defaulted copy/move assignment operator’s declared return type contains a placeholder type, its return type is deduced as if from
, where
return r ; is an lvalue reference to the object for which the assignment operator is invoked.
r 16․ [Example:
—end example]struct A { decltype ( auto ) operator = ( A && ) = default ; // Return type is A& }; struct B { auto operator = ( B && ) = default ; // error: Return type is B, which violates [dcl.fct.def.default]/2.5 };