D2952R3
Deduced return types for defaulted operator functions

Draft Proposal,

Authors:
Audience:
CWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Draft Revision:
18

Abstract

Current C++ forbids explicitly defaulted functions to have placeholder return types such as auto&, except for C++20’s operator<=>. We remove this syntactic restriction in cases where the deduced return type would be the same as the expected one. This allows more consistency and less repetition when declaring defaulted functions.

1. Changelog

2. Motivation

Current C++ permits =default to appear only on certain signatures, with certain return types. The current wording prohibits the use of placeholder types such as auto& to express these return types, with the single exception of C++20’s operator<=>. This leads to redundant repetition, such as in this real code from libc++'s test suite:

Today After P2952
struct ForwardDiffView {
  [...]
  ForwardDiffView(ForwardDiffView&&) = default;
  ForwardDiffView(const ForwardDiffView&) = default;
  ForwardDiffView& operator=(ForwardDiffView&&) = default;
  ForwardDiffView& operator=(const ForwardDiffView&) = default;
  [...]
};
struct ForwardDiffView {
  [...]
  ForwardDiffView(ForwardDiffView&&) = default;
  ForwardDiffView(const ForwardDiffView&) = default;
  auto& operator=(ForwardDiffView&&) = default;
  auto& operator=(const ForwardDiffView&) = default;
  [...]
};

The comparison operators are inconsistent among themselves: operator<=> can deduce strong_ordering, but the others cannot deduce bool.

Today After P2952
auto operator<=>(const MyClass& rhs) const = default;
bool operator==(const MyClass& rhs) const = default;
bool operator<(const MyClass& rhs) const = default;
auto operator<=>(const MyClass& rhs) const = default;
auto operator==(const MyClass& rhs) const = default;
auto operator<(const MyClass& rhs) const = default;

The status quo is inconsistent between non-defaulted and defaulted functions, making it unnecessarily tedious to upgrade to =default:

Today After P2952

auto& operator=(const MyClass& r) { i = r.i; return *this; }
MyClass& operator=(const MyClass& r) = default;

auto& operator=(const MyClass& r) { i = r.i; return *this; }
auto& operator=(const MyClass& r) = default;

auto operator==(const MyClass& r) const { return i == r.i; }
bool operator==(const MyClass& r) const = default;

auto operator==(const MyClass& r) const { return i == r.i; }
auto operator==(const MyClass& r) const = default;

auto operator++(int) { auto tmp = *this; ++*this; return tmp; }
MyClass operator++(int) = default;

auto operator++(int) { auto tmp = *this; ++*this; return tmp; }
auto operator++(int) = default;

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 bool, instead of simply specifying that its return type must be bool. 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 operator<=> 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:

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.

For the copy-assignment operator, our proposal gives the following behavior:
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 operator==, 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 auto& operator=(const A&) = default. 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 Container is well-formed. Its operator<=> is defaulted as deleted (so that operator is unusable), but the instantiation of class Container<mutex> 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 "operator<=> 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

[CWG2586] (adopted for C++23) permits defaulted functions to have explicit object parameters. This constrains the wordings we can choose for operator=: we can’t say “the return type is deduced as if from return *this” because there might not be a *this.

There’s a quirk with rvalue-ref-qualified assignment operators — not move assignment, but assignment where the destination object is explicitly rvalue-ref-qualified.

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; // Ill-formed since P2953; would have deduced 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; C++29 has adopted our [P2953] which forbids them entirely. Either way, 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.

[P3668], adopted for C++29, made postfix operator++(int) defaultable in the same way as a secondary comparison operator, by adding wording to [over.inc]. P2952 needs to modify this wording (see § 6.5 [over.inc.default] below) to specify exactly how return type deduction works for defaulted increment and decrement operators.

3.5. Other return types are still forbidden

Notice that returning any type other than X& from a defaulted X::operator= 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 void operator= would have to consider very carefully whether to call that operator "trivial." Today, is_trivially_move_assignable_v<T> is true only when decltype(declval<T&>() = declval<T>()) is T&. 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 void instead. And certainly the return-by-copy X operator=(X&&) = default shouldn’t be considered "trivial" for non-trivially-copy-constructible X.

But none of this is a problem for this proposal P2952, because we propose to continue rejecting a defaulted X::operator= that returns any type but X&. We merely propose to allow the programmer to spell that return type using a placeholder such as auto&.

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. The compilers tested are GCC 16, Clang 23, MSVS 18.6, and EDG 6.9.

