D2953R1
Forbid defaulting operator=(X&&) &&

Draft Proposal,

Author:
Audience:
EWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Draft Revision:
7

Abstract

Current C++ permits explicitly-defaulted special members to differ from their implicitly-defaulted counterparts in various ways, including parameter type and ref-qualification. This permits implausible signatures like A& operator=(const A&) && = default, where the left-hand operand is rvalue-ref-qualified. We propose to forbid such signatures.

1. Changelog

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 const A& operator=(const A&) const& = default is forbidden because it is additionally const-qualified, and also because its return type differs from the implicitly-defaulted A&. 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 A& operator=(const A&) && = default is permitted! This has three minor drawbacks:

To eliminate all three drawbacks, we propose that a defaulted copy/move assignment operator should not be allowed to add to its implicit signature an rvalue ref-qualifier.

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
};

2.1. Interaction with P2952

[CWG2586] (adopted for C++23) permits operator= to have an explicit object parameter.

[P2952] proposes that operator= should (also) be allowed to have a placeholder return type. If P2952 is adopted without P2953, then we’ll have:

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 (conservative): Deleted, object parameter is not C&
    // Proposed (ambitious): Ill-formed, object parameter is not C&
};

The first, non-defaulted, operator "does the natural thing" by returning its left-hand operand, and deduces C&&. The second operator also "does the natural thing" by being defaulted; but after P2952 it will deduce C&. (For rationale, see [P2952] §3.3 "Deducing this and CWG2586.") The two "natural" implementations deduce different types! This looks inconsistent.

If we adopt P2953 alongside P2952, then the second operator= will go back to being unusable, which reduces the perception of inconsistency.

Today P2952
Today C&&/ill-formed C&&/C&
P2953 C&&/ill-formed C&&/deleted

2.2. "Deleted" versus "ill-formed"

(See also [P2952] §3.2 "Defaulted as deleted".)

[dcl.fct.def.default]/2.6 goes out of its way to make many explicitly defaulted constructors, assignment operators, and comparison operators "defaulted as deleted," rather than ill-formed. This was done by [P0641] (resolving [CWG1331]), in order to support class templates with "canonically spelled" defaulted declarations:

struct A {
  // Permitted by (2.4)
  A(A&) = default;
  A& operator=(A&) = default;
};

template<class T>
struct C {
  T t_;
  explicit C();
  // Permitted, but defaulted-as-deleted, by (2.6), since P0641
  C(const C&) = default;
  C& operator=(const C&) = default;
};

C<A> ca; // OK

There is similar wording in [class.spaceship] and [class.eq]. We don’t want to interfere with these use-cases; that is, we want to continue permitting programmers to write things like the above C<A>.

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 Proposed
(conservative)
Proposed
(ambitious)
link
C& operator=(C&) = default;
link
C& operator=(const C&&) = default;
deleted deleted deleted deleted
link
C& operator=(const C&) const = default;
deleted deleted deleted deleted
link
C& operator=(const C&) && = default;
deleted
link
C&& operator=(const C&) && = default;
link
template<class>
struct C {
  static const C& f();
  C& operator=(decltype(f()) = default;
};
link
struct M {
  M& operator=(const M&) volatile;
};
struct C {
  volatile M m;
  C& operator=(const C&) = default;
};
deleted deleted deleted inconsistent deleted deleted deleted

3. Implementation experience

Arthur has implemented § 4 Proposed wording (conservative) in his fork of Clang, and used it to compile both LLVM/Clang/libc++ and another large C++17 codebase. Naturally, it caused no problems except in the relevant parts of Clang’s own test suite.

There is no implementation experience for § 5 Proposed wording (ambitious).

4. Proposed wording (conservative)

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 form = default ; is called an explicitly-defaulted definition. A function that is explicitly defaulted shall

  • (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 F1 is allowed to differ from the corresponding special member function F2 that would have been implicitly declared, as follows:

  • (2.1) if F2 is an assignment operator, F1 and F2 may have differing ref-qualifiers F1 may have an lvalue ref-qualifier;

  • (2.2) if F2 has an implicit object parameter of type “reference to C” is an assignment operator with an implicit object parameter of type C&, F1 may be an explicit object member function whose have an explicit object parameter is of type (possibly different) “reference to C” C&, in which case the type of F1 would differ from the type of F2 in that the type of F1 has an additional parameter;

  • (2.3) F1 and F2 may have differing exception specifications; and

  • (2.4) if F2 has a non-object parameter of type const C&, the corresponding non-object parameter of F1 may be of type C&.

If the type of F1 differs from the type of F2 in a way other than as allowed by the preceding rules, then:

  • (2.5) if F1 F2 is an assignment operator, and the return type of F1 differs from the return type of F2 or F1’s non-object parameter type is not a reference, the program is ill-formed;

  • (2.6) otherwise, if F1 is explicitly defaulted on its first declaration, it is defined as deleted;

  • (2.7) otherwise, the program is ill-formed.

[...]

5. Proposed wording (ambitious)

Note: The intent of this "ambitious" wording is to lock down the signatures of defaultable member functions as much as possible, and make errors as eager as possible, except in the cases covered by § 2.2 "Deleted" versus "ill-formed" (which we want to keep working, i.e., "defaulted as deleted").

5.1. [dcl.fct.def.default]

Modify [dcl.fct.def.default] as follows:

1․ A function definition whose function-body is of the form = default ; is called an explicitly-defaulted definition. A function that is explicitly defaulted shall

  • (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 F1 is allowed to differ from the corresponding special member function F2 that would have been implicitly declared, as follows:

  • (2.1) if F2 is an assignment operator, F1 and F2 may have differing ref-qualifiers F1 may have an lvalue ref-qualifier;

  • (2.2) if F2 has an implicit object parameter of type “reference to C” is an assignment operator with an implicit object parameter of type C&, F1 may be an explicit object member function whose have an explicit object parameter is of type (possibly different) “reference to C” C&, in which case the type of F1 would differ from the type of F2 in that the type of F1 has an additional parameter; and

  • (2.3) F1 and F2 may have differing exception specifications; and

  • (2.4) if F2 has a non-object parameter of type const C&, the corresponding non-object parameter of F1 may be of type C&; and

  • (2.5) if F2 has a non-object parameter of type C&, the corresponding non-object parameter of F1 may be of type const C&.

If the type of F1 differs from the type of F2 in a way other than as allowed by the preceding rules, then:

  • (2.5) if F1 is an assignment operator, and the return type of F1 differs from the return type of F2 or F1’s non-object parameter type is not a reference, the program is ill-formed;
  • (2.6) otherwise, if F1 is explicitly defaulted on its first declaration, it is defined as deleted;
  • (2.7) otherwise, the program is ill-formed.

[...]

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

6. Proposed straw polls

The next revision of this paper (if any) will be guided by the outcomes of these three 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).
EWG would like to pursue § 5 Proposed wording (ambitious) (regardless of the next poll).
Advance § 4 Proposed wording (conservative) to CWG (regardless of the previous poll).

References

Informative References

[CWG1331]
Daniel Krügler. const mismatch with defaulted copy constructor. June 2011. URL: https://cplusplus.github.io/CWG/issues/1331.html
[CWG2586]
Barry Revzin. Explicit object parameter for assignment and comparison. May–July 2022. URL: https://cplusplus.github.io/CWG/issues/2586.html
[P0641]
Daniel Krügler; Botond Ballo. Resolving CWG1331: const mismatch with defaulted copy constructor. November 2017. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0641r2.html
[P2952]
Arthur O'Dwyer; Matthew Taylor. auto& operator=(X&&) = default. August 2023. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2952r0.html