Introduction to C++23 Deduced this and Conversion Functions

C++23 introduced "deducing this" (Explicit Object Parameters, defined in P0847R7), allowing member functions to take an explicit object parameter instead of relying on implicit *this mechanics. However, combining explicit object parameters with member conversion functions—especially templated vs. non-templated variants—can lead to surprising compiler divergence and subtleties in overload resolution.

Let's look at an interesting case where an implicit conversion behaves differently from an explicit member function call.

The Problem Scenario

Consider the following snippet involving a templated rvalue conversion operator and a non-templated lvalue-reference conversion operator:

struct S {
    template<typename T>
    constexpr operator T(this S&&) {
        return 10;
    }

    constexpr operator int(this const S&) {
        return 4;
    }
};

static_assert(S{} == 10);                       // #1: GCC/EDG OK, Clang/MSVC reject
// static_assert(S{}.operator int() == 10);     // #2: All major compilers reject

Why do compilers disagree on #1, and why is #2 universally rejected (or expected to evaluate to 4)? What does the C++ Standard actually mandate?

Case #1: Implicit Conversion via S{} == 10

What the Standard Dictates

When evaluating S{} == 10, overload resolution considers candidates to make S{} comparable to int. Because operator== isn't overloaded for S, standard built-in equality comparison candidates are generated. To bind to int == int, the compiler looks for candidate conversion functions in S.

According to [over.match.funcs.general] and [over.match.conv]:

  • Candidate 1 is the function template: template<typename T> constexpr S::operator T(this S&&). Deducing T = int yields the candidate operator int(this S&&).
  • Candidate 2 is the non-template member: constexpr S::operator int(this const S&).

Ranking the Candidates

Both candidates produce an int. Next, the compiler ranks the implicit conversion sequences for the explicit object parameter:

  • Candidate 1 binds an rvalue (S{}) to an rvalue reference parameter S&&.
  • Candidate 2 binds an rvalue (S{}) to a const lvalue reference parameter const S&.

Per [over.ics.rank] paragraph 3.2.3, binding an rvalue to an rvalue reference is a better conversion sequence than binding an rvalue to a const lvalue reference. Therefore, Candidate 1 is the best viable function. S{} is converted via the templated operator returning 10, making 10 == 10 evaluate to true.

Verdict for #1: GCC and EDG implement the standard correctly here. Clang and MSVC rejections stem from incomplete or buggy early implementations of C++23 explicit object parameters during standard conversion operator lookup.

Case #2: Explicit Call via S{}.operator int()

Why does S{}.operator int() fail or not select the template returning 10?

Name Lookup vs. Template Specialization

When calling .operator int() explicitly, name lookup is performed for the specific identifier operator int.

Per [temp.arg.explicit], template arguments for conversion function templates cannot be explicitly specified in a member access expression like S{}.operator int<int>(). More importantly, ordinary member name lookup for operator int finds the non-template member function declared as operator int(this const S&).

Because the non-template overload operator int(this const S&) matches the lookup, overload resolution is restricted to candidates named operator int. Even though the templated conversion operator can convert to int via deduction during implicit conversions, explicit naming of a conversion-type-id (here, int) does not trigger template argument deduction for the template template<typename T> operator T() in the same manner.

Thus, S{}.operator int() calls the non-template member:

  • It binds S{} to this const S&.
  • It returns 4.
  • The assertion 4 == 10 evaluates to false (or fails if the compiler rejects explicit member invocation with explicit object parameters).

Summary and Takeaways

  • Implicit conversions (#1): The compiler considers both non-template and deduced template conversion functions. Binding S&& to a temporary is preferred over const S&, selecting the templated overload (returns 10).
  • Explicit calls (#2): Calling .operator int() directly targets the concrete operator int member, bypassing deduction for operator T and evaluating the const reference overload (returns 4).
  • Compiler divergence on #1 is due to evolving C++23 support in toolchains like Clang and MSVC.