URL Code GCC Clang MSVC EDG Correct
link
const bool operator==(const C&) const = default;
link
friend bool operator==(const C, const C) = default;
link
decltype(auto) operator<=>(const C&) const = default;
Today:
Proposed: ✓
link
const auto operator<=>(const C&) const = default;
Today:
Proposed: ✗
link
True auto operator<=>(const C&) const = default;
Today:
Proposed: ✓
link
False auto operator<=>(const C&) const = default;
unmet Today:
Proposed: unmet
link
struct U { U(std::strong_ordering); };
struct C {
  U operator<=>(const C&) const = default;
};
deleted Today: ✓
Proposed: deleted
link
struct U { U(std::strong_ordering); operator int(); };
struct C {
  int i;
  U operator<=>(const C&) const = default;
};
deleted deleted
link
struct C {
  int i;
  const std::strong_ordering&
    operator<=>(const C&) const = default;
};
ICE deleted deleted
link
struct C {
  const std::strong_ordering&
    operator<=>(const C&) const = default;
};
deleted Today: ✓
Proposed: deleted
link
auto operator<=>(const M&) const
  noexcept(false) = default;
noexcept incon-
sistent
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;

only since [P2953]
link
C&& operator=(const C&) && = default;

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. Straw poll results

Arthur O’Dwyer presented P2952R1 in the EWG telecon of 2025-01-08. The following straw polls were taken. The first was interpreted as "no consensus"; the second was interpreted as consensus (pending electronic polling).

SF F N A SA
EWG prefers this paper contains the change in P2953
(banning explicitly defaulted operator= with rvalue ref-qualifier).
[Chair: This means EWG wants to see this paper again.]
2 4 9 1 0
Forward P2952R1 to CWG for inclusion in C++26, pending online polling. 3 10 4 1 0

The electronic poll was taken as follows:

SF F N A SA
Forward P2952R1 to CWG for C++26 (pending confirmation during in-person meeting). 3 6 1 1 0

This paper was presented again to EWG in Hagenberg. The following poll was taken:

SF F N A SA
P2952R2 auto& operator=(X&&) = default: forward to CWG for C++26. 5 25 13 5 4

6. Proposed wording

6.1. [class.eq]

DRAFTING NOTE: The phrase "equality operator function" ([over.binary]) means == 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 the return type bool. If its declared return type contains a placeholder type, its return type is deduced as if from return true;.

2․ A defaulted == operator function for a class C is defined as deleted unless, for each xi in the expanded list of subobjects for an object x of type C, xi == xi is usable ([class.compare.default]).

3․ The return value of a defaulted == operator function with parameters x and y is determined by comparing corresponding elements xi and yi in the expanded lists of subobjects for x and y (in increasing index order) until the first index i where xi == yi yields a result value which , when contextually converted to bool, yields false. The return value is false if such an index exists and true otherwise.

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]

6.2. [class.spaceship]

DRAFTING NOTE: There are only three "comparison category types" in C++, and strong_ordering::equal is implicitly convertible to all three of them. The status quo already effectively forbids <=> to return a non-comparison-category type, since either R is deduced as a common comparison type (which is a comparison category type by definition), or else a synthesized three-way comparison of type R must exist (which means R must be a comparison category type), or else the sequence xi must be empty (in which case there are no restrictions on R except that it be constructible from strong_ordering::equal). We strengthen the wording to directly mandate that the return type be a comparison category type, even in the empty case.

DRAFTING NOTE: The "new" wording below incorporates the Tentatively Ready fix for [CWG3207], which tries to ensure that the placeholder return type of a deleted function is never deduced. Our proposed wording in [over.inc.default] also matches CWG3207’s intent. But our proposed wording in [class.eq] and [class.compare.secondary] and [class.copy.assign] do not match CWG3207’s intent. Should CWG3207 therefore remain open? For an alternative wording that almost works, see our R2.

Modify [class.spaceship] as follows:

[...]

2․ Let R be the declared return type of a defaulted three-way comparison operator function, and let xi be the elements of the expanded list of subobjects for an object x of type C.

— (2.1) If R is auto contains a placeholder type , then let cvi R S i be the type of the expression xi <=> xi. The operator function is defined as deleted if that expression is not usable or if R S i is not a comparison category type ([cmp.categories.pre]) for any i. The Otherwise, the return type is deduced as if from return Q(std::strong_ordering::equal); where Q is the common comparison type (see below) of R S 0, R S 1, ..., R S n-1.

— (2.2) Otherwise, R shall not contain a placeholder type. If if the synthesized three-way comparison of type R between any objects xi and xi is not defined, the operator function is defined as deleted.

x․ A defaulted three-way comparison operator function that is not defined as deleted shall have a return type which is a comparison category type ([cmp.categories.pre]).

