CppCon 2024 学习:Back To Basics Generic Programming
英文原文理解模板是 C++ 泛型编程的核心工具,用于让代码可以在多种类型上复用。模板的一般形式是:可以是:类 (class) / 结构体 (struct函数类型别名 (using变量概念 (concept是:或模板通常定义在头文件里,因为编译器需要在实例化模板时看到完整定义。示例代码T m0;U m1;public:pair() { }模板类允许同一份代码适用于不同类型TUT, UTU。pair<
1⃣ 泛型编程 (Generic Programming)
英文原文:
Generic Programming: Same code works on different, unrelated types
理解:
泛型编程是一种编程范式,它允许相同的代码可以应用于不同类型,甚至是没有任何继承关系的类型。
也就是说,我们不必为每个类型写重复的函数,只需要写一次“模板化”的代码,就可以处理多种类型。
示例代码分析:
T sum(C container) {
T result = 0;
for (T value : container) {
result += value;
}
return result;
}
C container表示容器类型,可以是std::vector<int>、std::list<double>等。T表示容器中元素的类型。- 通过循环
for (T value : container),对每个元素进行加法操作result += value。 - 最终返回
result。
数学化理解:
如果我们有一个容器 C=x1,x2,...,xnC = {x_1, x_2, ..., x_n}C=x1,x2,...,xn,元素类型为 TTT,那么函数sum的作用就是计算:
sum(C)=∑i=1nxi \text{sum}(C) = \sum_{i=1}^{n} x_i sum(C)=i=1∑nxi
这和数学中的求和公式完全一致。
2⃣ 静态多态 (Static Polymorphism)
英文原文:
Static polymorphism
理解:
静态多态指的是在编译期决定函数或操作的具体实现,与动态多态(运行时多态,通过虚函数实现)相对。
在 C++ 中,模板就是实现静态多态的典型方法:编译器在实例化模板时,会根据实际类型生成对应的代码。
和泛型编程的关系:
- 泛型编程依赖模板实现“类型无关”的算法。
- 静态多态保证模板函数在不同类型上调用时,会在编译期生成高效代码,无需运行时开销。
数学化理解:
假设有函数模板 f(T)f(T)f(T),不同类型 TTT 会生成不同的函数实现 fint(int)f_{\text{int}}(int)fint(int)、fdouble(double)f_{\text{double}}(double)fdouble(double)。
3⃣ C++ 模板 (C++ Templates)
- 模板是 C++ 泛型编程的核心工具。
- 它允许同一份代码应用于不同类型。
- 结合静态多态,编译器在实例化模板时,会生成具体类型的代码,不产生额外的运行时开销。
总结关系:
| 概念 | 理解 | 数学化/公式表示 |
|---|---|---|
| 泛型编程 | 相同代码作用于不同类型 | ∑i=1nxi\sum_{i=1}^{n} x_i∑i=1nxi,不依赖具体类型 TTT |
| 静态多态 | 编译期确定函数实现 | f(T)→fint,fdoublef(T) \to f_{\text{int}}, f_{\text{double}}f(T)→fint,fdouble |
| 模板 | 实现泛型和静态多态的工具 | template<typename T> |
1⃣ 定义模板 (Define a Template)
英文原文:
template <template-parameters> declaration;
declaration can be: class / struct / function / type alias / variable / concept
template-parameter is: class | typename identifier [= default-value]
Template definition should be in a header file
理解:
- 模板是 C++ 泛型编程的核心工具,用于让代码可以在多种类型上复用。
- 模板的一般形式是:
template <template-parameters> declaration;
- declaration 可以是:
- 类 (
class) / 结构体 (struct) - 函数
- 类型别名 (
using) - 变量
- 概念 (
concept)
- 类 (
- template-parameter 是:
class identifier或typename identifier- 可以带默认值:
typename T = int
- 模板通常定义在头文件里,因为编译器需要在实例化模板时看到完整定义。
2⃣ 类模板定义 (Class Template Definition)
示例代码:
template <class T, class U>
class pair {
T m0;
U m1;
public:
pair() { }
pair(T v0, U v1) : m0(v0), m1(v1) { }
T first() const { return m0; }
U second() const { return m1; }
};
理解:
template <class T, class U>- 表示这是一个类模板,有两个类型参数
T和U。 - 可以用不同类型实例化这个模板,例如:
pair<int, double> p1; pair<std::string, int> p2; - 这里
class T和typename T是等价的。
- 表示这是一个类模板,有两个类型参数
- 成员变量:
T m0;和U m1;保存两个不同类型的数据。
- 构造函数:
- 默认构造函数
pair() { }。 - 带参构造函数
pair(T v0, U v1),初始化成员变量。
- 默认构造函数
- 成员函数:
T first() const返回第一个元素。U second() const返回第二个元素。
数学化理解
可以把 pair<T,U> 类看作一个有序二元组 (x0,x1)(x_0, x_1)(x0,x1),其中:
x0∈T,x1∈U x_0 \in T, \quad x_1 \in U x0∈T,x1∈U
- 构造函数
pair(T v0, U v1)对应数学上的赋值操作:
(x0,x1)←(v0,v1) (x_0, x_1) \gets (v_0, v_1) (x0,x1)←(v0,v1) first()返回 x0x_0x0,second()返回 x1x_1x1:
first()=x0,second()=x1 \text{first}() = x_0, \quad \text{second}() = x_1 first()=x0,second()=x1
总结
- 模板类允许同一份代码适用于不同类型 T,UT, UT,U。
pair<T,U>对应数学上的有序二元组 (T,U)(T,U)(T,U)。- 成员函数对应数学上的取值操作。
- 静态多态保证编译器在实例化不同类型时生成高效代码。
1⃣ 函数模板定义 (Function Template Definition)
示例代码:
template <class T>
void swap_pointed_to(T* a, T* b) {
T temp = *a;
*a = *b;
*b = temp;
}
理解
template <class T>- 这是函数模板的定义,
T是类型参数。 - 意味着我们可以用不同类型实例化这个函数,比如
int、double、std::string等。
- 这是函数模板的定义,
- 函数参数:
T* a, T* b表示指向类型T的指针。- 例如:
int* a, int* b或double* a, double* b。
- 函数功能:
- 交换指针指向的值:
T temp = *a; // 保存 a 指向的值 *a = *b; // 将 b 指向的值赋给 a *b = temp; // 将原来的 a 值赋给 b - 相当于数学上的交换操作。
- 交换指针指向的值:
数学化理解
假设指针 a 和 b 指向的值分别是 xxx 和 yyy:
x=∗a,y=∗b x = *a, \quad y = *b x=∗a,y=∗b
交换操作可以表示为:
(x,y)←(y,x) (x, y) \gets (y, x) (x,y)←(y,x)
- 用临时变量 ttt 表示:
t=x x=y y=t t = x \ x = y \ y = t t=x x=y y=t
这正好对应代码中的:
T temp = *a; // t = x
*a = *b; // x = y
*b = temp; // y = t
泛型优势
- 这段代码可以作用于任意类型
T,只要支持赋值操作(=)。 - 例如:
int a = 3, b = 5; swap_pointed_to(&a, &b); // a=5, b=3 double x = 1.2, y = 3.4; swap_pointed_to(&x, &y); // x=3.4, y=1.2 - 数学本质是同一个函数可以操作不同类型的有序对 (x,y)(x, y)(x,y),实现交换:
(x,y)↦(y,x) (x, y) \mapsto (y, x) (x,y)↦(y,x)
总结
| 概念 | 理解 | 数学化表示 |
|---|---|---|
| 函数模板 | 允许函数在不同类型上复用 | swap(x,y)=(y,x)swap(x,y) = (y,x)swap(x,y)=(y,x) |
| 类型参数 T | 可替换的任意类型 | T∈int, double, ...T \in {\text{int, double, ...}}T∈int, double, ... |
| 指针操作 | 交换指针指向的值 | (∗a,∗b)←(∗b,∗a)(*a, *b) \gets (*b, *a)(∗a,∗b)←(∗b,∗a) |
1⃣ 类型别名模板定义 (Type Alias Template Definition)
示例 1:
template <class T>
using ptr = T*;
理解:
template <class T>表示这是一个模板,T是类型参数。using ptr = T*;定义了一个类型别名模板ptr。- 作用:把
T*重命名为ptr<T>,方便代码书写。
使用示例:
ptr<int> p; // 等价于 int* p;
ptr<double> q; // 等价于 double* q;
数学化理解:
- 可以把指针
T*看作数学上某个类型 TTT 的“指向”操作:
ptr(T)=T∗ ptr(T) = T^* ptr(T)=T∗ - 这里 T∗T^*T∗ 表示指向类型 TTT 的指针。
示例 2:
template <class Iter1, class Iter2>
using result_type = typename std::common_type<
typename std::iterator_traits<Iter1>::value_type,
typename std::iterator_traits<Iter2>::value_type
>::type;
理解:
Iter1, Iter2是两个迭代器类型。std::iterator_traits<Iter>::value_type提取迭代器指向的元素类型。std::common_type<T1, T2>::type计算两个类型的公共类型(可以容纳两者的类型)。- 最终
result_type<Iter1, Iter2>就是两个迭代器指向元素的公共类型。
使用示例:
std::vector<int> v1;
std::vector<double> v2;
result_type<decltype(v1.begin()), decltype(v2.begin())> x;
// x 的类型是 double,因为 int 和 double 的公共类型是 double
数学化理解:
- 假设两个迭代器指向类型分别为 T1T_1T1 和 T2T_2T2:
T1=value_type of Iter1,T2=value_type of Iter2 T_1 = \text{value\_type of Iter1}, \quad T_2 = \text{value\_type of Iter2} T1=value_type of Iter1,T2=value_type of Iter2 - 公共类型 TcT_cTc 定义为能容纳 T1T_1T1 和 T2T_2T2 的最小类型:
Tc=common_type(T1,T2) T_c = \text{common\_type}(T_1, T_2) Tc=common_type(T1,T2) - 也就是模板别名
result_type<Iter1, Iter2>数学上可以表示为:
result_type(Iter1,Iter2)=common_type(T1,T2) result\_type(Iter1, Iter2) = \text{common\_type}(T_1, T_2) result_type(Iter1,Iter2)=common_type(T1,T2)
总结
| 模板类型 | 理解 | 数学化表示 |
|---|---|---|
ptr<T> |
类型别名,把 T* 重命名为 ptr<T> |
ptr(T)=T∗ptr(T) = T^*ptr(T)=T∗ |
result_type<Iter1,Iter2> |
两个迭代器指向元素的公共类型 | result_type(Iter1,Iter2)=common_type(T1,T2)result\_type(Iter1,Iter2) = \text{common\_type}(T_1,T_2)result_type(Iter1,Iter2)=common_type(T1,T2) |
| 特点: |
- 简化类型书写:减少长类型的重复书写。
- 模板化:可随类型参数自动生成对应类型。
- 数学意义清晰:把类型关系抽象为函数 ptr(T)ptr(T)ptr(T) 或 common_type(T1,T2)common\_type(T_1,T_2)common_type(T1,T2)。
1⃣ 变量模板定义 (Variable Template Definition)
示例代码:
template <class T>
constexpr bool is_big_and_trivial =
sizeof(T) > 16 &&
std::is_trivially_copyable<T>::value &&
std::is_trivially_destructible<T>::value;
理解
- 模板声明
template <class T>表示这是一个模板,T是类型参数。- 这个模板会根据类型
T生成一个布尔值常量。
constexpr bool is_big_and_trivial- 定义了一个编译期常量布尔值。
- 这个布尔值会在编译期计算。
- 判断条件
sizeof(T) > 16 &&
std::is_trivially_copyable<T>::value &&
std::is_trivially_destructible<T>::value
sizeof(T) > 16- 类型
T占用的字节数大于 16。 - 数学上:如果 s(T)s(T)s(T) 表示类型 TTT 的大小(字节),条件为:
s(T)>16 s(T) > 16 s(T)>16
- 类型
std::is_trivially_copyable<T>::value- 类型
T是平凡可拷贝的(trivially copyable)。 - 平凡拷贝意味着可以直接用
memcpy拷贝,不需要调用自定义拷贝构造函数。
- 类型
std::is_trivially_destructible<T>::value- 类型
T是平凡可析构的(trivially destructible)。 - 平凡析构意味着对象销毁不需要调用自定义析构函数。
- 类型
- 综合条件:
is_big_and_trivial(T)=(s(T)>16)∧trivially_copyable(T)∧trivially_destructible(T) \text{is\_big\_and\_trivial}(T) = (s(T) > 16) \wedge \text{trivially\_copyable}(T) \wedge \text{trivially\_destructible}(T) is_big_and_trivial(T)=(s(T)>16)∧trivially_copyable(T)∧trivially_destructible(T)
使用示例
struct S1 { int a[5]; }; // sizeof(S1)=20, trivially copyable/destructible → true
struct S2 { std::string s; }; // sizeof(S2)>16,但不是 trivially copyable → false
static_assert(is_big_and_trivial<S1>, "S1 is big and trivial");
static_assert(!is_big_and_trivial<S2>, "S2 is not big and trivial");
- 编译期就能确定每个类型是否符合条件。
总结
=—
| 元素 | 理解 | 数学化表示 |
|---|---|---|
template<class T> |
模板类型参数 | TTT |
constexpr bool |
编译期布尔常量 | B(T)∈0,1B(T) \in {0,1}B(T)∈0,1 |
sizeof(T) > 16 |
类型大于16字节 | s(T)>16s(T) > 16s(T)>16 |
trivially_copyable |
平凡可拷贝 | trivially_copyable(T)\text{trivially\_copyable}(T)trivially_copyable(T) |
trivially_destructible |
平凡可析构 | trivially_destructible(T)\text{trivially\_destructible}(T)trivially_destructible(T) |
| 综合 | 判断类型是否大且平凡 | KaTeX parse error: Expected 'EOF', got '_' at position 9: \text{is_̲big_and_trivial… |
| 特点: |
- 编译期计算,无需运行时开销。
- 泛型化,适用于任意类型
T。 - 可以用于条件编译、类型优化等场景。
1⃣ 模板参数的三种类型
C++ 模板参数分为三类:
| 类型 | 语法 | 理解 | 数学化表示 | |
|---|---|---|---|---|
| 类型模板参数 (Type Template Parameter) | `class | typename identifier` | 用于指定类型,可以在模板中使用类型占位符 T |
T∈TypeT \in \text{Type}T∈Type |
| 非类型模板参数 (Non-type Template Parameter, NTTP) | `type | auto identifier` | 用于传递常量值(整数、指针、引用等)到模板 | v∈Valuev \in \text{Value}v∈Value |
| 模板模板参数 (Template Template Parameter) | `template class | typename identifier` | 用于传递模板本身作为参数 | Template(...)\text{Template}(...)Template(...) |
2⃣ 名称是可选的
- 类型模板参数、非类型模板参数、模板模板参数都可以不写标识符。
示例:
template <typename> class MyClass; // 类型模板参数没有名字
template <int> class Array; // 非类型模板参数没有名字
template <template<class> class> class Container; // 模板模板参数没有名字
数学理解:
- 没有名字时,表示“占位符类型或值”:
T is a type placeholder,N is a value placeholder,TT is a template placeholder T \text{ is a type placeholder}, \quad N \text{ is a value placeholder}, \quad TT \text{ is a template placeholder} T is a type placeholder,N is a value placeholder,TT is a template placeholder
3⃣ 默认值是可选的
- 模板参数可以有默认值:
template <typename T = int>
class MyClass {};
template <int N = 10>
class Array {};
template <template<class> class TT = std::vector>
class Container {};
- 数学上,默认值表示:
T=default type,N=default value,TT=default template T = \text{default type}, \quad N = \text{default value}, \quad TT = \text{default template} T=default type,N=default value,TT=default template
4⃣ 可变参数模板(Variadic Template Parameters)
- 模板参数可以是可变数量,用
...表示:
template <typename... Ts> class MyTuple {}; // 可变类型参数
template <int... Ns> class MyArray {}; // 可变非类型参数
template <template<class> class... TTs> class MyContainers {}; // 可变模板模板参数
- 数学化理解:
- 对可变类型参数:
Ts=(T1,T2,…,Tn),n≥0 Ts = (T_1, T_2, \dots, T_n), \quad n \ge 0 Ts=(T1,T2,…,Tn),n≥0 - 对可变非类型参数:
Ns=(N1,N2,…,Nm),m≥0 Ns = (N_1, N_2, \dots, N_m), \quad m \ge 0 Ns=(N1,N2,…,Nm),m≥0 - 对可变模板模板参数:
TTs=(Template1,Template2,…,Templatek) TTs = (\text{Template}_1, \text{Template}_2, \dots, \text{Template}_k) TTs=(Template1,Template2,…,Templatek)
- 对可变类型参数:
5⃣ 总结
- 模板参数类型:
- 类型模板参数:占位类型 TTT
- 非类型模板参数:占位常量 NNN
- 模板模板参数:占位模板 TTTTTT
- 特性:
- 名称可选
- 默认值可选
- 可以是可变参数模板
- 数学意义:
- 模板可以看作函数 F(T,N,TT)F(T, N, TT)F(T,N,TT) 或 F(T1,...,Tn,N1,...,Nm,TT1,...,TTk)F(T_1, ..., T_n, N_1, ..., N_m, TT_1, ..., TT_k)F(T1,...,Tn,N1,...,Nm,TT1,...,TTk),用于生成不同实例化。
#include <iostream>
#include <vector>
// ===============================
// 1⃣ 类型模板参数(Type Template Parameter)
// ===============================
template <typename T = int> // 默认类型为 int
class MyClass {
public:
T value;
MyClass(T v) : value(v) {}
void print() { std::cout << value << std::endl; }
};
// ===============================
// 2⃣ 非类型模板参数(Non-type Template Parameter, NTTP)
// ===============================
template <int N = 5> // 默认值为 5
class MyArray {
public:
int arr[N];
void print() {
for (int i = 0; i < N; ++i) std::cout << arr[i] << " ";
std::cout << std::endl;
}
};
// ===============================
// 3⃣ 模板模板参数(Template Template Parameter)
// ===============================
template <template <typename, typename...> class Container = std::vector, typename T = int>
class ContainerWrapper {
public:
Container<T> data;
void add(T value) { data.push_back(value); }
void print() {
for (auto& v : data) std::cout << v << " ";
std::cout << std::endl;
}
};
// ===============================
// 4⃣ 可变模板参数(Variadic Template Parameters)
// ===============================
template <typename... Ts>
class Tuple {
public:
void print() { std::cout << "Tuple with " << sizeof...(Ts) << " elements\n"; }
};
// ===============================
// 主函数
// ===============================
int main() {
// 类型模板参数
MyClass<> a(42); // 使用默认 int
MyClass<double> b(3.14); // 使用 double
a.print();
b.print();
// 非类型模板参数
MyArray<> arr1; // 默认大小 5
MyArray<3> arr2; // 大小为 3
for (int i = 0; i < 5; i++) arr1.arr[i] = i + 1;
for (int i = 0; i < 3; i++) arr2.arr[i] = i + 10;
arr1.print();
arr2.print();
// 模板模板参数
ContainerWrapper<> cw1; // 默认 std::vector<int>
ContainerWrapper<std::vector, double> cw2; // vector<double>
cw1.add(1);
cw1.add(2);
cw2.add(3.5);
cw2.add(4.5);
cw1.print();
cw2.print();
// 可变模板参数
Tuple<int, double, char> t1;
Tuple<> t2;
t1.print();
t2.print();
return 0;
}
1⃣ 示例代码
template <class T, std::size_t N>
class array {
T m_data[N];
public:
constexpr std::size_t size() const { return N; }
// 其他成员函数...
};
理解
- 模板参数
template <class T, std::size_t N>
T是类型模板参数,用于指定数组中元素的类型。std::size_t N是 非类型模板参数,表示数组大小,是一个编译期常量。- 非类型模板参数允许在模板中使用固定值,比如数组长度、整数常量、指针等。
- 成员变量
T m_data[N];
- 根据模板参数
N创建固定大小的数组m_data。 - 数组大小在编译期已确定。
- 成员函数
constexpr std::size_t size() const { return N; }
- 返回数组大小
N。 - 使用
constexpr,保证在编译期即可获取数组大小。
数学化理解
- 模板类
array<T,N>可以抽象为一个定长向量:
array(T,N)≈(x1,x2,…,xN),xi∈T \text{array}(T, N) \approx (x_1, x_2, \dots, x_N), \quad x_i \in T array(T,N)≈(x1,x2,…,xN),xi∈T - 成员函数
size()对应数学函数:
size(array(T,N))=N \text{size}(\text{array}(T,N)) = N size(array(T,N))=N - 非类型模板参数 NNN 的本质:
- 它是一个编译期常量,用于确定容器的维度:
N∈N+,m_data=x1,x2,…,xN N \in \mathbb{N}^+, \quad \text{m\_data} = {x_1, x_2, \dots, x_N} N∈N+,m_data=x1,x2,…,xN
使用示例
array<int, 5> a; // 创建长度为 5 的 int 数组
array<double, 3> b; // 创建长度为 3 的 double 数组
std::cout << a.size() << std::endl; // 输出 5
std::cout << b.size() << std::endl; // 输出 3
- 编译器在实例化模板时,会根据
N生成不同的类。 N必须在编译期确定,运行时变量不能作为非类型模板参数。
总结
| 元素 | 理解 | 数学化表示 |
|---|---|---|
T |
类型模板参数 | T∈TypeT \in \text{Type}T∈Type |
N |
非类型模板参数,数组长度 | N∈N+N \in \mathbb{N}^+N∈N+ |
m_data[N] |
固定长度数组 | (x1,x2,…,xN)(x_1, x_2, \dots, x_N)(x1,x2,…,xN) |
size() |
返回数组长度 | size(array(T,N))=N\text{size}(\text{array}(T,N)) = Nsize(array(T,N))=N |
| 特点: |
- 非类型模板参数是编译期常量,用于生成固定结构的数据。
- 类型安全,数组大小在编译期确定,无运行时开销。
- 可用于定长数组、静态缓冲区、编译期优化等场景。
1. 类模板成员函数 — 类内定义
原始代码:
template <class T>
struct A {
T f() const {
return T{};
}
template <class U>
T g(U u) const {
return static_cast<T>(u);
}
};
理解
- 类模板定义
template <class T> struct A { ... };
- 这里定义了一个类模板
A,模板参数是T。 - 也就是说
A可以根据不同类型T生成不同的类:A<int>生成的类里T=int。A<double>生成的类里T=double。
- 成员函数
f
T f() const { return T{}; }
- 函数
f返回类型是模板参数T。 T{}是 值初始化(value initialization),类似数学中表示的“零元”或“默认值”:
f():A<T>→T,f()=0T f(): A<T> \to T, \quad f() = 0_T f():A<T>→T,f()=0T
其中 0T0_T0T 表示类型 TTT 的默认值。const表示这个成员函数不会修改对象本身。
- 成员函数模板
g
template <class U>
T g(U u) const { return static_cast<T>(u); }
g是 函数模板,它有自己的模板参数U,与类模板的T是独立的。g的作用是把传入的类型U转换为类模板类型T:
g:U→T,g(u)=static_cast<T>(u) g: U \to T, \quad g(u) = \text{static\_cast<T>}(u) g:U→T,g(u)=static_cast<T>(u)- 例如:
A<int> a; double x = 3.14; int y = a.g(x); // y = 3
2. 类模板成员函数 — 类外定义
原始代码:
template <class T>
struct A {
T f() const;
template <class U> T g(U u) const;
};
template <class T>
T A<T>::f() const {
return T{};
}
template <class T> template <class U>
T A<T>::g(U u) const {
return static_cast<T>(u);
}
理解
- 声明和定义分离
- 类内只声明函数:
类外再给出具体实现。T f() const; template <class U> T g(U u) const; - 注意 函数模板的类外定义 有两层模板:
- 第一层是 类模板的 T:
template <class T> T A<T>::f() const { ... } - 第二层是 成员函数模板的 U:
template <class T> template <class U> T A<T>::g(U u) const { ... }
- 第一层是 类模板的 T:
- 语法规则
- 类外定义成员函数模板时,必须写两层
template<>。 - 函数名前必须写完整的作用域
A<T>::。 - 返回类型仍然依赖于类模板参数
T。
- 数学理解
- 类模板:
A<T> 是一个类型,依赖于 T A<T> \text{ 是一个类型,依赖于 } T A<T> 是一个类型,依赖于 T - 成员函数:
f:A<T>→T,g:A<T>×U→T f: A<T> \to T, \quad g: A<T> \times U \to T f:A<T>→T,g:A<T>×U→Tf无参数,只返回默认值T{}。g将任意类型U的输入转换成T。
3. 总结
| 特性 | 类内定义 | 类外定义 |
|---|---|---|
| 类模板成员函数 | 可以直接在类里写实现 | 类内只声明,类外再实现 |
| 成员函数模板 | 同样可以类内写,也可类外写 | 类外写时必须用双层模板 template<class T> template<class U> |
| 使用 | A<int> a; a.f(); a.g(3.14); |
同上 |
| 核心要点: |
- 类模板参数
T决定类的总体类型。 - 成员函数模板参数
U可以独立于T,增加函数灵活性。 - 类外定义模板函数 要注意两层模板语法。
1. 模板的使用(Using a template)
模板在使用时的基本语法:
template-name < template-arguments >
即模板名后面跟尖括号参数。
在数学意义上可以理解为:
Template(args) \text{Template}( \text{args} ) Template(args)
或某种“类型函数”。
2. 模板实参的种类必须匹配模板形参的种类
模板参数分为三类,对应必须用正确的模板实参:
| 模板形参种类 | 示例 | 说明 |
|---|---|---|
| 类型参数(type) | template<class T> |
需要一个类型,例如 int、std::string |
| 编译期常量(non-type parameter) | template<int N> |
需要一个常量表达式,例如 42 |
| 模板模板参数(template template parameter) | template<template<class> class C> |
需要一个模板名,例如 std::vector |
| 数学上可表示为: |
- 类型参数:TTT
- 非类型参数:N∈ZconstexprN \in \mathbb{Z}_{\text{constexpr}}N∈Zconstexpr
- 模板模板参数:C:Type→TypeC: \text{Type} \to \text{Type}C:Type→Type
3. 示例:最普通的模板用法
示例 1:pair<int, std::string>
pair<int, std::string> id_name = { 123, "Joe" };
解释:
pair<T1, T2>是一个模板- 这里传入了两个类型实参
int与std::string:
pair⟨int,std::string⟩ \text{pair} \langle int, std::string \rangle pair⟨int,std::string⟩
4. 示例:传入类型实参
swap_pointed_to<int>(p, q);
说明:
swap_pointed_to是一个模板函数- 需要一个类型参数
T - 调用时显式写出
<int>:
swap_pointed_to⟨int⟩(p,q) swap\_pointed\_to \langle int \rangle (p, q) swap_pointed_to⟨int⟩(p,q)
5. 示例:模板参数可以是复杂类型表达式
result_type<decltype(it), int*> result = ...;
其中:
- 模板
result_type<T, U>接受两个类型参数 - 第一个实参是一个表达式的类型:
decltype(it) - 第二个实参是指针类型
int*
数学表示:
result_type⟨type(it),int∗⟩ result\_type \langle \text{type}(it), int^* \rangle result_type⟨type(it),int∗⟩
6. 示例:使用模板的布尔结果(traits)
static_assert(not is_big_and_trivial<int>);
解释:
is_big_and_trivial<T>是一个类型特征(type trait),返回bool- 实参是
int类型:
is_big_and_trivial⟨int⟩=false is\_big\_and\_trivial \langle int \rangle = \text{false} is_big_and_trivial⟨int⟩=false - 若结果为
false,static_assert通过。
7. 使用 C++17 类模板实参推导(CTAD)
最后一个示例:
pair id_name = { 123, "Joe"s };
swap_pointed_to(p, q);
result_type<decltype(it), int*> result = ...;
static_assert(not is_big_and_trivial<int>);
关键变化:pair 不再写 <int, std::string>!
C++17 允许从初始值推导模板参数,这叫 CTAD(Class Template Argument Deduction)。
这里:
123推出int"Joe"s推出std::string
因此自动推导:
pair⇒pair⟨int,;std::string⟩ pair \Rightarrow pair \langle int, ; std::string \rangle pair⇒pair⟨int,;std::string⟩
无需再写模板参数。
8. 总结与概念图
模板使用规则(关键点)
- 使用模板时必须写:
template-name < template-arguments >
- 模板实参必须匹配模板形参种类:
- 类型 ↔ 类型
- 非类型 ↔ 编译期常量
- 模板 ↔ 模板
1. Substitution vs Instantiation 的总览
Substitution
Substitute template arguments for template parameters
Results in class declaration or function declaration
Checks the correctness of the template arguments
Instantiation
Full definition of the class or function or type alias or variable
Happens after substitution, only when full definition is needed
Checks the correctness of the definition
2. Substitution(替换)
Substitution 做什么?
将模板实参代入模板形参。
数学意义上:
Substitution:T←A,U←B \text{Substitution}: \quad T \leftarrow A, U \leftarrow B Substitution:T←A,U←B
但替换后只生成 声明(declaration),不会生成完整定义。
Substitution 只检查:
- 模板实参是否合法
- 参数个数是否匹配
- 参数种类是否匹配(类型/非类型/模板模板参数)
- 函数签名是否可形成
不会检查类体内部
不会检查函数体内部
3. Instantiation(实例化)
Instantiation 做什么?
将替换结果展开为一个完整的定义(class/function/alias/variable)。
数学上就是:
Template(T)⇒Concrete class or function \text{Template}(T) \quad \Rightarrow \quad \text{Concrete class or function} Template(T)⇒Concrete class or function
只有当某些代码被“用到”时才会发生实例化。
Instantiation 会检查:
- 类中的所有成员定义
- 函数体的代码是否合法
- 是否有非法操作(如“返回数组类型”)
4. Substitution 的两个典型场景
场景 1:类模板 → 不需要完整类型
A<int, std::string>* ap;
只需要知道 A<int, std::string> 是一个“类型名”,
不会访问内容,因此不需要实例化。
所以:
- 仅做 Substitution
- 得到一个“未完成类型”(incomplete type)
- 类体内部不会被检查
例如错误不会报:
class A {
int x = undeclared; // 未定义标识符
};
场景 2:函数模板 → 重载决议阶段
重载决议时:
- 所有候选模板进行 Substitution
- 检查函数签名(参数类型、返回类型、noexcept)
- 不会检查函数体内容
- 未被选中的模板不会实例化
示例:
template<class T>
void f(T t) {
undeclared; // 错误!
}
f(0);
此时:
- Substitution 生成
void f(int) - 但不会实例化,因此
undeclared不报错
(直到真正用到函数体)
5. Instantiation(类模板)
定义:
将模板参数替换后对整个类进行完整展开。
例子:
template<class T, class U>
class pair {
T m0;
U m1;
public:
pair();
pair(T v0, U v1);
T first() const { return m0; }
U second() const { return m1; }
};
你写:
pair<int, data>
编译器生成:
class pairIi4dataE {
int m0;
data m1;
public:
pairIi4dataE();
pairIi4dataE(int v0, data v1);
int first() const;
data second() const;
};
这是编译器内部符号名称(mangling)。
6. Instantiation 示例:数组类型
你写:
pair<int[10], data>
编译器生成:
class pairIA10_i4dataE {
int m0[10];
data m1;
public:
pairIA10_i4dataE();
pairIA10_i4dataE(int v0[10], data v1);
int (first())[10] const;
data second() const;
};
注意这里:
- 返回类型是
int[10],这是非法的(不能返回数组) - 所以编译器报错:
error: function returning an array
这个错误只在 Instantiation 阶段 发生。
即:
- Substitution 时不会报错
- Instantiation 时才会检查函数体,发现问题
这正是模板两阶段语义的强大点。
7. Instantiation(函数模板)
例:
template<class T>
void swap_pointed_to(T* a, T* b) {
T temp = *a;
*a = *b;
*b = temp;
}
你调用:
swap_pointed_to<double>(p, q);
编译器生成:
void swap_pointed_toIdEvPT_S1_(double* a, double* b) {
double temp = *a;
*a = *b;
*b = temp;
}
这是函数模板的完整实例化。
8. 总结(极其重要)
| 阶段 | 作用 | 何时发生 | 检查内容 |
|---|---|---|---|
| Substitution(替换) | 把参数代入模板 | 每次出现模板名时 | 只检查参数合法性、声明是否合法 |
| Instantiation(实例化) | 完整生成类 / 函数定义 | 需要用到完整定义时 | 检查所有成员、函数体、语义错误 |
| 数学化表达: | |||
| $$ | |||
| \text{Substitution}: \quad \text{Template}[T \to A] | |||
| $$ | |||
| $$ | |||
| \text{Instantiation}: \quad \text{Expand}(\text{Template}[T \to A]) | |||
| $$ |
1. SFINAE 是什么?
SFINAE 的全称是:
Substitution Failure Is Not An Error \text{Substitution Failure Is Not An Error} Substitution Failure Is Not An Error
:
当模板在 Substitution 阶段 出现替换失败时,这不算编译错误,而是把该候选函数丢弃。
2. SFINAE 发生在 Substitution 阶段,而不是 Instantiation 阶段
也就是说:
- 模板实参代入形参(Substitution)
- 如果生成的声明有问题
→ 不报错
→ 该模板被从候选集中移除
数学描述:
令 F(T)F(T)F(T) 是一个函数模板。
在进行重载决议时:
F(T)[,T←A,] F(T)[,T \leftarrow A,] F(T)[,T←A,]
如果替换后得到的声明无效,则:
discard(F(A)) \text{discard}(F(A)) discard(F(A))
而不是:
error(F(A)) \text{error}(F(A)) error(F(A))
3. 为什么有 SFINAE?
因为:
- 模板需要“试探性匹配”
- 模板重载、模板偏特化都需要根据参数类型来挑选最合适的版本
- 如果每次匹配失败都报错,模板根本没法用
SFINAE 是模板元编程能够存在的根基。
4. SFINAE 的关键特性
替换错误不会导致编译错误
只会被从候选函数列表中“丢弃”。
SFINAE 只发生在 Substitution 阶段
Instantiation 阶段出现的错误算真正的编译错误。
用途
- 函数模板重载
- 类模板部分特化(partial specialization)
- 检查某个类型是否有某个成员(has_member 检测)
- 各种 type traits 的实现基础
5. 最典型例子:函数模板重载选择
例子:
template<class T>
auto f(T t) -> decltype(t.x, void()) { }
template<class T>
void f(...) { }
解释:
- 第一个模板要求 TTT 必须可以访问成员 xxx
- 若替换失败(类型没有
.x),SFINAE 会:
discard(f(T)) \text{discard}(f(T)) discard(f(T)) - 第二个模板作为兜底方案(fallback)
因此:
struct A { int x; };
struct B { };
f(A{}); // 选第一个
f(B{}); // 第一个替换失败 → 选第二个
6. 声明中允许 SFINAE 的位置
在函数模板声明中,只有签名中的类型相关部分参与 SFINAE:
包括:
- 返回类型
- 参数类型
- 默认参数
- noexcept 条件
- requires 子句(C++20)
例如:
template<class T>
auto f(T t) -> typename T::value_type; // OK: 替换失败触发 SFINAE
不包括:
- 函数体(不检查)
- 类体(Substitution 不检查类内容)
7. 一个更数学化的例子:检查是否可调用
template<class T>
auto g(T t) -> decltype(t()); // 需要 t 是可调用的
替换:
- 若 T=intT = intT=int:
int()int()int() 可以 → OK - 若 T=std::stringT = std::stringT=std::string:
std::string()std::string()std::string() 是可调用 → OK - 若 T=std::vector<int>T = std::vector<int>T=std::vector<int>:
v()v()v() 不存在 → 替换失败 → 丢弃
数学描述:
∃t():T⇒candidate \exists t(): T \Rightarrow \text{candidate} ∃t():T⇒candidate
∄t():T⇒discard \not\exists t(): T \Rightarrow \text{discard} ∃t():T⇒discard
8. 为什么它非常重要?
没有 SFINAE:
- 重载将无法选择最合适模板
- 偏特化也无法选择正确版本
- 许多 type traits 根本写不出来
- C++ 泛型编程会非常弱
模板的威力很大一部分来自于:
SFINAE+两阶段名称查找 \text{SFINAE} + \text{两阶段名称查找} SFINAE+两阶段名称查找
9. 总结(极简版)
| 概念 | 意义 |
|---|---|
| S | Substitution(替换阶段) |
| F | Failure(替换出问题) |
| I N A E | Is Not An Error(不算错误) |
| SFINAE 的核心思想: |
替换失败 → 丢弃该候选 → 不影响编译
这是模板重载与模板特化能够工作的基础。
Using Class Templates
Class Template Instantiation is a Type
类模板实例化本质上是一个类型
原文:
Class Template Instantiation is a Type
class-template-name < template-arguments >
Results in a regular type
Each instantiation is a distinct and unrelated type
详细理解:
- 当我们写
这是一个类模板,不是类型。template <class T> struct D { }; - 当我们实例化它,例如
这些 都是独立的具体类型(regular type)。D<int> D<double> - 关键点:
每一个实例化都形成一个独立的、不相关(unrelated)的类型。
也就是说,类型
D<int>与D<double>D<int> \quad \text{与} \quad D<double>D<int>与D<double>
之间没有任何隐式或自动的转换关系,哪怕int和double有关系,该关系不会传递到它们的模板实例化上。
Instantiations are Unrelated Types
下面逐个例子解释编译错误的原因。
示例 1:普通类型
struct A { };
struct B { };
A a;
B b = a; //
B* bp = &a; //
解读:
A和B是两个完全无关的结构体类型。- 因此赋值
B=AB = AB=A
是非法的。 - 指针
B∗=A∗B^* = A^*B∗=A∗
也非法。
编译器错误: error: conversion from 'A' to 'B' ...error: cannot convert 'A*' to 'B*' ...
这些只是提醒你这两种类型没有任何关系。
示例 2:类模板实例化同样不相关
template <class T> struct D { };
D<int> di;
D<double> dd = di; //
D<double>* ddp = &di; //
虽然
int→doubleint \to doubleint→double
是可以显式转换的,但模板实例化不会继承这种可转换性。
理解:
- D<int>D<int>D<int> 和 D<double>D<double>D<double> 是两个没有关系的类型。
- 不能把 D<int>D<int>D<int> 当成 D<double>D<double>D<double>。
- 也不能把 D<int>∗D<int>^*D<int>∗ 当成 D<double>∗D<double>^*D<double>∗。
所以错误如下是预期的: - “cannot convert ‘D’ to ‘D’”
- “cannot convert ‘D’ to ‘D’”
示例 3:T 和 const T 的实例化类型仍然无关
template <class T> struct D { };
D<int> di;
D<const int> dci = di; //
D<const int>* dcip = &di; //
虽然 int → const int 是一个合法的 顶层加 const(top-level const) 转换,但模板实例化仍然不共享这种关系。
换句话说:
D<int>≠D<constint>D<int> \neq D<const int>D<int>=D<constint>
它们之间没有继承、没有转换规则,也不互为基类。
总结(非常重要)
模板的每个不同模板实参都会生成一个互不相关的类型:
D<T1>, D<T2>, D<T3> 等都是完全独立的类型 D<T_1>,\ D<T_2>,\ D<T_3>\ \text{等都是完全独立的类型} D<T1>, D<T2>, D<T3> 等都是完全独立的类型
哪怕 T1T_1T1 和 T2T_2T2 本身存在隐式转换、继承关系,模板实例化得到的类型也不会共享这种关系。
因此以下转换都不允许:
- D<T1>→D<T2>D<T_1> \to D<T_2>D<T1>→D<T2>
- D<T1>∗→D<T2>∗D<T_1>^* \to D<T_2>^*D<T1>∗→D<T2>∗
- D<T>→D<constT>D<T> \to D<const T>D<T>→D<constT>
- D<T>∗→D<constT>∗D<T>^* \to D<const T>^*D<T>∗→D<constT>∗
一、核心结论(一句话)
模板实参的“种类”必须与模板形参的“种类”完全匹配——即类型参数、非类型(编译期常量)参数、模板模板参数三类不能混淆。
数学上可以写成:
kind(template-argument)=kind(template-parameter) \text{kind}(\text{template-argument}) = \text{kind}(\text{template-parameter}) kind(template-argument)=kind(template-parameter)
否则编译器报 type/value mismatch。
二、常见的三类模板参数(及对应实参)
- 类型参数(type parameter)
声明形式例如:
需要传入一个类型作为实参,例如template<class T> struct A { /* ... */ };$int$、$std::string$、A<int>等(都是“类型”)。
例:template<class T> struct A { }; A<int> ok; // 正确 A<1> wrong; // 错误:期待类型,得到了整数常量 - 非类型模板参数(non-type template parameter,NTTP)
声明形式例如:
需要传入一个编译期常量值,类型须与声明中指定的类型一致(这里是template<class T, std::size_t N> class std::array;std::size_t)。举例:
要点:位置对应——第 1 个模板形参是类型,就必须给类型;第 2 个形参是非类型,就必须给常量。std::array<int, 10> a; // 正确:10 是 size_t 的常量 std::array<int, double> b; // 错误:期待常量(size_t),得到了类型 double std::array<1, 2> c; // 错误:第一个参数期待类型,得到了常量 1注:C++ 标准在不同版本里对 NTTP 的可接受类型范围有演进(例如允许指针、引用、
nullptr、C++20/23 对字面类型的扩展等),但核心规则不变:实参的“类别/类型”必须匹配形参的要求。如果你需要我把标准各版本允许的 NTTP 种类列出来,我可以补充(会标注标准版本)。 - 模板模板参数(template-template parameter)
声明形式例如:
这要求传入的是模板名本身,形如template< template<class> class X > struct C { };template<class> struct Something,而不是一个类型或整数。
示例:
要点:模板模板参数不仅要求“模板”,还要求该模板的参数列表(形状)与期待的匹配(比如template<class T> struct A { }; template<int N> struct B { }; C<A> match; // 正确:A 的签名是 template<class> class C<B> no_match; // 错误:B 的签名是 template<int>,和要求不匹配 C<int> wrong_kind; // 错误:int 既不是模板也不是类型参数所需的东西template<class>与template<int>是不同“形状”)。
三、典型错误信息和它的含义
error: type/value mismatch at argument 2 in template parameter list for 'template<class _Tp, long unsigned int _Nm> struct std::array'
note: expected a constant of type 'long unsigned int', got 'double'
这句的含义逐字解释:
- “type/value mismatch” → 模板第 2 个实参的“种类/类型”与形参不匹配。
- “expected a constant of type ‘long unsigned int’” → 期望一个
std::size_t(编译期常量)。 - “got ‘double’” → 实参写成了
double(类型),而不是一个常量值,或写了一个与形参类型不匹配的常量。
另一个例子:
error: expected a class template, got 'template<int N> struct B'
note: expected a template of type 'template<class> class X', got 'template<int N> struct B'
含义:
C<...>要求传入template<class> class形式的模板(即参数类型为class),而提供的是template<int N> struct B(它的参数类型是int),因此形状不匹配。
四、为什么要这么严格?
- 模板参数的“类别”决定了如何在模板体内使用该参数(类型位置用于声明成员类型,非类型用于数组大小、常量表达式等,模板模板用于进一步参数化模板)。
- 若允许混用,编译器无法在替换/实例化阶段正确展开模板定义,会导致语义混乱或不确定性。
- 所以语言要求在语法层面就做严格匹配,越早报错越好(避免深入展开后出现难以定位的问题)。
五、简短数学化示例(帮助记忆)
std::array<T, N>:TTT 是 类型,NNN 是 常量。写作:
std::array⟨T,N⟩ \text{std::array}\langle T, N\rangle std::array⟨T,N⟩- 模板模板参数
C<X>要求:
X:Type→Type(即 template<class> 形式) X: \text{Type} \to \text{Type} \quad(\text{即 } template<class>\ \text{形式}) X:Type→Type(即 template<class> 形式)
1. 为什么“函数模板一般不需要显式模板参数”
在 C++ 中,模板参数推导(template argument deduction) 让编译器自动根据函数调用实参推导模板参数,因此:
- 函数模板通常像普通函数一样调用
- 模板参数由编译器推导,而不是手动写出
例如:
template<class T>
T add(T a, T b) { return a + b; }
int x = add(1, 2); // 编译器自动推导 T = int
double y = add(1.0, 2.5); // 编译器自动推导 T = double
只有当函数的 API 无法根据实参推导出模板参数 时,才需要显式写模板参数。
2. 示例:std::transform_reduce 的模板签名
讲者展示的模板签名如下:
template<
class ExecutionPolicy,
class ForwardIt,
class T,
class BinaryOp,
class UnaryOp >
T transform_reduce(
ExecutionPolicy&& policy,
ForwardIt first, ForwardIt last,
T init, BinaryOp reduce,
UnaryOp transform);
虽然模板参数很多,但我们通常 不需要手写这些模板参数——调用时可以直接写:
std::transform_reduce(pol, first, last, init, reduce, transform);
因为所有模板参数(ExecutionPolicy, ForwardIt 等)都能从函数实参自动推导出来。
3. 示例:路线搜索(route search)
展示了一个计算“最佳路线”的例子:
route_cost find_best_route(int const* distances, int N) {
return std::transform_reduce(
std::execution::par,
counting_iterator<long>(0L),
counting_iterator<long>(factorial(N)),
route_cost(),
route_cost::min,
[=](long i) {
int cost = 0;
// ... calculate cost ...
return route_cost(i, cost);
});
}
我们来逐段解释。
4. 数学含义:为什么是 0 到 factorial(N)?
假设有 NNN 个城市,它们的所有排列有:
N!=1×2×⋯×N N! = 1 \times 2 \times \cdots \times N N!=1×2×⋯×N
每个排列代表一条路线(遍历全部城市一次)。
因此:
- 第一个迭代器:
counting_iterator<long>(0L)
→ 表示编号为 000 的路线 - 最后一个迭代器:
counting_iterator<long>(factorial(N))
→ 表示编号为 N!N!N! 的路线
于是transform_reduce会遍历
i=0,1,2,3,…,N!−1 i = 0, 1, 2, 3, …, N!-1 i=0,1,2,3,…,N!−1
每个 iii 代表一种路线排列。
5. 逐参解释 transform_reduce
我们调用:
std::transform_reduce(
std::execution::par, // 并行执行策略
counting_iterator<long>(0L), // 迭代起点
counting_iterator<long>(factorial(N)), // 迭代终点(不包含)
route_cost(), // 初始值
route_cost::min, // reduce(二元操作,比较找到最小 cost)
[=](long i) { // transform(一元操作,将 i 转成 route_cost)
int cost = 0;
// ... calculate cost ...
return route_cost(i, cost);
}
);
作用:
- 遍历所有路由编号 iii(从 0 到 N!−1N!-1N!−1)
- 用 transform 对每个 iii 计算出对应路线的代价 cost
- 用 reduce 找出所有 route_cost 中最小的(最佳路线)
数学上等价于:
min0≤i<N!(route_cost(i)) \min_{0 \le i < N!} \bigl( \text{route\_cost}(i) \bigr) 0≤i<N!min(route_cost(i))
6. 为什么不需要写模板参数?
例如:
ExecutionPolicy→ 自动从std::execution::par推导ForwardIt→ 自动从counting_iterator<long>推导T→ 自动从route_cost()推导BinaryOp和UnaryOp→ 自动从route_cost::min和 lambda 推导
所以完整调用只需要:
std::transform_reduce(...);
完全不需要写:
std::transform_reduce<ExecutionPolicy, ForwardIt, ...>(...)
7. 要点总结()
- 函数模板一般不需要手写模板参数(除非无法推导)。
std::transform_reduce是“transform + reduce”的组合算法。- 在该示例中,它遍历所有 N!N!N! 种路线,用并行方式计算并找出最优路线。
- 模板参数全部通过函数参数自动推导,因此调用非常简洁。
#⃣ 1. 什么是 Constraints(约束)
Constraints = 对模板参数的要求
即:模板参数 必须满足某些条件,例如:
- “必须是整数类型”
- “必须可比较”
- “必须可迭代”
- “类型必须有某个成员函数”
这些约束用于: - 在模板实参替换(substitution)阶段进行检查
- 而不是实例化阶段
在 C++ 的模板系统里,这一行为属于 SFINAE(Substitution Failure Is Not An Error) 原则。
数学上,它类似:
如果 T 不满足条件,则模板被移除,不作为候选。 \text{如果 } T \text{ 不满足条件,则模板被移除,不作为候选。} 如果 T 不满足条件,则模板被移除,不作为候选。
#⃣ 2. 最原始的模板(没有约束)
template <class T>
int count_one_bits(T arg);
问题:T 可以是任何东西,包括 double, std::string, vector<int>。
但 count_one_bits 显然只应该接受 整数类型。
#⃣ 3. C++11:std::enable_if(SFINAE 时代的约束)
写法 1:返回值使用 enable_if
template <class T>
typename std::enable_if<
std::is_integral<T>::value, int
>::type
count_one_bits(T arg);
解释:
enable_if<条件, 类型>::type当且仅当条件为真时才存在- 若
std::is_integral<T>::value == false,则替换失败 → 模板被移除
数学上等价于:
T∈IntegralTypes T \in \text{IntegralTypes} T∈IntegralTypes
写法 2:将 enable_if 放到默认模板参数中
template <class T,
class = typename std::enable_if<
std::is_integral<T>::value
>::type>
int count_one_bits(T arg);
含义相同,只是语法更复杂。
#⃣ 4. C++20 Concepts & requires — 新时代的约束
C++20 引入 concepts,让“约束”变得直观。
写法 3:使用 requires + trait
template <class T>
requires std::is_integral<T>::value
int count_one_bits(T arg);
写法 4:使用标准 Concept std::integral
template <class T>
requires std::integral<T>
int count_one_bits(T arg);
数学上:
T 满足 std::integral T \text{ 满足 } std::integral T 满足 std::integral
#⃣ 5. 直接把 Concept 写到模板参数上(更简洁)
写法 5:
template <std::integral T>
int count_one_bits(T arg);
比前面的写法都直观。
就像数学声明变量那样:
T:Integral T : \text{Integral} T:Integral
#⃣ 6. 把 requires 写在函数体之后(另一语法)
写法 6(带后置 requires):
template <class T>
int count_one_bits(T arg)
requires std::is_integral<T>::value;
写法 7(使用 concept):
template <class T>
int count_one_bits(T arg)
requires std::integral<T>;
意义完全相同,只是语法变化。
#⃣ 7. 直接使用 constrained auto(最现代写法)
int count_one_bits(std::integral auto arg);
完全等价于:
template <std::integral T>
int count_one_bits(T arg);
这是 最简洁、最现代、最推荐的写法。
数学上等价于:
arg∈IntegralTypes \text{arg} \in \text{IntegralTypes} arg∈IntegralTypes
#⃣ 8. 一个有趣的例子:type_is_integral(^^T)
讲者用了一个玩笑式语法:
template <class T> requires (type_is_integral(^^T))
int count_one_bits(T arg);
这是故意展示“概念可以非常灵活”,^^T 看起来像某种反射(reflection)语法,是一种“未来可能的 C++ 语法想象”。
其实只是为了说明:
concepts 的约束表达式可以是任意可求值的 boolean 表达式
#⃣ 9. 总结:约束从 C++11 到 C++20 的进化
| 写法 | 时代 | 可读性 | 推荐度 |
|---|---|---|---|
| enable_if 返回值 | C++11 | 糟糕 | |
| enable_if 默认参数 | C++11 | 更糟糕 | |
| requires + trait | C++20 | 清晰 | |
| requires + concept | C++20 | 更清晰 | |
template <std::integral T> |
C++20 | 最简洁 | 最推荐 |
std::integral auto arg |
C++20 | 超现代 |
#⃣ 10. 一句话总结
Constraints 是对模板参数施加的条件,从 enable_if 到 concepts,语法不断变简洁,语义不断变清晰。
#⃣ 1. KISS 原则
KISS = Keep It Simple and Straightforward
保持简单直接,不要炫技
在写类模板时:
- 不要写过度复杂的模板元编程
- 不要让用户难以理解类型推导
- 不要设计过度抽象的 metafunction
- 要简单、直接
- 用户易于理解才是最重要的
讲者强调:
Class 模板不应该为了“聪明”而变复杂。
模板使用者的体验比模板作者的炫技更重要。
#⃣ 2. 文档化模板参数的“要求”(Document Requirements)
模板类通常有类型要求。例如:
- 模板参数必须可复制
- 类型必须可比较
- 类型必须满足某种接口
两种方式表达要求:
方式 1:在代码中记录(最佳方式)
使用 constraints(约束):
template <std::copyable T>
class MyContainer { ... };
比文档更强,因为:
- 编译器可以检查
- 错误信息清晰
- 用户无须看文档也知道限制
数学表示:
T∈Copyable T \in \text{Copyable} T∈Copyable
方式 2:写文档说明(如果无法约束)
例如:
/// T must have a member size()
template <class T>
class Something { ... };
如果约束无法写进代码,你必须写文档,否则用户会踩坑。
“成员函数”也可以有额外要求
示例:
template <class T>
class X {
public:
void draw() requires Drawable<T>;
};
这让:
- 类模板本身不要求
T可以 draw - 但调用
x.draw()时,编译器会检查T是否满足 Drawable
这种延迟约束非常强大,类似 Rust 的:
impl<T> X<T>
where T: Drawable
{
fn draw(&self) {...}
}
#⃣ 3. Specialization(特化)
特化用于:
当类模板的 特定实例 需要与普通实例有不同表现时。
模板特化包括:
- 完全特化(full specialization)
- 类似但不在此讲的部分特化(partial specialization)
警告:
“特化可以有完全不同的接口,但这是一个坏主意。”
原因:
- 破坏泛型一致性
- 用户以为是同一个类型,结果接口完全不一样
- 不利于维护与扩展
例如:
template <>
struct Foo<void> {
// 完全不同的函数
};
这是有风险的设计。
#⃣ 4. Specialization 示例:safe_sizeof
先看普通版本:
template <class T>
struct safe_sizeof {
static constexpr std::size_t value = sizeof(T);
};
等价于数学定义:
safe_sizeof(T)=sizeof(T) \text{safe\_sizeof}(T) = \text{sizeof}(T) safe_sizeof(T)=sizeof(T)
对 void 的特化
因为:
sizeof(void)在 C++ 中是非法的- 但我们可以通过特化,让它成为可用值
所以:
template <>
struct safe_sizeof<void> {
static constexpr std::size_t value = 0;
};
数学意义上:
safe_sizeof(void)=0 \text{safe\_sizeof}(\text{void}) = 0 safe_sizeof(void)=0
#⃣ 5. 为什么这个例子能展示 Specialization?
- 普通模板对任意 T 都给出
sizeof(T) - 但对
void不能sizeof(void) - 所以必须为
void提供一个特别版本 - 这个“特别版本”称为 完全特化
使用:
safe_sizeof<int>::value // = 4 (示例)
safe_sizeof<double>::value // = 8 (示例)
safe_sizeof<void>::value // = 0
#⃣ 6. 简洁总结(讲者核心观点)
编写类模板要 KISS
保持简单,非常简单。
把模板参数的要求写在代码里(concepts)
用户不用看文档也知道限制。
若必须使用特化,要谨慎
特化能完全改变行为,但不要改变接口,会造成 API 一致性问题。
safe_sizeof<void> 是一个典型、合理的特化例子
因为它改善了类型安全性,让 void 在某些场景下更易处理。
#⃣ 1. 什么是 Partial Specialization(部分特化)
完全特化(full specialization):
- 所有模板参数都被明确到具体类型
- 例如
safe_sizeof<void>
部分特化(partial specialization): - 只有部分模板参数被具体化
- 或者 模板参数符合某种“模式”
例如: - 任意类型的数组:
T[] - 任意函数类型:
R(Args...)
数学类比:
完全特化是:
f(void)=0 f(\text{void}) = 0 f(void)=0
部分特化是:
f(T[])=0,∀T f(T[]) = 0,\quad \forall T f(T[])=0,∀T
#⃣ 2. safe_sizeof 的完整例子解析
这个例子展示了三种特化:
- 普通模板
- 完全特化
safe_sizeof<void> - 数组模式的部分特化
safe_sizeof<T[]> - 函数类型的部分特化
safe_sizeof<R(Args...)>
## 2.1 基础模板(primary template)
template<class T>
struct safe_sizeof{
static constexpr std::size_t value = sizeof(T);
};
数学意义:
safe_sizeof(T)=sizeof(T) \text{safe\_sizeof}(T) = \text{sizeof}(T) safe_sizeof(T)=sizeof(T)
适用于绝大多数类型。
## 2.2 完全特化:处理 void
template <>
struct safe_sizeof<void> {
static constexpr std::size_t value = 0;
};
因为:
sizeof(void) 在 C++ 中非法 \text{sizeof}(\text{void})\ \text{在 C++ 中非法} sizeof(void) 在 C++ 中非法
所以我们人为定义:
safe_sizeof(void)=0 \text{safe\_sizeof}(\text{void}) = 0 safe_sizeof(void)=0
## 2.3 部分特化:数组类型
template <class T>
struct safe_sizeof<T[]> {
static constexpr std::size_t value = 0;
};
匹配所有“不定界数组”:
int[]double[]MyType[]
并定义:
safe_sizeof(T[])=0 \text{safe\_sizeof}(T[]) = 0 safe_sizeof(T[])=0
为什么?
因为不定长数组T[]在 C++ 中无法sizeof(T[])。
## 2.4 部分特化:函数类型
template <class R, class... Args>
struct safe_sizeof<R(Args...)> {
static constexpr std::size_t value = 0;
};
匹配所有函数类型:
int()double(float, int)void(const char*)
数学定义:
safe_sizeof(R(Args...))=0 \text{safe\_sizeof}(R(Args...)) = 0 safe_sizeof(R(Args...))=0
原因:函数类型也不能sizeof。
#⃣ 3. 用 constraints 替代部分特化(新的写法)
讲者展示了使用 requires 的现代写法:
## 3.1 初始版本:全部默认值为 0
template <class T>
struct safe_sizeof {
static constexpr std::size_t value = 0;
};
这使得:
safe_sizeof(T)=0 \text{safe\_sizeof}(T) = 0 safe_sizeof(T)=0
## 3.2 第二版:给能 sizeof 的 T 才计算 sizeof
template <class T> requires (sizeof(T) > 0)
struct safe_sizeof<T> {
static constexpr std::size_t value = sizeof(T);
};
数学表示:
if sizeof(T)>0,then safe_sizeof(T)=sizeof(T)else 0 \text{if } sizeof(T) > 0,\\ \text{then } \text{safe\_sizeof}(T) = sizeof(T) \\ \text{else } 0 if sizeof(T)>0,then safe_sizeof(T)=sizeof(T)else 0
这个写法本质是使用 约束匹配 来替代部分特化。
#⃣ 4. 第三版:变量模板形式(最简)
template <class T>
constexpr std::size_t safe_sizeof = 0;
template <class T> requires (sizeof(T) > 0)
constexpr std::size_t safe_sizeof<T> = sizeof(T);
变量模板也支持部分特化(type alias 和 concept 不行)。
此写法更优雅、更现代,减少样板代码。
#⃣ 5. 哪些“模板实体”允许部分特化?
| 模板种类 | 允许特化? | 说明 |
|---|---|---|
| 类模板 class template | Yes | 最常用 |
| 变量模板 variable template | Yes | C++14 引入 |
| 类型别名模板 type alias template | No | 因为别名不是“实体”,无法重定义 |
| concept 概念模板 | No | 只能写多个 concept,不支持特化 |
| 函数模板 function template | ?受限 | 不能部分特化,但可以用重载模拟 |
| 讲者提到: |
Function template cannot be partially specialized
See next section (使用重载来替代)
#⃣ 6. 小结(讲者核心观点)
部分特化 = 匹配模板参数的某个模式
例如 T[], R(Args…)。
safe_sizeof 是展示部分特化的绝佳例子
因为:
- void 不能 sizeof → 完全特化
- T[] 不能 sizeof → 部分特化
- 函数类型不能 sizeof → 部分特化
Modern C++ 更推荐使用 constraints 替代部分特化
例如:
template <class T>
requires (sizeof(T) > 0)
struct safe_sizeof<T> { ... };
变量模板也支持部分特化,是最优雅的写法
#⃣ 1. 背景:为什么需要 Type Alias Specialization Workaround
在 C++ 中:
- 类模板(class template) 支持部分特化
- 类型别名模板(type alias template) 不支持特化
举例:
template <class T>
using remove_pointer_t = ???; // 想特化 T* ?不行!
直接对 remove_pointer_t<T*> 做部分特化是非法的。
解决方案:先写一个 可特化的类模板,再用类型别名模板引用它。
#⃣ 2. 基本模式
template <class T>
struct remove_pointer {
using type = T; // 默认情况:不是指针,类型不变
};
template <class T>
struct remove_pointer<T*> {
using type = T; // 特化 T*,移除指针
};
// 类型别名模板,方便使用
template <class T>
using remove_pointer_t = typename remove_pointer<T>::type;
#⃣ 3. 数学解析
定义一个映射函数:
remove_pointer(T)={Tif T 不是指针Uif T=U∗(指针类型) \text{remove\_pointer}(T) = \begin{cases} T & \text{if } T \text{ 不是指针} \\ U & \text{if } T = U^* \text{(指针类型)} \end{cases} remove_pointer(T)={TUif T 不是指针if T=U∗(指针类型)
然后类型别名模板相当于取 .type:
remove_pointer_t(T)=remove_pointer(T).type \text{remove\_pointer\_t}(T) = \text{remove\_pointer}(T).\text{type} remove_pointer_t(T)=remove_pointer(T).type
例子:
remove_pointer_t<int> // = int
remove_pointer_t<int*> // = int
remove_pointer_t<int**> // = int*
#⃣ 4. 工作原理
- 类模板 remove_pointer 是可特化的
- 默认模板:
T非指针 → type = T - 部分特化模板:
T*→ type = T
- 默认模板:
- 类型别名模板 remove_pointer_t 是对
remove_pointer<T>::type的引用- 相当于封装了一层方便的“快捷访问”
- 为什么要这么做?
因为类型别名模板不能直接特化,但类模板可以。
所以这是 Type Alias Specialization Workaround 的标准方法。
#⃣ 5. 现代 C++ 使用场景
- 去掉指针类型:
T*→T - 去掉 const/volatile 修饰:
std::remove_cv<T>::type - 去掉引用类型:
std::remove_reference<T>::type
这种模式是 C++ 元编程库的基础。
#⃣ 6. 总结
- 问题:type alias template 不允许特化
- 解决方案:
- 写一个可特化的类模板
remove_pointer - 写一个类型别名模板
remove_pointer_t引用类模板的.type
- 写一个可特化的类模板
- 数学模型:
remove_pointer_t(T)={TT不是指针UT=U∗ \text{remove\_pointer\_t}(T) = \begin{cases} T & T \text{不是指针} \\ U & T = U^* \end{cases} remove_pointer_t(T)={TUT不是指针T=U∗ - 优点:语法简洁、可扩展、符合标准库惯例
#⃣ 1. 背景:为什么模板需要 typename
在 C++ 模板中,如果你有一个依赖模板参数的类型:
T::value_type
编译器在解析模板定义时无法判断:
T::value_type是类型(type)- 还是静态成员或其它值(non-type)
这是因为模板参数
T尚未实例化,编译器只能猜测。
#⃣ 2. 例子:错误用法
template <class T>
void do_something(T t) {
T::value_type* p(T::addr(T::object));
}
编译器报错:
dependent-name 'T::value_type' is parsed as a non type,
but instantiation yields a type
- 依赖名(dependent name)
T::value_type被解析为非类型 - 实际上,在实例化时它是一个类型
- 解决方法:告诉编译器它是一个类型 → 使用
typename
#⃣ 3. 正确写法
template <class T>
void do_something(T t) {
typename T::value_type* p(T::addr(T::object));
}
解释:
typename告诉编译器:T::value_type是一个类型- 这样编译器就可以正确解析指针声明
*p - 依赖类型名 (dependent type name) 解析问题就解决了
数学表达:
p:pointer to (T.value_type) p : \text{pointer to } (T.\text{value\_type}) p:pointer to (T.value_type)
#⃣ 4. 为什么编译器需要帮助
考虑下面的语法歧义:
A * B(C(D));
- 编译器可能理解为 表达式语句:
A乘以B(C(D)) - 或理解为 变量定义:
B是指向A的指针,初值为C(D) - 或理解为 函数声明:
B是一个函数,返回A*,参数类型是函数C(D)的指针
依赖模板参数时,编译器无法判断
T::value_type是类型还是非类型,必须显式告诉它
#⃣ 5. 使用 typename 的规则
- 必须出现在依赖类型名前(dependent type name)
- 例如
T::value_type、T::iterator
- 例如
- 不能用于非依赖类型
- 例如
int::value_type没意义
- 例如
- 不能用于模板参数本身
- 例如
template <typename T>中的T已经是类型,不需要typename
数学上可以表示为:
typename T::X ⟹ T::X 是类型 \text{typename } T::X \implies T::X \text{ 是类型} typename T::X⟹T::X 是类型
- 例如
#⃣ 6. 变量声明示例
typename T::value_type* p(T::addr(T::object));
解释:
typename T::value_type→ 类型*p→ 指针T::addr(T::object)→ 初始化表达式
逻辑上:
p:pointer to (T.value_type) p : \text{pointer to } (T.\text{value\_type}) p:pointer to (T.value_type)
#⃣ 7. 总结
- 依赖类型名必须加
typename - 依赖非类型不加
typename - 解决了模板解析歧义问题
- 这是 C++ 模板元编程中的常见陷阱
如果你愿意,我可以帮你画一张typename的语法歧义图,直观展示: - 表达式语句 vs 变量声明 vs 函数声明
- 以及
dependent type解析为什么必须加typename
#⃣ 1. Deducible Template Parameters(可推导模板参数)
1.1 原则
- 尽量让 所有模板参数都可以通过函数参数推导
- 除非必须手动指定
示例:
template <class Result, class Source>
Result my_fancy_cast(const Source& src) {
// ...
}
调用:
my_fancy_cast<Widget>(parts.getFidget());
Source可以通过函数参数自动推导Result必须手动指定
数学表示:
my_fancy_cast:(Source→Result) \text{my\_fancy\_cast}: (Source \to Result) my_fancy_cast:(Source→Result)
尽量让 SourceSourceSource 可以由
src推导,而 ResultResultResult 可选手动指定
1.2 限制
- 无法推导“父类型”或依赖类型
例如:
template <class T>
void f(typename T::type arg) { }
调用:
f(2); // 编译失败
原因:
T的类型不能通过参数arg推导- 依赖类型名
T::type是一个 dependent type - 解决方法:必须手动指定模板参数或使用
requires约束
数学上:
f:T::type→void,T无法由 T::type推导 f : T::type \to void, \quad T \text{无法由 } T::type \text{推导} f:T::type→void,T无法由 T::type推导
#⃣ 2. Overload sets(重载集合)
原则:
- 避免复杂的重载集合
- 尤其是和初始化列表、默认参数结合时容易产生歧义
示例:
explicit vector(size_type count);
vector(std::initializer_list<T> init);
区别:
std::vector<int> a(100); // 100 个元素,值默认 0
std::vector<int> b{100}; // 1 个元素,值为 100
如果有两个参数:
vector(size_type count, const T& value);
vector(std::initializer_list<T> init);
vector<int> a(100, 200);→ 100 个元素,每个值为 200vector<int> b{100, 200};→ 2 个元素,值为 100 和 200
数学上可以写为:
vector(n)=[0,0,…,0]n个元素 \text{vector}(n) = [0, 0, \dots, 0] \quad n\text{个元素} vector(n)=[0,0,…,0]n个元素
vectorx=[x] \text{vector}{x} = [x] vectorx=[x]
#⃣ 3. Be Choosy(谨慎设计函数模板)
- 避免编写接受“任意类型”的函数模板
- 尤其是常用名字,如
operator==、count_* - 否则容易破坏类型安全或引发二义性
示例:
template <class T, class U>
bool operator==(const T& t, const U& u) { ... } // 不推荐
数学上:
∀T,U:operator==(T,U) \forall T, U: \text{operator==}(T, U) ∀T,U:operator==(T,U)
对所有类型都成立,风险太高
#⃣ 4. Function Template Specialization(函数模板特化)
规则:
| 内容 | 是否允许 |
|---|---|
| 非成员函数模板全特化 | 允许 |
| 非成员函数模板部分特化 | ✖ 不允许 |
| 成员函数模板任何特化 | ✖ 不允许 |
C++ 标准规定:函数模板不能部分特化
#⃣ 5. 替代方案:使用重载而非特化
5.1 全特化(full specialization)
- 不要特化函数模板
- 使用 非模板重载
示例:
template <class T> requires some_condition<T>
void do_something(const T& arg) {
// 默认模板实现
}
void do_something(const char* s) {
// 针对 char* 的特殊实现
}
数学表示:
do_something:T→void,T=char* 时有特殊实现 \text{do\_something} : T \to \text{void}, \quad T = \text{char*} \text{ 时有特殊实现} do_something:T→void,T=char* 时有特殊实现
5.2 部分特化(partial specialization)
- 用 模板重载 模拟
- 利用
requires或模板模式匹配
示例:
template <class C> requires is_container<C>
void process_collection(const C& c) {
// 对所有容器通用处理
}
template <class T>
void process_collection(const std::list<T>& c) {
// 对 std::list 的特殊处理
}
- 第一个模板匹配任意容器
- 第二个模板匹配
std::list类型 - 相当于 partial specialization 的效果
数学上:
process_collection(C)={special handlingC=std::list<T>generic handlingC 是其它容器 \text{process\_collection}(C) = \begin{cases} \text{special handling} & C = \text{std::list<T>} \\ \text{generic handling} & C \text{ 是其它容器} \end{cases} process_collection(C)={special handlinggeneric handlingC=std::list<T>C 是其它容器
#⃣ 6. 总结原则
- 尽量让 模板参数可推导
- 避免复杂 重载集合
- 谨慎设计 通用函数模板,避免名字过于常用
- 函数模板不要特化,用重载替代
- 部分特化效果用 模板重载 + requires 约束 实现
更多推荐



所有评论(0)