Consider a function template f
that binds a non-const lvalue reference to a deduced non-type template parameter
template <auto N>
void f()
{
auto & n = N;
}
This works when f
is instantiated over class types
struct S {};
f<S{}>(); // ok
But doesn't work when instantiated over non class types (as I expected)
f<42>(); // error non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
The same error is emitted if an lvalue argument is used to instantiate f
as well
constexpr int i = 42;
f<i>(); // also error
Here's a demo to play with.
This looks like non-type template parameters of class type are lvalues, which seems odd. Where does the standard make a distinction between these kinds of instantiations, and why is there a difference if the argument to the template is of class type?
CodePudding user response:
The reason for the distinction is because class non-type template parameters weren't always there. Originally, value template parameters could only be pointers, integers, or a few other things. Such parameters were simple and the value was just a single number known at compile-time. As such, making them rvalues (remember: prvalue is C 11) was OK.
Once NTTPs could be more complex object types, you have to start engaging with certain questions. Should you be able to do this:
template<std::array<int, 5> arr>
void func()
{
for(int i: arr)
//stuff
}
The obvious answer is "of course you should." But that would require that you can get a reference to arr
itself. That's how range-based for
is defined, after all.
Now that can still work. After all, range-based for
uses auto&&
to store its reference, so it can reference a prvalue. But that has consequences.
Namely, if you create a reference to a prvalue, that causes the materialization of a temporary. A new temporary object distinct from all other objects.
This means that if you use a class NTTP in multiple places, you will get different objects with different addresses and different addresses for their subobjects. And this is something you can detect, since you can get the addresses of their subobjects.
Forcing compile-time code to create temporaries every time you use the name is bad for performance. So two different uses of such a parameter need to result in talking about the same object.
Therefore, class NTTPs need to be lvalues; each use of the name within a template is referring to the same object. But you can't go back and make all existing NTTPs lvalues too; that would break existing code.
So that's where we are.
As for where this is defined, it is in [temp.param]/8:
An id-expression naming a non-type template-parameter of class type
T
denotes a static storage duration object of typeconst T
, known as a template parameter object, whose value is that of the corresponding template argument after it has been converted to the type of the template-parameter. All such template parameters in the program of the same type with the same value denote the same template parameter object.