3․ The return value of type R of the defaulted three-way comparison operator function with parameters x and y of the same type is determined by comparing corresponding elements xi and yi in the expanded lists of subobjects for x and y (in increasing index order) until the first index i where the synthesized three-way comparison of type R between xi and yi yields a result value vi where vi != 0, contextually converted to bool, yields true. The return value is a copy of vi if such an index exists and static_cast<R>(std::strong_ordering::equal) otherwise.

4․ The common comparison type U of a possibly-empty list of n comparison category types T0, T1, ..., Tn-1 is defined as follows: [...]

6.3. [class.compare.secondary]

Modify [class.compare.secondary] as follows:

1․ A secondary comparison operator @ is a relational operator ([expr.rel]) or the != operator. A defaulted operator function ([over.binary]) for a secondary comparison operator @ shall have a declared return type bool.

x․ A defaulted secondary comparison operator function shall have the return type bool. If its declared return type contains a placeholder type, its return type is deduced as if from return true;.

2․ The A defaulted secondary comparison operator function with parameters x and y is defined as deleted if

— (2.1) a first overload resolution ([over.match]), as applied to x @ y,

— (2.1.1) does not result in a usable candidate, or

— (2.1.2) the selected candidate is not a rewritten candidate, or

— (2.2) a second overload resolution for the expression resulting from the interpretation of x @ y using the selected rewritten candidate ([over.match.oper]) does not result in a usable candidate (for example, that expression might be (x <=> y) @ 0), or

— (2.3) x @ y cannot be implicitly converted to bool.

In any of the two overload resolutions above, the defaulted operator function is not considered as a candidate for the @ operator. Otherwise, the operator function yields x @ y.

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]

6.4. [class.copy.assign]

DRAFTING NOTE: [class.copy.assign]/6 already clearly states that "The implicitly-declared copy/move assignment operator for class X has the return type X&." But we need this new wording to ensure that an explicitly-defaulted copy/move assignment operator will deduce that same type. (If it deduces a different type, then the explicitly-defaulted operator becomes ill-formed, as in example B below.)

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 the declared return type of a defaulted copy/move assignment operator for a class X contains a placeholder type, its return type is deduced as if from return r;, where r is an lvalue reference to X.

16․ [Example:

struct A {
  decltype(auto) operator=(A&&) = default;
    // OK, return type is A&
};
struct B {
  auto operator=(B&&) = default;
    // error: return type is B, which violates [dcl.fct.def.default]
};
end example]

6.5. [over.inc.default]

Modify [over.inc.default] as follows:

1․ A defaulted postfix increment or decrement operator function for a type C shall be a non-template function that

— (1.1) has a first parameter of type “reference to C” or a first parameter of type “reference to volatile C”, where the implicit object parameter (if any) is considered to be the first parameter,

— (1.2) is defined as defaulted in C or in a context where C is complete, and

— (1.3) has a declared the return type of C.

[...]

3․ The implicit definition of a defaulted postfix increment or decrement operator function F that is not defined as deleted for a type C is equivalent to

C tmp(c);
++c;
return tmp;
for a postfix increment operator function, or
C tmp(c);
--c;
return tmp;
for a postfix decrement operator function, where tmp is a variable defined for exposition only, and c is an lvalue that denotes *this if F is an implicit object member function, or the first parameter of F otherwise.

x. If the declared return type of a defaulted postfix increment or decrement operator function that is not defined as deleted contains a placeholder type, its return type is deduced as if from return tmp;.

4․ [Example: [...]

6.6. Annex C, [diff.cpp26]

Add a clause to Annex C, [diff.cpp26.class], as follows:

Affected clause: [class.spaceship]
Change: The defaulted three-way comparison operator of an empty class type must return a comparison category type.
Rationale: Consistency with the existing restriction on defaulted three-way comparison operators of non-empty class types.
Effect on original feature: A valid C++26 program that contains a defaulted three-way comparison operator with a return type that is not a comparison category type is ill-formed.
Example:
struct U { U(std::strong_ordering); };
struct C {
  U operator<=>(const C&) const = default;
    // ill-formed; previously well-formed
};

References

Non-Normative References

[CWG2586]
Barry Revzin. Explicit object parameter for assignment and comparison. May–July 2022. URL: https://cplusplus.github.io/CWG/issues/2586.html
[CWG3207]
Arthur O'Dwyer. Deduced return type of a deleted three-way comparison operator function. June–July 2026. URL: https://cplusplus.github.io/CWG/issues/3207.html
[P2953]
Matthew Taylor; Arthur O'Dwyer. Adding restrictions to defaulted assignment operator functions. June 2026. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p2953r5.html
[P3668]
Matthew Taylor; Alex (Waffl3x). Defaulting postfix increment and decrement operations. June 2026. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3668r4.html