Understanding Explicit Object Parameters and Conversion Operators in C++23
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 rejectWhy 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&&). DeducingT = intyields the candidateoperator 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 parameterS&&. - Candidate 2 binds an rvalue (
S{}) to a const lvalue reference parameterconst 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{}tothis const S&. - It returns
4. - The assertion
4 == 10evaluates tofalse(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 overconst S&, selecting the templated overload (returns10). - Explicit calls (#2): Calling
.operator int()directly targets the concreteoperator intmember, bypassing deduction foroperator Tand evaluating the const reference overload (returns4). - Compiler divergence on
#1is due to evolving C++23 support in toolchains like Clang and MSVC.