Since C++17, you can use placeholder types (auto and decltype(auto)) as non-type template parameter
types. This means that we can write generic code for non-type parameters of different types.
13.1 Using auto for Template Parameters
Since C++17, you can use auto to declare a non-type template parameter. For example:
template class S {
...
};
This allows us to instantiate the non-type template parameter N for different types:
S<42> s1; // OK: type of N in S is int
S<'a'> s2; // OK: type of N in S is char
However, you cannot use this feature to get instantiations for types that in general are not allowed as template
parameters:
S<2.5> s3; // ERROR: template parameter type still cannot be double
We can even have a specific type as a partial specialization:
template class S {
...
};
Even class template argument deduction is supported. For example:
template<typename T, auto N>
class A {
public:
A(const std::array<T,N>&) {
}
A(T(&)[N]) {
}
...
};
This class can deduce the type of T, the type of N, and the value of N:
A a2{"hello"}; // OK, deduces A<const char, 6> with N being std::size_t
std::array<double,10> sa1;
A a1{sa1}; // OK, deduces A<double, 10> with N being std::size_t
You can also qualify auto, for example, to require the type of the template parameter to be a pointer:
template<const auto* P> struct S;
Furthermore, by using variadic templates, you can parameterize templates to use a list of heterogeneous
constant template arguments:
template<auto... VS> class HeteroValueList {
};
or a list of homogeneous constant template arguments:
template<auto V1, decltype(V1)... VS> class HomoValueList {
};
For example:
HeteroValueList<1, 2, 3> vals1; // OK
HeteroValueList<1, 'a', true> vals2; // OK
HomoValueList<1, 2, 3> vals3; // OK
HomoValueList<1, 'a', true> vals4; // ERROR
13.1.1 Parameterizing Templates for Characters and Strings
One application of this feature is to allow you to pass both a character or a string as a template parameter.
For example, we can improve the way we output an arbitrary number of arguments with fold expressions as
follows:
tmpl/printauto.hpp ✞ ☎
#include
template<auto Sep = ' ', typename First, typename... Args>
void print(const First& first, const Args&... args) {
std::cout << first;
auto outWithSep = [](const auto& arg) {
std::cout << Sep << arg;
};
(... , outWithSep(args));
std::cout << '\n';
}
✝ ✆
We can still print the arguments, with a space being the default argument for the template parameter Sep:
template<auto Sep = ' ', typename First, typename... Args>
void print (const First& firstarg, const Args&... args) {
...
}
That is, we can still call:
std::string s{"world"};
print(7.5, "hello", s); // prints: 7.5 hello world
However, by having print() parameterized for the separator Sep, we can now explicitly pass a different
character as the first template argument:
print<'-'>(7.5, "hello", s); // prints: 7.5-hello-world
Furthermore, due to the use of auto, we can even use the workaround to pass a string literal, by declaring it
as an object with no linkage:
static const char sep[] = ", ";
print(7.5, "hello", s); // prints: 7.5, hello, world
Alternatively, we can pass a separator of any other type that can be used as a template parameter (which can
make more sense than here):
print<-11>(7.5, "hello", s); // prints: 7.5-11hello-11world
13.1.2 Defining Metaprogramming Constants
Another application of the auto feature for template parameters is to make it easier to define compile-time
constants.1
Instead of defining:
template<typename T, T v>
struct constant
{
static constexpr T value = v;
};
using i = constant<int, 42>;
using c = constant<char, 'x'>;
using b = constant<bool, true>;
you can now just do the following:
template
struct constant
{
static constexpr auto value = v;
};
using i = constant<42>;
using c = constant<'x'>;
using b = constant;
Instead of:
template<typename T, T... Elements>
struct sequence {
};
using indexes = sequence<int, 0, 3, 4>;
you can now just implement:
template<auto... Elements>
struct sequence {
};
using indexes = sequence<0, 3, 4>;
You can now even define compile-time objects that represent a heterogeneous list of values (something like
a condensed tuple):
using tuple = sequence<0, 'h', true>;
13.2 Using auto as Variable Template Parameter
You can also use auto as template parameters with variable templates.
2 For example, the following declaration, which might occur in a header file, defines a variable template arr parameterized for the type of
elements and both the type and value of the number of elements:
template<typename T, auto N> std::array<T,N> arr;
In each translation unit, all uses of arr<int,10> share the same global object, while arr<long,10> and
arr<int,10u> would be different global objects (again, both can be used in all translation units).
As a full example, consider the following header file:
tmpl/vartmplauto.hpp ✞ ☎
#ifndef VARTMPLAUTO_HPP
#define VARTMPLAUTO_HPP
#include
template<typename T, auto N> std::array<T,N> arr{};
void printArr();
#endif // VARTMPLAUTO_HPP
✝ ✆
Here, one translation unit could modify the values of two different instances of this variable template:
tmpl/vartmplauto1.cpp ✞ ☎
#include "vartmplauto.hpp"
int main()
{
arr<int,5>[0] = 17;
arr<int,5>[3] = 42;
arr<int,5u>[1] = 11;
arr<int,5u>[3] = 33;
printArr();
}
✝ ✆
Another translation unit could print these two variables:
tmpl/vartmplauto2.cpp ✞ ☎
#include "vartmplauto.hpp"
#include
void printArr()
{
std::cout << "arr<int,5>: ";
for (const auto& elem : arr<int,5>) {
std::cout << elem << ' ';
}
std::cout << "\narr<int,5u>: ";
for (const auto& elem : arr<int,5u>) {
std::cout << elem << ' ';
}
std::cout << '\n';
}
✝ ✆
The output of the program would be:3
arr<int,5>: 17 0 0 42 0
arr<int,5u>: 0 11 0 33 0
In the same way, you can declare a constant variable of an arbitrary type deduced from its initial value:
template constexpr auto val = N; // OK since C++17
and use it later, for example, as follows:
auto v1 = val<5>; // v1 == 5, v1 is int
auto v2 = val; // v2 == true, v2 is bool
auto v3 = val<'a'>; // v3 == ’a’, v3 is char
To clarify what is happening here:
std::is_same_v<decltype(val<5>), int> // yields false
std::is_same_v<decltype(val<5>), const int> // yields true
std::is_same_v<decltype(v1), int>; // yields true (because auto decays)
13.3 Using decltype(auto) as Template Parameter
You can also use the other placeholder type decltype(auto) (introduced with C++14) as template parameter. However, note that this type has very special rules regarding how the type is deduced. According
to the rules of decltype, if expressions instead of names are passed to decltype(auto), the deduction
yields a type that depends on the value category of the expression:
• type for a prvalue (e.g., temporaries)
• type& for an lvalue (e.g., objects with names)
• type&& for an xvalue (e.g., objects marked with std::move()).
That means that you can easily deduce template parameters to become references, which might result in
surprising effects.
For example:
tmpl/decltypeauto.cpp ✞ ☎
#include
template<decltype(auto) N>
struct S {
void printN() const {
std::cout << "N: " << N << '\n';
}
};
static const int c = 42;
static int v = 42;
int main()
{
S s1; // deduces N as const int 42
S<(c)> s2; // deduces N as const int& referring to c
s1.printN();
s2.printN();
S<(v)> s3; // deduces N as int& referring to v
v = 77;
s3.printN(); // prints: N: 77
}