Caiwen的博客

C++ 模板元编程

2026-08-30 09:52

1. 模板

1.1 类模板

类模板声明:

C++
1
template <typename T> class ClassA;

类模板定义:

C++
1
2
3
4
template <typename T> class ClassA { T member; };

template 是C++关键字,意味着我们接下来将定义一个模板。和函数一样,模板也有一系列参数。这些参数都被囊括在template之后的< >中。在上文的例子中, typename T便是模板参数。在定义类的时候,除了一般类可以使用的类型外,你还可以使用在模板参数中使用的类型 T,你可以通过指定模板实参,将T替换成你所需要的类型。

模板的这种用法,我们称之为“泛型”,它最常见的应用,即是STL中的容器类模板。比如vector,它对于任意的元素类型都具有 push_back 和 clear 的操作。

C++
1
2
3
4
5
6
7
8
9
10
template <typename T> class vector { public: void push_back(T const&); void clear(); private: T* elements; };

类模板作为模板,不能直接用来定义变量:

C++
1
vector unknownVector; // 错误示例

这样就是错误的。我们把通过类型绑定将类模板变成“普通的类”的过程,称之为模板实例化。实例化的语法是:

C++
1
模板名 < [模板实参1,模板实参2,...] >

对于类模板,如果要在类的类型之外写成员函数的定义,需要这么写:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T> class vector { public: void clear(); // 注意这里只有声明 private: T* elements; }; template <typename T> void vector<T>::clear() // 函数的实现放在这里 { // Function body }

1.2 函数模板

比如

C++
1
2
3
4
template <typename T> T Add(T a, T b) { return a + b; }

函数模板的调用格式是:

Unknown
1
函数模板名 < 模板参数列表 > ( 参数 )

例如,我们想对两个 int 求和,那么套用类的模板实例化方法,我们可以这么写:

C++
1
2
3
int a = 5; int b = 3; int result = Add<int>(a, b);

这时我们等于拥有了一个新函数:

C++
1
int Add<int>(int a, int b) { return a + b; }

模板参数推导

实际上编译器会根据传入的参数类型推导出模板参数,直接这么写也是可以的:

C++
1
int result = Add(a, b);

但如下的例子,编译器不能直接完成模板参数的推导:

C++
1
2
3
4
5
6
int a = 5; char b = 3; int result = Add(a, b); // Visual Studio 2012 // error C2782: 'T _1_2_2::Add(T,T)' : template parameter 'T' is ambiguous

以及编译器无法根据返回值类型来进行推导,函数调用的时候,返回值被谁接受还不好说了。

C++
1
2
3
4
5
6
7
8
9
10
11
float data[1024]; template <typename T> T GetValue(int i) { return static_cast<T>(data[i]); } float a = GetValue(0); // 出错了! int b = GetValue(1); // 也出错了! float a = GetValue<float>(0); // ok int b = GetValue<int>(1); // ok

如果你只想让编译器推导一部分模板参数,那么需要把要被推导的模板参数写到模板参数列表的后面:

C++
1
2
3
4
5
6
7
8
9
// 执行 C 风格的类型转换 template <typename DstT, typename SrcT> DstT c_style_cast(SrcT v) { return (DstT)(v); } int v = 0; // 只需要手动指定 DstT,SrcT 会由编译器根据传入的函数参数类型自动推导 float i = c_style_cast<float>(v);

1.3 非类型模板参数

整型

模板参数除了类型外,也可以是一个整型数(Integral Number)。这里的整型数比较宽泛,包括布尔型,不同位数、有无符号的整型,甚至包括指针。

比如:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T, int Size> struct Array { T data[Size]; }; Array<int, 16> arr; // 等价于 class IntArrayWithSize16 { int data[16]; // int 替换了 T, 16 替换了 Size }; IntArrayWithSize16 arr;

因为模板的匹配是在编译的时候完成的,所以实例化模板的时候所使用的参数,也必须要在编译期就能确定。例如以下的例子编译器就会报错:

C++
1
2
3
4
5
6
7
8
template <int i> class A {}; void foo() { int x = 3; A<5> a; // 正确! A<x> b; // error C2971: '_1_3::A' : template parameter 'i' : 'x' : a local variable cannot be used as a non-type argument }

非整型

C++ 20 放宽了模板参数类型,允许浮点类型和结构体/类这种复合类型。编译器在比较两个模板参数是否相等的时候,会比较数据在内存角度是否完全一致,或者说比较的时候会忽略掉对 == 运算符的重载。

浮点数:

C++
1
template <float a> class E {};

结构体:

C++
1
2
3
4
5
struct two_part { int a; char b; /* constexpr 构造 */ }; template <two_part P> struct tagged {}; static_assert(!std::is_same_v<tagged<two_part{1,'x'}>, tagged<two_part{1,'y'}>>); // 只差 b static_assert(!std::is_same_v<tagged<two_part{2,'x'}>, tagged<two_part{1,'x'}>>); // 只差 a

字符串不能直接作为模板参数:

C++
1
2
3
4
5
template <const char* S> struct bad_field {}; using X = bad_field<"id">; // error: '"id"' is not a valid template argument for type 'const char*' // because string literals can never be used in this context

这是因为模板实例化要求同样的参数必须对应同一个类型。而字符串字面量 "id" 在不同编译单元里地址可能不同,编译器没法保证 A 文件的 "id" 和 B 文件的 "id" 是同一个指针。地址不确定,就没法保证类型唯一,所以标准直接禁止。

一个解决办法是,将字符串包装成一个对象,对象中用数组存储字符串的内容。这样就会比较两个结构体的具体内容,而不是比较地址:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <algorithm> #include <cstddef> template <std::size_t N> struct fixed_string { char data[N]; constexpr fixed_string(const char (&text)[N]) { std::copy_n(text, N, data); } }; template <fixed_string Name> struct field { static constexpr auto name = Name; }; using IdField = field<"id">; using NameField = field<"name">; static_assert(IdField::name.data[0] == 'i'); static_assert(!std::is_same_v<IdField, NameField>); // 不同名 → 不同类型 static_assert(std::is_same_v<field<"id">, IdField>); // 同名 → 同一类型

其中 template <fixed_string Name> 并不需要写 fixed_string 的模板参数,这是因为编译器在这里会自动推导 N

2. 模板运算

模板元编程可以被看作是一套在编译期运行的“小语言”。我们可以将类型和值(非类型模板参数)看作是数据,模板看作是函数,模板特化看作是分支。比如用类模板计算阶乘:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <cstddef> template <std::size_t N> struct Factorial { static constexpr std::size_t value = N * Factorial<N - 1>::value; }; template <> struct Factorial<0> { static constexpr std::size_t value = 1; }; static_assert(Factorial<5>::value == 120);

编译器为了得到 Factorial<5>::value,会依次实例化 Factorial<4>Factorial<3>,直到遇到 Factorial<0>。主模板负责递归,Factorial<0> 的显式特化负责终止。

当然这个实现只是用来理解模板元编程,而不建议在实际代码中使用。

2.1 返回值

结构体通常可作为模板元编程中的函数,模板参数作为函数参数,结构体中的成员作为函数的返回值。返回值一般有两种:值或是类型。通常约定返回类型放在 ::type 中,返回值放在 ::value 中:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 返回值 template <int A, int B> struct Add { static constexpr int value = A + B; }; // 返回类型 template <bool Condition, typename TrueType, typename FalseType> struct Choose { using type = TrueType; }; template <typename TrueType, typename FalseType> struct Choose<false, TrueType, FalseType> { using type = FalseType; }; static_assert(Add<2, 3>::value == 5); static_assert(std::is_same_v< Choose<false, int, double>::type, double >);

像是

C++
1
std::is_integral<int>::value

在 C++ 14 中提供了一种更简洁的写法:

C++
1
std::is_integral_v<int>

通常约定 _v 对应 ::value_t 对应 ::type

_t 是通过模板别名来实现的:

C++
1
2
3
template <typename T> using add_const_pointer_t = typename add_const_pointer<T>::type;

_v 是通过变量模板来实现的:

C++
1
2
3
template <typename T> inline constexpr bool is_integral_v = is_integral<T>::value;

很多模板的作用是判断某个类型是否满足某条件,返回值为 bool 类型。对于这种模板,可以让结构体继承 std::true_type 或是 std::false_type,这两个东西定义了 value 字段,前者 value 恒为 true,后者 value 恒为 false,于是就不需要自己再重复写要返回的 value 字段了:

C++
1
2
3
4
5
6
7
template <typename T> struct my_is_pointer : std::false_type {}; template <typename T> struct my_is_pointer<T*> : std::true_type {}; // my_is_pointer::value 可以取到布尔值,而不用再 struct 中自行定义 value 字段

2.2 特化

特化相当于是模板元编程中的分支,并且也是模板元编程的核心。

2.2.1 显式特化

显式特化为一个精确的模板实参组合提供专门实现,比如:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 首先,要写出模板的一般形式(原型) template <typename T> class AddFloatOrMulInt { static T Do(T a, T b) { // 在这个例子里面一般形式里面是什么内容不重要,因为用不上 // 这里就随便给个0吧。 return T(0); } }; // 其次,我们要指定T是int时候的代码,这就是特化: template <> class AddFloatOrMulInt<int> { public: static int Do(int a, int b) // { return a * b; } }; // 再次,我们要指定T是float时候的代码: template <> class AddFloatOrMulInt<float> { public: static float Do(float a, float b) { return a + b; } }; void foo() { // 这里面就不写了 }

定义模板的特化形式之前必须先声明模板的一般形式。其中 template <> 的形式较为奇怪,解释如下:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 我们这个模板的基本形式是什么? template <typename T> class AddFloatOrMulInt; // 但是这个类,是给T是Int的时候用的,于是我们写作 class AddFloatOrMulInt<int>; // 当然,这里编译是通不过的。 // 但是它又不是个普通类,而是类模板的一个特化(特例)。 // 所以前面要加模板关键字template, // 以及模板参数列表 template </* 这里要填什么? */> class AddFloatOrMulInt<int>; // 最后,模板参数列表里面填什么?因为原型的T已经被int取代了。所以这里就不能也不需要放任何额外的参数了。 // 所以这里放空。 template <> class AddFloatOrMulInt<int> { // ... 针对Int的实现 ... };

2.2.2 偏特化

偏特化不是匹配一个精确类型,而是匹配一类形状。

C++
1
2
3
4
5
6
7
8
9
10
11
#include <type_traits> template <typename T> struct my_is_pointer : std::false_type {}; template <typename T> struct my_is_pointer<T*> : std::true_type {}; static_assert(!my_is_pointer<int>::value); static_assert(my_is_pointer<int*>::value); static_assert(my_is_pointer<const int*>::value);

my_is_pointer<T*> 可以匹配所有指针类型。这里的 T 会被推导为指针指向的类型。

2.3 类型操作

标准库中已经编写出来很多用于操作类型的工具模板,这种工具类模板被称为 type trait。

2.3.1 类型变换

C++
1
2
3
4
5
6
7
8
9
#include <type_traits> using A = std::remove_reference_t<int&>;// 去掉类型的引用 using B = std::remove_const_t<const int>;// 去掉类型的 const using C = std::add_pointer_t<int>;// 给类型加上指针 static_assert(std::is_same_v<A, int>); static_assert(std::is_same_v<B, int>); static_assert(std::is_same_v<C, int*>);

remove_cvref_t<T> 只移除引用和顶层 cv 限定。decay_t<T> 还会让数组退化为指针,让函数类型退化为函数指针。

C++
1
2
3
4
5
#include <type_traits> using A = std::remove_cvref_t<const int&>;// int using B = std::decay_t<const int&>;// int using C = std::decay_t<int[3]>;// int*

2.3.2 类型判断

C++
1
2
3
4
5
6
7
#include <type_traits> static_assert(std::is_integral_v<int>);// 判断是否为整数类型 static_assert(std::is_floating_point_v<double>);// 判断是否为浮点数类型 static_assert(std::is_pointer_v<int*>);// 判断是否为指针类型 static_assert(std::is_same_v<int, int>);//判断两个类型是否相同 static_assert(std::is_base_of_v<std::exception, std::runtime_error>);// 判断一个类型的基类是否为另一个类型

2.3.3 类型选择

使用 std::conditional 可以实现根据不同的条件选择类型,比如:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 根据 T 是否为整形来选择类型 template <typename T> using result_type = std::conditional_t< std::is_integral_v<T>, long long, double >; // 根据类型存储的大小来选择类型 template <typename T> using storage_type = std::conditional_t< (sizeof(T) <= 4), std::uint32_t, std::uint64_t >;

2.3.4 integral_constant

std::integral_constant<T, v> 把一个编译期值包装成一个类型,比如:

C++
1
2
3
4
5
6
#include <type_traits> using Four = std::integral_constant<int, 4>; static_assert(Four::value == 4); static_assert(std::is_same_v<Four::value_type, int>);

这里的 Four 不是整数变量,而是一个类型。这个类型内部携带整数 4

std::integral_constant 可以让我们把同一个类型的不同值当作不同的类型区分开:

C++
1
2
3
using Three = std::integral_constant<int, 3>; using Four = std::integral_constant<int, 4>; static_assert(!std::is_same_v<Three, Four>);

模板系统擅长根据类型做选择。例如可以针对 ThreeFour 提供不同重载:

C++
1
2
3
4
5
6
7
8
void process(std::integral_constant<int, 3>) { // 针对 3 的实现 } void process(std::integral_constant<int, 4>) { // 针对 4 的实现 } process(std::integral_constant<int, 3>{}); process(std::integral_constant<int, 4>{});

用 integral_constant 得到的类型还能进行运算:

C++
1
2
3
4
5
6
7
8
9
10
11
12
template <typename A, typename B> struct add : std::integral_constant< int, A::value + B::value > {}; using Two = std::integral_constant<int, 2>; using Three = std::integral_constant<int, 3>; using Five = std::integral_constant<int, 5>; static_assert(std::is_same_v<Five, add<Two, Three>>());

其中比较常见的是标准库中的std::true_typestd::false_type ,这两个分别是 std::integral_constant<bool, true>std::integral_constant<bool, false>

2.4 递归

通过递归,我们可以实现类似循环的效果:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 1. 计算一个类型嵌套了几层 vector template <typename T> struct vector_depth_impl : std::integral_constant<std::size_t, 0> {}; template <typename Element, typename Allocator> struct vector_depth_impl<std::vector<Element, Allocator>> : std::integral_constant< std::size_t, 1 + vector_depth_impl<Element>::value > {}; template <typename T> struct vector_depth : vector_depth_impl<std::remove_cvref_t<T>> {}; template <typename T> inline constexpr std::size_t vector_depth_v = vector_depth<T>::value; // 2. 给类型包装上指定层数的 vector template <typename T, std::size_t K> struct wrap_vector { using type = std::vector<typename wrap_vector<T, K - 1>::type>; }; template <typename T> struct wrap_vector<T, 0> { using type = T; }; template <typename T, std::size_t K> using wrap_vector_t = typename wrap_vector<T, K>::type; // 测试 using V0 = int; using V1 = std::vector<int>; using V2 = std::vector<std::vector<int>>; using V3 = std::vector<std::vector<std::vector<int>>>; static_assert(vector_depth_v<V0> == 0); static_assert(vector_depth_v<V1> == 1); static_assert(vector_depth_v<V2> == 2); static_assert(vector_depth_v<V3> == 3); static_assert(vector_depth_v<const V3&> == 3); static_assert(std::is_same_v< wrap_vector_t<int, 0>, V0 >); static_assert(std::is_same_v< wrap_vector_t<int, 1>, V1 >); static_assert(std::is_same_v< wrap_vector_t<int, 2>, V2 >); static_assert(std::is_same_v< wrap_vector_t<int, 3>, V3 >);

2.5 变参模板

模板支持个数不定的参数:

C++
1
template <typename... Ts> class tuple;

我们可以获得参数包的个数:

C++
1
2
3
4
5
6
template <typename... Ts> struct type_count { static constexpr std::size_t value = sizeof...(Ts); }; static_assert(type_count<int, double, char>::value == 3);

我们还可以递归地将参数包中的参数给解出来,比如如下实现判断一个类型是否存在一个列表中:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename... Ts> struct type_list {}; using supported_types = type_list<int, double, std::string>; template <typename T, typename List> struct contains; template <typename T> struct contains<T, type_list<>> : std::false_type {}; template <typename T, typename Head, typename... Tail> struct contains<T, type_list<Head, Tail...>> : std::conditional_t< std::is_same_v<T, Head>, std::true_type, contains<T, type_list<Tail...>> > {}; static_assert(contains<double, supported_types>::value); static_assert(!contains<char, supported_types>::value);

2.6 高阶元函数

如果一个模板不仅接收数据,还接收“如何处理数据的模板”,它就具有高阶函数的味道,比如:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// transform 可以对一个列表中的所有元素都进行某个操作 template <typename F, typename List> struct transform; template <typename F, typename... Ts> struct transform<F, type_list<Ts...>> { using type = type_list< typename F::template apply<Ts>::type... >; }; // filter 可以只将一个列表中满足某个条件的元素给过滤掉 template <typename Pred, typename List> struct filter; template <typename Pred> struct filter<Pred, type_list<>> { using type = type_list<>; }; template <typename T, typename List> struct push_front; template <typename T, typename... Ts> struct push_front<T, type_list<Ts...>> { using type = type_list<T, Ts...>; }; template <typename Pred, typename Head, typename... Tail> struct filter<Pred, type_list<Head, Tail...>> { private: using filtered_tail = typename filter<Pred, type_list<Tail...>>::type; public: using type = std::conditional_t< Pred::template apply<Head>::value, typename push_front<Head, filtered_tail>::type, filtered_tail >; };
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct add_pointer_fn { template <typename T> using apply = std::add_pointer<T>; }; struct is_integral_pred { template <typename T> using apply = std::is_integral<T>; }; using input = type_list<int, double, char>; using pointers = typename transform<add_pointer_fn, input>::type; using integers = typename filter<is_integral_pred, input>::type; static_assert(std::is_same_v< pointers, type_list<int*, double*, char*> >); static_assert(std::is_same_v< integers, type_list<int, char> >);

3. 代码生成

我们最后的目标还是希望能根据模板运算出来的结果来生成不同的代码逻辑。

3.1 类型分支

单纯针对于类型本身进行的分支选择

3.1.1 标签分派

结合 integral_constant 和函数重载,我们可以做到按照不同的类型来执行不同的逻辑,这种手法也叫做标签分派:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream> #include <type_traits> template <typename T> void print_impl(const T& value, std::true_type) { std::cout << "整数:" << value << '\n'; } template <typename T> void print_impl(const T&, std::false_type) { std::cout << "不是整数\n"; } template <typename T> void print(const T& value) { print_impl(value, std::is_integral<T>{}); } print(42); // 选择 true_type 版本 print(3.14); // 选择 false_type 版本

std::bool_constant<表达式> 可以把一个布尔表达式重新包装成 std::false_typestd::true_type,于是我们可以实现组合逻辑:

C++
1
2
3
4
5
6
template <typename T> struct is_signed_integer : std::bool_constant< std::is_integral_v<T> && std::is_signed_v<T> > {};

3.1.2 enable_if

C++ 在从多个候选的模板中决定选择哪个的时候会存在 SFINAE 机制。SFINAE 是 “Substitution Failure Is Not An Error”,如果把模板实参代入函数声明的直接上下文后发现表达式或类型无效,这个候选可以被移出重载集合,而不是让整个编译立刻失败。比如:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct X { typedef int type; }; struct Y { typedef int type2; }; template <typename T> void foo(typename T::type); // Foo0 template <typename T> void foo(typename T::type2); // Foo1 template <typename T> void foo(T); // Foo2 void callFoo() { foo<X>(5); // Foo0: Succeed, Foo1: Failed, Foo2: Failed foo<Y>(10); // Foo0: Failed, Foo1: Succeed, Foo2: Failed foo<int>(15); // Foo0: Failed, Foo1: Failed, Foo2: Succeed }

上面的代码中,当我们把 Y 作为模板参数的时候,首先尝试 Foo0 这个模板,发现 Y 根本没有 type 这个成员,所以跳过,转而尝试下一个。尝试 Foo1 这个模板时发现可以得到 Y::type2,于是就选择了这个模板。

std::enable_if<Condition, T> 有这样一种性质:在条件为真时提供成员类型 type = T;条件为假时没有 type。这个有类型或没有类型的差异会触发 SFINAE,于是就可以帮助我们进行分支选择:

常见写法有三种:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 放在返回类型 template <typename T> std::enable_if_t<std::is_integral_v<T>, T> normalize(T value); // 放在普通函数参数 template <typename T> void normalize(T value, std::enable_if_t<std::is_integral_v<T>>* = nullptr); // 放在额外模板参数,通常更容易读 template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0> T normalize(T value);

这样可以做到,T 类型满足某个条件时调用函数的某个重载,不满足某个条件时去调用另一个重载。

3.1.3 if constexpr

在 C++ 17 中引入了 if constexpr,可以帮助我们更好地进行分支选择的逻辑:

C++
1
2
3
4
5
6
7
8
template <typename T> void print(const T& value) { if constexpr (std::is_integral_v<T>) { std::cout << "整数:" << value << '\n'; } else { std::cout << "不是整数\n"; } }

3.2 类型能力分支

有时候我们还希望判断一个类型能否具备某种能力,比如是否支持 .func() 含有某个成员方法,来决定调用函数的哪个重载。

3.2.1 decltype

decltype 有个性质:当计算的表达式不合法时会触发 SFINAE 机制。

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Counter { void increase() { // Implements } }; template <typename T> void inc_counter(T& intTypeCounter, std::decay_t<decltype(++intTypeCounter)>* = nullptr) { ++intTypeCounter; } template <typename T> void inc_counter(T& counterObj, std::decay_t<decltype(counterObj.increase())>* = nullptr) { counterObj.increase(); } void doSomething() { Counter cntObj; uint32_t cntUI32; // blah blah blah inc_counter(cntObj); inc_counter(cntUI32); }

这样就做到了,对于可以 ++ 运算的类型调用 ++ 的重载,可以 .increase() 的类型调用 .increase() 的重载。

3.2.2 void_t

如果我们只是想写一个模板来判断某个类型是否具有某个能力,可以使用 std::void_t<Ts...>

std::void_t<Ts...> 在所有 Ts... 都是合法类型时得到 void。如果其中某个类型表达式不合法,就会触发 SFINAE 机制:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <type_traits> #include <utility> template <typename, typename = void> struct has_size : std::false_type {}; template <typename T> struct has_size<T, std::void_t< decltype(std::declval<const T&>().size()) >> : std::true_type {}; static_assert(has_size<std::string>::value); static_assert(!has_size<int>::value);

其中 std::declval<const T&>() 表示假设这里存在一个 const T& 类型的值。当 std::declval<const T&>().size() 合法时,std::void_t 会返回 void,然后编译器会认为下面的这个模板比上面的那个模板更加特化,于是就选择了下面这个。

于是我们可以编写判断某个类型是否具有某个能力的模板,然后再结合 if constexpr 来进行更好地分支选择。

3.2.3 concepts

C++20 引入了 concepts 这个特性,允许我们直接写出模板参数必须具备什么能力:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <concepts> // 写法1 // 使用标准库中已有的约束 template <std::integral T> T twice(T value); // 自己创建约束 template <typename T> concept Addable = requires(T a, T b) { { a + b } -> std::convertible_to<T>; }; template <Addable T> constexpr T add(T a, T b) { return a + b; } // 写法2: requires 子句放在模板参数之后 template <typename T> requires std::integral<T> T twice(T value); // 写法3: 尾置 requires 子句 template <typename T> T twice(T value) requires std::integral<T>; // 写法4: 缩写函数模板 std::integral auto twice(std::integral auto value);

Requires 表达式的成立会检查如下几个方面:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T> concept ContainerLike = requires(T c, const T cc) { // 1. 简单要求:表达式必须有效 c.begin(); c.end(); // 2. 类型要求:嵌套类型必须存在 typename T::value_type; // 3. 复合要求:表达式有效,并检查 noexcept 或结果类型 { cc.size() } -> std::convertible_to<std::size_t>; // 4. 嵌套要求:放入额外布尔约束 requires std::same_as< decltype(*c.begin()), typename T::value_type& >; };

复合要求的完整形状是 { expression } noexcept -> type-constraint;,其中 noexcept 和返回类型约束都可以省略。

3.3 展开参数包

3.3.1 递归展开

C++11/14 常用递归处理参数包:

C++
1
2
3
4
5
6
7
void print() {} template <typename First, typename... Rest> void print(First&& first, Rest&&... rest) { std::cout << std::forward<First>(first) << ' '; print(std::forward<Rest>(rest)...); }

空参数的 print() 是终止函数。这种写法需要递归层和终止条件,错误信息也较长。

3.3.2 折叠表达式

C++17 引入了折叠表达式,可以把参数包按一个二元运算符合并。四种基本形式是:

C++
1
2
3
4
(pack op ...) (... op pack) (pack op ... op init) (init op ... op pack)

常见例子:

C++
1
2
3
4
5
6
7
8
9
10
11
12
template <typename... Ts> constexpr auto sum(Ts... xs) { return (xs + ...); } template <typename... Ts> constexpr bool all_true(Ts... xs) { return (... && xs); } static_assert(sum(1, 2, 3, 4) == 10); static_assert(all_true(true, true, true));

批量执行操作时常用逗号折叠:

C++
1
2
3
4
template <typename Vector, typename... Values> void push_all(Vector& v, Values&&... values) { (v.push_back(std::forward<Values>(values)), ...); }

括号是折叠表达式语法的一部分,不能省略。空包的一元折叠只有 &&|| 和逗号运算符有规定好的结果,求和之类的操作最好提供初始值:

C++
1
2
3
4
template <typename... Ts> constexpr auto safe_sum(Ts... xs) { return (0 + ... + xs); // 空包时结果为 0 }

3.4 展开 tuple

std::tuple 是一个长度固定,每个位置类型可以不同的容器,相当于一个匿名的 struct:

C++
1
std::tuple<int, double, std::string> t{42, 3.14, "hello"};

配套的三个工具:

  • std::get<I>(t) 取 tuple t 中的第 I 个元素(从 0 开始),其中 I 必须为编译器常量。

  • std::tuple_size_v<decltype(t)> 得到 tuple 的长度

  • std::tuple_element_t<I, decltype(t)> 得到 tuple 第 I 个位置的类型

由于 tuple 每个位置的类型不同,所以如果想要遍历 tuple 需要在编译期进行。

3.4.1 索引展开

展开 tuple 的一种方法是,把一串整数塞进类型里当模板参数:

C++
1
2
template <std::size_t... I> struct my_index_seq {}; // my_index_seq<0, 1, 2> 就是一个类型,它"携带"了 0,1,2

注意这个类型是空的(没有成员、没有数据),它唯一的价值就是 I... 这个参数包。一旦你能把 I... 拿到手,就可以用 ... 折叠一次性生成 get<0>get<1>get<2> 三份代码。

在标准库中已经为我们提供了生成整数序列的工具:

C++
1
2
3
4
5
6
7
8
9
// 自己指定序列元素类型 T std::integer_sequence<int, 1, 2, 3> // 携带 int 类型的 1,2,3 // T 固定为 size_t,专门用于下标 std::index_sequence<0, 1, 2> // 等价于 std::integer_sequence<std::size_t, 0, 1, 2> // make_index_sequence<N> 直接生成 0..N-1,不用手写 std::make_index_sequence<4> // 等价于 std::index_sequence<0, 1, 2, 3>

于是我们可以基于折叠表达式来展开 tuple:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template <typename Tuple, typename F, std::size_t... I> constexpr void tuple_for_each_impl( Tuple&& tuple, F&& f, std::index_sequence<I...>) { (std::forward<F>(f)( std::get<I>(std::forward<Tuple>(tuple)) ), ...); } template <typename Tuple, typename F> constexpr void tuple_for_each(Tuple&& tuple, F&& f) { using RawTuple = std::remove_reference_t<Tuple>; constexpr std::size_t count = std::tuple_size_v<RawTuple>; tuple_for_each_impl( std::forward<Tuple>(tuple), std::forward<F>(f), std::make_index_sequence<count>{} ); } auto data = std::tuple{42, 3.14, std::string{"hello"}}; tuple_for_each(data, [](const auto& item) { std::cout << item << '\n'; });

其中 tuple_for_each_impl 需要传入一个 std::index_sequence<I...> 作为参数,并且在调用 tuple_for_each_impl 的时候也需要传入一个 std::make_index_sequence<count>{},这是为了根据 I 来展开表达式,但实际上我们并不真正让 I 这个类型出现在最后生成的代码中,所以参数只是用来占位,参数名不写,实参也只是简单构造了一个空对象。

3.4.2 std::apply

C++17 提供了 std::apply,可以将 tuple 转成一个参数包:

C++
1
2
3
4
5
6
7
8
auto values = std::tuple{1, 2, 3, 4}; auto total = std::apply( [](auto... xs) { return (xs + ...); }, values );

这样的话我们可以更简洁地实现上面的 for_each_tuple

C++
1
2
3
4
5
6
7
8
9
template <typename Tuple, typename F> constexpr void for_each_tuple(Tuple&& tuple, F&& f) { std::apply( [&f](auto&&... items) { (f(std::forward<decltype(items)>(items)), ...); }, std::forward<Tuple>(tuple) ); }

4. 应用

编译期接口选择

C++
1
2
3
4
5
6
7
8
9
10
11
template <typename T> concept HasReserve = requires(T c, std::size_t n) { c.reserve(n); }; template <typename Container> void prepare(Container& c, std::size_t expected) { if constexpr (HasReserve<Container>) { c.reserve(expected); } }

std::vector 可以提前预留空间,std::list 没有 reserve,对应分支不会实例化。

通用序列化入口

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T> concept HasMemberSerialize = requires(const T& value) { value.serialize(); }; template <typename T> void save(const T& value) { if constexpr (std::integral<T>) { save_integer(value); } else if constexpr (HasMemberSerialize<T>) { value.serialize(); } else { static_assert(dependent_false_v<T>, "save: unsupported type"); } }

编译期配置

C++
1
2
3
4
5
6
7
8
9
10
11
template <bool Debug, bool Logging> struct Config { static void run() { if constexpr (Debug) { enable_debug_checks(); } if constexpr (Logging) { enable_logging(); } } };

不同配置会产生不同实例,未启用的分支可以在编译期移除。适合嵌入式、性能敏感库和固定部署配置。若配置要由用户运行时切换,就不应做成模板参数。

编译期查找表

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <array> #include <cstddef> template <std::size_t N> constexpr std::array<unsigned, N> make_powers_of_two() { std::array<unsigned, N> values{}; unsigned current = 1; for (auto& value : values) { value = current; current *= 2; } return values; } constexpr auto powers = make_powers_of_two<8>(); static_assert(powers[7] == 128);

适合固定查表、字符分类、协议常量和小型数学表。大型表会增加编译时间和目标文件大小,未必比运行时生成更划算。

最后更新于:2026-08-30 09:58

Caiwen
本文作者
一只蒟蒻,爱好编程和算法