Search code examples
c++language-lawyerc++20implicit-conversion

Does lvalue-to-rvalue conversion is applied to non-type lvalue template argument?


struct S{
   constexpr S() {};
};
template <auto x> void f();

int main() {

    S s{};
    f<s>();
}

First off, per [temp.arg.nontype]/1

If the type T of a template-parameter (13.2) contains a placeholder type (9.2.9.6) or a placeholder for a deduced class type (9.2.9.7), the type of the parameter is the type deduced for the variable x in the invented declaration

T x = template-argument;

If a deduced parameter type is not permitted for a template-parameter declaration (13.2), the program is ill-formed.

Our template parameter contains a placeholder type auto, so the type of the parameter is the type deduced for the variable x in the invented declaration auto x = s; In this case, the type of the parameter is S, and S is a permitted type for the parameter declaration because S is a structural literal type.

Second, per [temp.arg.nontype]/2

A template-argument for a non-type template-parameter shall be a converted constant expression (7.7) of the type of the template-parameter.

This means the template-argument s shall be a converted constant expression. So per [expr.const]/10:

A converted constant expression of type T is an expression, implicitly converted to type T, where the converted expression is a constant expression and the implicit conversion sequence contains only

  • [..]
  • (10.2) — lvalue-to-rvalue conversions (7.3.2)
  • [..]

I'm not sure whether or not the lvalue s is converted to prvalue before any implicit conversions are applied to it. Note the definition of converted constant expression in C++14 is relatively changed. N4140 §5.19 [expr.const]/3: (emphasis mine)

A converted constant expression of type T is an expression, implicitly converted to a prvalue of type T, where the converted expression is a core constant expression and the implicit conversion sequence contains only [..]

So per C++14, It's guaranteed that the converted expression is converted to a prvalue before any implicit conversion is applied to it.

I'm not so sure whether or not an lvalue-to-rvalue conversion is applied to template-argument s. In other words, I'm not sure that the lvalue s is converted to a prvalue.

So in general, if I passed an lvalue as a template argument for a non-type template parameter, does an lvalue-to-rvalue conversion applied to that lvalue?

And aside from that, Is the object s constant-initialized?


Solution

  • The meaning of [expr.const]/10 is that whenever an expression appears in a context that requires a "converted constant expression of type T", that expression is "implicitly converted to type T" and the additional restrictions in [expr.const]/10 shall apply.

    So s is not "converted to a prvalue before any implicit conversions are applied to it". Rather, s is implicitly converted to type S, and this implicit conversion might involve some standard and/or user-defined conversions, such as an lvalue-to-rvalue conversion. To know whether or not it does, we would have to look at the rules for implicit conversions, which are in [conv.general]. In particular [conv.general]/6 states that

    The effect of any implicit conversion is the same as performing the corresponding declaration and initialization and then using the temporary variable as the result of the conversion. The result is an lvalue if T is an lvalue reference type or an rvalue reference to function type ([dcl.ref]), an xvalue if T is an rvalue reference to object type, and a prvalue otherwise. The expression E is used as a glvalue if and only if the initialization uses it as a glvalue.

    I believe the wording here is a relic of older standard editions. What it should say (in modern language) is that when T is a non-reference type, an implicit conversion of E to T yields a prvalue that initializes its result object (call it t) as if by T t = E;.

    So we consider S t = s; What does this initialization do? It calls the copy constructor of S to initialize t; [dcl.init.general]/16.6.2.1. There is no lvalue-to-rvalue conversion in this scenario. (Lvalue-to-rvalue conversions on class types are rare, but do occur in some places in the language, e.g., [expr.call]/12, [expr.cond]/7. If an lvalue-to-rvalue conversion were performed on s, it would also call the copy constructor; [conv.lval]/3.2. But in this particular case, the rules of the language do not require an lvalue-to-rvalue conversion.)

    Consequently, the result of using s as a template argument for a template parameter of type S is that a prvalue is generated that initializes its result object by calling the copy constructor with s as the argument. (This particular copy constructor is implicitly defined to not do anything, since there are no subobjects to copy.)

    This answers your question regarding whether an lvalue-to-rvalue conversion is applied. (You might be wondering what actually happens to the prvalue and whether the copy constructor actually gets called, but that's a separate topic that I don't want to get into right now, because it would make this answer too long.)

    As for whether s is constant-initialized, yes it is. It satisfies (2.1) (since it has an initializer) and (2.2) since the full-expression of its initialization is a constant expression (there is nothing to stop it from being a constant expression, since it does nothing other than calling the constexpr default constructor, which itself does nothing). I'm not sure how this is relevant to your other question, though.