1. Changelog
-
R0:
-
Initial revision.
-
2. Motivation and proposal
Currently, [dcl.fct.def.default]/2.5 permits an explicitly defaulted special member function to differ from the implicit one by adding ref-qualifiers, but not cv-qualifiers.
For example, the signature
is forbidden
because it is additionally const-qualified, and also because its return type differs
from the implicitly-defaulted
. This might be considered unfortunate, because that’s
a reasonable signature for a const-assignable proxy-reference type.
But programmers aren’t clamoring for that signature to be supported, so we do not propose to support it here.
Our concern is that the unrealistic signature
is permitted! This has three minor drawbacks:
-
The possibility of these unrealistic signatures makes C++ harder to understand. Before writing [P2952], Arthur didn’t know such signatures were possible.
-
The wording to permit these signatures is at least a tiny bit more complicated than if they weren’t permitted.
-
The quirky interaction with [CWG2586] and [P2952] discussed in the next subsection.
To eliminate all three drawbacks, we propose that a defaulted copy/move assignment operator should not be permitted to add to its implicit signature an rvalue ref-qualifier (nor an explicit object parameter of rvalue reference type).
2.1. Interaction with P2952
[CWG2586] (adopted for C++23) permits
to have an explicit object parameter.
[P2952] proposes that
should (also) be allowed to have a placeholder return type.
If P2952 is adopted without P2953, then we will have the following pub-quiz fodder:
struct C { auto && operator = ( this C && self , const C & ) { return self ; } // Today: OK, deduces C&& // After P2952: Still OK, still deduces C&& // Proposed: Still OK, still deduces C&& auto && operator = ( this C && self , const C & ) = default ; // Today: Ill-formed, return type involves placeholder // After P2952: OK, deduces C& // Proposed: Deleted, object parameter is not C& };
The first, non-defaulted, operator "does the natural thing" by returning its left-hand operand,
and deduces
. The second operator also "does the natural thing" by being defaulted; but
after P2952 it will deduce
. (For rationale, see [P2952] §3.3 "Deducing
and CWG2586.")
The two "natural" implementations deduce different types! This might be perceived as inconsistency.
If we adopt P2953 alongside P2952, then the second
will go back to being unusable,
which reduces the perception of inconsistency.
Today | P2952 | |
---|---|---|
Today | /ill-formed
| /
|
P2953 | /ill-formed
| /deleted
|
2.2. "Deleted" versus "ill-formed"
[dcl.fct.def.default] goes out of its way
to make many explicitly defaulted assignment operators "defaulted as deleted," rather than ill-formed.
I think I understand the reason for this in the case of comparison operators (see [P2952] §3.2 "Defaulted as deleted"), but it’s non-obvious why we should care about
the corresponding cases for constructors, destructors, and assignment operators.
(Clang handles this example correctly; GCC, MSVC, and EDG
already non-conformingly treat both
and
as ill-formed.)
template < template < class > class TT > struct C { C & operator = ( TT < const C > ) = default ; }; C < std :: add_lvalue_reference_t > cl ; // OK, operator= is defaulted // (and GCC/MSVC/EDG have a bug) C < std :: add_rvalue_reference_t > cr ; // OK, operator= is defaulted as deleted // (but why not just make it ill-formed?)
P2953 isn’t yet proposing to change the complicated status quo, but Arthur would certainly like to learn the status quo’s rationale. If we were willing to aggressively change the status quo, we could simplify [dcl.fct.def.default] something like this:
1․ A function definition whose function-body is of the formis called an explicitly-defaulted definition. A function that is explicitly defaulted shall
= default ;
(1.1) be a special member function or a comparison operator function ([over.binary]), and
(1.2) not have default arguments.
2․ An explicitly defaulted special member function
1 is allowed to differ from the corresponding special member function
F 2 that would have been implicitly declared, as follows:
F
(2.1) if
2 is an assignment operator,
F 1 and
F 2 may have differing ref-qualifiers
F 1 may have an lvalue ref-qualifier;
F (2.2) if
2
F has an implicit object parameter of type “reference to C”is an assignment operator with an implicit object parameter of type,
C & 1 may
F be an explicit object member function whosehave an explicit object parameterisof type(possibly different) “reference to C”, in which case the type of
C & 1 would differ from the type of
F 2 in that the type of
F 1 has an additional parameter; and
F (2.3)
1 and
F 2 may have differing exception specifications.
F ; and(2.4) if2 has a non-object parameter of type
F , the corresponding non-object parameter of
const C & 1 may be of type
F .
C & If the type of
1 differs from the type of
F 2 in a way other than as allowed by the preceding rules, then:
F
(2.5) if1 is an assignment operator, and the return type of
F 1 differs from the return type of
F 2 or
F 1’s non-object parameter type is not a reference, the program is ill-formed;
F (2.6)
otherwise,if1 is a three-way comparison operator explicitly defaulted on its first declaration, it is defined as deleted;
F (2.7) otherwise, the program is ill-formed.
[...]
Again, P2953 doesn’t yet propose the above change; but it would be good to know why we shouldn’t.
2.3. 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 |
| ✓ | ✓ | ✓ | ✓ | ✓ (§2.2: ✗) |
link |
| deleted | ✗ | ✗ | deleted | deleted (§2.2: ✗) |
link |
| deleted | ✗ | ✗ | deleted | deleted (§2.2: ✗) |
link |
| ✓ | ✓ | ✓ | ✓ | Today: ✓ Proposed: deleted (§2.2: ✗) |
link |
| ✗ | ✗ | ✗ | ✗ | ✗ |
link |
| ✓ | ✗ | ✗ | ✗ | ✓ |
2.4. Impact on existing code
This proposal takes code that was formerly well-formed C++23, and makes it ill-formed. The affected constructs are extremely implausible in Arthur’s opinion; but of course we need some implementation and usage experience in a real compiler before adopting this proposal.
struct C { C & operator = ( const C & ) && = default ; // Today: Well-formed // Tomorrow: Deleted }; struct D { D & operator = ( this D && self , const C & ) = default ; // Today: Well-formed // Tomorrow: Deleted };
3. Implementation experience
None yet.
4. Proposed wording
4.1. [dcl.fct.def.default]
Note: The only defaultable special member functions are default constructors, copy/move constructors, copy/move assignment operators, and destructors. Of these, only the assignment operators can ever be cvref-qualified at all.
Modify [dcl.fct.def.default] as follows:
1․ A function definition whose function-body is of the formis called an explicitly-defaulted definition. A function that is explicitly defaulted shall
= default ;
(1.1) be a special member function or a comparison operator function ([over.binary]), and
(1.2) not have default arguments.
2․ An explicitly defaulted special member function
1 is allowed to differ from the corresponding special member function
F 2 that would have been implicitly declared, as follows:
F
(2.1) if
2 is an assignment operator,
F 1 and
F 2 may have differing ref-qualifiers
F 1 may have an lvalue ref-qualifier;
F (2.2) if
2
F has an implicit object parameter of type “reference to C”is an assignment operator with an implicit object parameter of type,
C & 1 may
F be an explicit object member function whosehave an explicit object parameterisof type(possibly different) “reference to C”, in which case the type of
C & 1 would differ from the type of
F 2 in that the type of
F 1 has an additional parameter;
F (2.3)
1 and
F 2 may have differing exception specifications; and
F (2.4) if
2 has a non-object parameter of type
F , the corresponding non-object parameter of
const C & 1 may be of type
F .
C & If the type of
1 differs from the type of
F 2 in a way other than as allowed by the preceding rules, then:
F
(2.5) if
1
F 2 is an assignment operator, and the return type of
F 1 differs from the return type of
F 2 or
F 1’s non-object parameter type is not a reference, the program is ill-formed;
F (2.6) otherwise, if
1 is explicitly defaulted on its first declaration, it is defined as deleted;
F (2.7) otherwise, the program is ill-formed.
[...]
4.2. [class.copy.assign]
Note: If we do the wording patch above, then I think nothing in [class.copy.assign] needs
to change. But much of the wording above is concerned specifically with copy/move assignment operators,
so it might be nice to move that wording out of [dcl.fct.def.default] and into [class.copy.assign].
Also note that right now a difference in
-ness is handled explicitly by [dcl.fct.def.default]
for special member functions but only by omission-and-note in [class.compare] for comparison operators.
Modify [class.copy.assign] as follows:
TODO FIXME BUG HACK
5. Proposed straw polls
The next revision of this paper (if any) will be guided by the outcomes of these two straw polls.
SF | F | N | A | SA | |
---|---|---|---|---|---|
EWG would like to forbid rvalue-ref-qualified assignment operators (by any means, not necessarily by this proposed wording). | – | — | — | — | — |
P2953R1 should pursue §2.2’s wording, making some "defaulted-as-deleted" operators into hard errors. | – | — | — | — | — |