Is this an issue in the template argument deduction process | Conversion function template

Viewed 112

Consider the following example:

struct S
{
    template <class T>
    operator const T()
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
        return T();
    }
} s;

int&& res = s; 

Per [temp.deduct.conv]/1: (emphasis mine)

Template argument deduction is done by comparing the return type of the conversion function template (call it P) with the type that is required as the result of the conversion (call it A) [..]

and [temp.deduct.conv]/4: (emphasis mine)

If A is a cv-qualified type, the top-level cv-qualifiers of A's type are ignored for type deduction. If A is a reference type, the type referred to by A is used for type deduction.

and [temp.deduct.conv]/5 (emphasis mine)

In general, the deduction process attempts to find template argument values that will make the deduced A identical to A. However, there are four cases that allow a difference:

  • (5.1) If the original A is a reference type, A can be more cv-qualified than the deduced A (i.e., the type referred to by the reference).
  • [..]

I'm expecting either of the following is occurring:

  • The types of P and A will be: P = const T, A = int&& Now, since A is a reference type, no adjustments occurred in P. But the type A would be adjusted per [temp.deduct.conv]/4. So the final types of P and A is P = const T, A = int. Now we've ended up with a deduction failure, so alternative deductions defined in [temp.deduct.conv]/5 are considered. The bullet (5.1) is what's needed in this case. Now A (int) is not more cv-qualified than deduced A (int). So the program is ill-formed because the initialization is invalid.

  • The reference initialization is invalid duto binding a reference of type int&& to a value of type const Twill drop qualifiers. So the program is ill-formed because the initialization is invalid..

Unfortunately, nothing of what I expected happened. The Live Demo clearly shows that the deduced T for the above example is int.

My question: What am I missing/conflating here?

1 Answers

N4861, to which you link, is not the newest working draft. I'm going to assume that you linked to it because it's the closest draft to C++20. The quotations that I'm going to provide below are from C++20 itself, just in case there are any differences.

According to [temp.deduct.conv]/1, we need to consult [dcl.init], [over.match.conv], and [over.match.ref] to determine what is A.

And in fact, [dcl.init] is really where you should have started looking in the first place, since the overload resolution is triggered by the declaration int&& res = s;.

[dcl.init.ref]/5.3 and 5.4 govern the initialization of rvalue references. 5.3 is tried first:

Otherwise, if the initializer expression

  • is an rvalue (but not a bit-field) or function lvalue and "cv1 T1" is reference-compatible with "cv2 T2", or
  • has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be converted to an rvalue or function lvalue of type "cv3 T3", where "cv1 T1" is reference-compatible with "cv3 T3*" (see [over.match.ref]),

then the value of the initializer expression in the first case and the result of the conversion in the second case is called the converted initializer. If the converted initializer is a prvalue, its type T4 is adjusted to type "cv1 T4" ([conv.qual]) and the temporary materialization conversion ([conv.rval]) is applied. In any case, the reference is bound to the resulting glvalue (or to an appropriate base class subobject).

So we need to consult [over.match.ref] to see if such a cv3 T3 exists (in which case the conversion will be performed, and res will be bound to the result of that conversion). (Note that [over.match.conv] will not be reached in this case; it would be reached if we got to [dcl.init.ref]/5.4, but we will not, because [dcl.init.ref]/5.3 will succeed.)

[over.match.ref] says:

Under the conditions specified in [dcl.init.ref], a reference can be bound directly to the result of applying a conversion function to an initializer expression. Overload resolution is used to select the conversion function to be invoked. Assuming that "reference to cv1 T" is the type of the reference being initialized, and "cv S" is the type of the initializer expression, with S a class type, the candidate functions are selected as follows:

  • The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type "lvalue reference to cv2 T2" (when initializing an lvalue reference or an rvalue reference to function) or "cv2 T2" or "rvalue reference to cv2 T2" (when initializing an rvalue reference or an lvalue reference to function), where "cv1 T" is reference-compatible ([dcl.init.ref]) with "cv2 T2", are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden within S and yield type "lvalue reference to cv2 T2" (when initializing an lvalue reference or an rvalue reference to function) or "rvalue reference to cv2 T2" (when initializing an rvalue reference or an lvalue reference to function), where T2 is the same type as T or can be converted to type T with a qualification conversion ([conv.qual]), are also candidate functions.

The argument list has one argument, which is the initializer expression.

The only type that int is reference-compatible with is int itself, and we are initializing an rvalue reference, so according to this section, we look for conversion functions of S with type int and int&&.

Only at this point, we are ready to perform template argument deduction under [temp.deduct.conv]/1. We will need to do this twice: once with A = int, and once with A = int&&. The results are then merged, and if there is more than one candidate in the resulting set, then we need to use overload resolution to select the one that gets called.

With A = int, [temp.deduct.conv]/3.3 tells us that the top-level cv-qualifiers of P are ignored. Clearly, this results in T being deduced as int; the candidate is S::operator const int().

With A = int&&, in this case the top-level cv-qualifiers of P are not ignored; P remains const T. [temp.deduct.conv]/4 tells us that A is transformed into the type it refers to, namely int. We do the deduction using the transformed types (P = const T, A = int). This fails; there's no T such that const T is int. So there are no candidates generated in this step.

The result is that S::operator const int() is called, producing a prvalue of type const int, but cv-qualification of prvalues of scalar type is discarded prior to any further analysis. So it is materialized into a temporary object of type int, and res is bound to that temporary.

Related