从C++17开始,你可以使用占位符类型(auto
和decltype(auto)
作为非类型模板参数。这意味着我们可以针对不同类型的非模板参数写泛型代码。
从C++17开始,你可以使用auto来声明一个非类型模板参数。比如:
template<auto N> class S {
...
};
这允许我们针对不同类型都可以实例化非类型模板参数N:
S<42> s1; // OK: type of N in S is int
S<'a'> s2; // OK: type of N in S is char
然而,对于那些规则不允许的类型作为模板类型,这个特性仍然是没用的,即不会实例化成功:
S<2.5> s3; // ERROR: template parameter type still cannot be double
我们甚至在偏特化中可以写具体类型:
template<int N> class S<N> {
...
};
甚至支持类模板参数推导。比如:
template<typename T, auto N>
class A {
public:
A(const std::array<T,N>&) {
}
A(T(&)[N]) {
}
...
};
可以推导T的类型,N的类型,N的值。:
A a2{"hello"}; // OK, deduces A<const char, 6> with N being int
std::array<double,10> sa1;
A a1{sa1}; // OK, deduces A<double, 10> with N being std::size_t
你也可以修饰auto
,比如,要求模板参数的类型是一个指针:
template<const auto* P> struct S;
使用可变参数模板,你可以参数化模板,使用一堆同构模板参数:
template<auto... VS> class HeteroValueList {
};
或者一堆异构模板参数:
template<auto V1, decltype(V1)... VS> class HomoValueList {
};
比如:
HeteroValueList<1, 2, 3> vals1; // OK
HeteroValueList<1, 'a', true> vals2; // OK
HomoValueList<1, 2, 3> vals3; // OK
HomoValueList<1, 'a', true> vals4; // ERROR
使用该特性的一种应用是允许同时传入字符和字符串作为模板参数。比如我们可以使用折叠表达式输出任意数量的参数的个数:
#include <iostream>
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';
}
使用空格作为模板参数的一个默认参数,我们可以输出带空格分隔的参数:
template<auto Sep = ' ', typename First, typename... Args>
void print (const First& firstarg, const Args&... args) {
...
}
我们仍然可以调用:
std::string s{"world"};
print(7.5, "hello", s); // prints: 7.5 hello world
但是有了参数化的print()
,而且带分隔符Sep,我们现在可以显式的传递一个不同的字符作为第一个模板参数:
print<' '>(7.5, "hello", s); // prints: 7.5-hello-world
因为用了auto
,我们甚至可以传一个字符串字面值,但这样的话就不得不声明一个没有链接(linkage)的对象:
static const char sep[] = ", ";
print<sep>(7.5, "hello", s); // prints: 7.5, hello, world
或者我们可以传一个分隔符,只要类型是允许作为模板参数的:
print<-11>(7.5, "hello", s); // prints: 7.5-11hello-11world
另一个auto这个特性的使用是更容易的定义编译时常量。你不必这样定义:
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>;
现在你可以这样子做:
template<auto v>
struct constant
{
static constexpr auto value = v;
};
using i = constant<42>;
using c = constant<'x'>;
using b = constant<true>;
也不必:
template<typename T, T... Elements>
struct sequence {
};
using indexes = sequence<int, 0, 3, 4>;
可以这样:
template<auto... Elements>
struct sequence {
};
using indexes = sequence<0, 3, 4>;
现在你甚至可以定义编译时对象来代表一系列异构类型的值:(有点像condensed tuple(译注:这里没理解condensed tuple啥意思,所以保留原文))
using tuple = sequence<0, 'h', true>;
你也可以使用auto作为带变量模板(不要被变量模板(variable template)所困扰,它指的是模板化的变量,并且是可变参数模板,即带有任意数量的参数)的模板参数。比如。下面的声明,可能在一个头文件里,它定义一个变量模板,这个模板是元素类型和元素个数已经被参数化后的:
template<typename T, auto N> std::array<T,N> arr;
在每个翻译单元中,所有使用arr<int,10>
的地方都共享一个全局对象,而arr<long,10>
和arr<int,10u>
将会是不同的全局对象。
下面的头文件将展示一个完整的例子:
#ifndef VARTMPLAUTO_HPP
#define VARTMPLAUTO_HPP
#include <array>
template<typename T, auto N> std::array<T,N> arr{};
void printArr();
#endif // VARTMPLAUTO_HPP
一个翻译单元可以修改这个变量模板的两个不同实例的值:
#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();
}
另一个翻译单元可以打印这两个变量:
#include "vartmplauto.hpp"
#include <iostream>
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';
}
输入结果如下:
arr<int,5>: 17 0 0 42 0
arr<int,5u>: 0 11 0 33 0
与之相同的方式,你可以声明一个常量,其类型是从初始值推导出来的:
template<auto N> constexpr auto val = N; // OK since C++17
然后后面使用它,比如,像下面一样:
auto v1 = val<5>; // v1 == 5, v1 is int
auto v2 = val<true>; // v2 == true, v2 is bool
auto v3 = val<'a'>; // v3 == ’a’, v3 is char
为了说明发生了什么,可以看看下面的例子:
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)
你可以使用另一种占位符类型,由C++14引入的decltype(auto)
。注意,这个东西对于类型是如何推导的有非常特殊的规则。根据decltype
的规则,如果传了个表达式而不是名字,它会根据表达式的值范畴(参见5.3)来推导类型:
- prvalue的类型是
type
(比如临时变量) - lvalue的类型是
type&
(比如对象名字) - xvalue的类型是
type&&
(比如通过std::move()
将对象转换为右值引用)
这意味着,你可以很容易的把参数模板推导成引用,其结果可能出乎意料:
#include <iostream>
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<c> 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
}
对于费类型模板参数可以使用占位符类型首先由James Touton和Michael Spertus在https://wg21.link/n4469中作为其一部分提出。最后的公认措辞是由James Touton和Michael Spertus在https://wg21.link/p0127r2中给出。