Cpp Notes

ch13.placeholder_like_auto_as_template_parameters

C++17 - The complete guide, Ch 13: Placeholder Types like auto as Template Parameters

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 dec￾laration, 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 para￾meter. 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 }