chapter13 占位符类型作为模板参数(例如auto

自从C++17起,你可以使用占位符类型(autodecltype(auto))作为非类型模板参数的类型。 这意味着我们可以写出泛型代码来处理不同类型的非类型模板参数。

13.1 使用auto模板参数

自从C++17起,你可以使用auto来声明非类型模板参数。例如:

  1. template<auto N> class S {
  2. ...
  3. };

这允许我们为不同类型实例化非类型模板参数N

  1. S<42> s1; // OK:S中N的类型是int
  2. S<'a'> s2; // OK:S中N的类型是char

然而,你不能使用这个特性来实例化一些不允许作为模板参数的类型:

  1. S<2.5> s3; // ERROR:模板参数的类型不能是double

我们甚至还可以用指明类型的版本作为部分特化版:

  1. template<int N> class S<N> {
  2. ...
  3. };

甚至还支持类模板参数推导。例如:

  1. template<typename T, auto N>
  2. class A {
  3. public:
  4. A(const std::array<T, N>&) {
  5. }
  6. A(T(&)[N]) {
  7. }
  8. ...
  9. };

这个类可以推导出T的类型、N的类型、N的值:

  1. A a2{"hello"}; // OK,推导为A<const char, 6>,N的类型是std::size_t
  2. std::array<double, 10> sa1;
  3. A a1{sa1}; // OK,推导为A<double, 10>,N的类型是std::size_t

你也可以修饰auto,例如,可以确保参数类型必须是个指针:

  1. template<const auto* P> struct S;

另外,通过使用可变参数模板,你可以使用多个不同类型的模板参数来实例化模板:

  1. template<auto... VS> class HeteroValueList {
  2. };

也可以用多个相同类型的参数:

  1. template<auto V1, decltype(V1)... VS> class HomoValueList {
  2. };

例如:

  1. HeteroValueList<1, 2, 3> vals1; // OK
  2. HeteroValueList<1, 'a', true> vals2; // OK
  3. HomoValueList<1, 2, 3> vals3; // OK
  4. HomoValueList<1, 'a', true> vals4; // ERROR

13.1.1 字符和字符串模板参数

这个特性的一个应用就是你可以定义一个既可能是字符也可能是字符串的模板参数。 例如,我们可以像下面这样改进用折叠表达式输出任意数量参数 的方法:

  1. #include <iostream>
  2. template<auto Sep = ' ', typename First, typename... Args>
  3. void print(const First& first, const Args&... args) {
  4. std::cout << first;
  5. auto outWithSep = [] (const auto& arg) {
  6. std::cout << Sep << arg;
  7. };
  8. (... , outWithSep(args));
  9. std::cout << '\n';
  10. }

将默认的参数分隔符Sep设置为空格,我们可以实现和之前相同的效果:

  1. template<auto Sep = ' ', typename First, typename... Args>
  2. void print(const First& firstarg, const Args&... args) {
  3. ...
  4. }

我们仍然可以像之前一样调用:

  1. std::string s{"world"};
  2. print(7.5, "hello", s); // 打印出:7.5 hello world

然而,通过把分隔符Sep参数化,我们也可以显示指明另一个字符作为分隔符:

  1. print<'-'>(7.5, "hello", s); // 打印出:7.5-hello-world

甚至,因为使用了auto,我们甚至可以传递被声明为无链接 的字符串字面量作为分隔符:

  1. static const char sep[] = ", ";
  2. print<sep>(7.5, "hello", s); // 打印出:7.5, hello, world

另外,我们也可以传递任何其他可以用作模板参数的类型:

  1. print<-11>(7.5, "hello", s); // 打印出:7.5-11hello-11world

13.1.2 定义元编程常量

auto模板参数特性的另一个应用是可以让我们更轻易的定义编译期常量。

原本的下列代码:

  1. template<typename T, T v>
  2. struct constant
  3. {
  4. static constexpr T value = v;
  5. };
  6. using i = constant<int, 42>;
  7. using c = constant<char, 'x'>;
  8. using b = constant<bool, true>;

现在可以简单的实现为:

  1. template<auto v>
  2. struct constant
  3. {
  4. static constexpr auto value = v;
  5. };
  6. using i = constant<42>;
  7. using c = constant<'x'>;
  8. using b = constant<true>;

同样,原本的下列代码:

  1. template<typename T, T... Elements>
  2. struct sequence {
  3. };
  4. using indexes = sequence<int, 0, 3, 4>;

现在可以简单的实现为:

  1. template<auto... Elements>
  2. struct sequence {
  3. };
  4. using indexes = sequence<0, 3, 4>;

你现在甚至可以定义一个持有若干不同类型的值的编译期对象(类似于一个简单的tuple):

  1. using tuple = sequence<0, 'h', true>;

13.2 使用auto作为变量模板的参数

你也可以使用auto作为模板参数来实现 变量模板(variable templates)

例如,下面的声明定义了一个变量模板arr,它的模板参数分别是元素的类型和数量:

  1. template<typename T, auto N> std::array<T, N> arr;

在每个编译单元中,所有对arr<int, 10>的引用将指向同一个全局对象。 而arr<long, 10>arr<int, 10u>将指向其他对象 (每一个都可以在所有编译单元中使用)。

作为一个完整的例子,考虑如下的头文件:

  1. #ifndef VARTMPLAUTO_HPP
  2. #define VARTMPLAUTO_HPP
  3. #include <array>
  4. template<typename T, auto N> std::array<T, N> arr{};
  5. void printArr();
  6. #endif // VARTMPLAUTO_HPP

这里,我们可以在一个编译单元内修改两个变量模板的不同实例:

  1. #include "vartmplauto.hpp"
  2. int main()
  3. {
  4. arr<int, 5>[0] = 17;
  5. arr<int, 5>[3] = 42;
  6. arr<int, 5u>[1] = 11;
  7. arr<int, 5u>[3] = 33;
  8. printArr();
  9. }

另一个编译单元内可以打印这两个变量模板:

  1. #include "vartmplauto.hpp"
  2. #include <iostream>
  3. void printArr()
  4. {
  5. std::cout << "arr<int, 5>: ";
  6. for (const auto& elem : arr<int, 5>) {
  7. std::cout << elem << ' ';
  8. }
  9. std::cout << "\narr<int, 5u>: ";
  10. for (const auto& elem : arr<int, 5u>) {
  11. std::cout << elem << ' ';
  12. }
  13. std::cout << '\n';
  14. }

程序的输出将是:

  1. arr<int, 5>: 17 0 0 42 0
  2. arr<int, 5u>: 0 11 0 33 0

用同样的方式你可以声明一个任意类型的常量变量模板,类型可以通过初始值推导出来:

  1. template<auto N> constexpr auto val = N; // 自从C++17起OK

之后可以像下面这样使用:

  1. auto v1 = val<5>; // v1 == 5,v1的类型为int
  2. auto v2 = val<true>; // v2 == true,v2的类型为bool
  3. auto v3 = val<'a'>; // v3 == 'a',v3的类型为char

这里解释了发生了什么:

  1. std::is_same_v<decltype(val<5>), int> // 返回false
  2. std::is_same_v<decltype(val<5>), const int> // 返回true
  3. std::is_same_v<decltype(v1), int> // 返回true(因为auto会退化)

13.3 使用decltype(auto)模板参数

你现在也可以使用另一个占位类型decltype(auto)(C++14引入)作为模板参数。 注意,这个占位类型的推导有非常特殊的规则。根据decltype的规则,如果使用 decltype(auto)来推导 表达式(expressions) 而不是变量名, 那么推导的结果将依赖于表达式的值类型:

  • prvalue(例如临时变量)推导出 type
  • lvalue(例如有名字的对象)推导出 type&
  • xvalue(例如用std::move()标记的对象)推导出 type&&

这意味着你很容易就会把模板参数推导为引用,这可能导致一些令人惊奇的效果。

例如:

  1. #include <iostream>
  2. template<decltype(auto) N>
  3. struct S {
  4. void printN() const {
  5. std::cout << "N: " << N << '\n';
  6. }
  7. };
  8. static const int c = 42;
  9. static int v = 42;
  10. int main()
  11. {
  12. S<c> s1; // N的类型推导为const int 42
  13. S<(c)> s2; // N的类型推导为const int&,N是c的引用
  14. s1.printN();
  15. s2.printN();
  16. S<(v)> s3; // N的类型推导为int&,N是v的引用
  17. v = 77;
  18. s3.printN(); // 打印出:N: 77
  19. }