C++ 从入门到精通之七

大纲

C++ 模板与泛型

  • 泛型编程

    • 泛型编程是一种以独立于任何特定类型的方式编写代码的编程方式。在使用泛型编程时,需要提供具体程序实例所需的类型、习惯或值。
    • 在 C++ 中,泛型编程(基于模板)可以用于实现静态多态(编译期多态),即在编译阶段根据具体类型生成对应的代码;与之对应,虚函数机制用于实现动态多态(运行时多态),通过基类指针或引用在运行时决定调用哪个派生类的实现。
  • 模板的作用

    • 模板是泛型编程的基础。它是一种用于创建类或函数的蓝图或公式,开发者只需为这些蓝图提供足够的信息,模板便能真正执行并生成具体的代码。
  • 模板的类型支持

    • 模板支持将类型作为参数进行程序设计,从而直接支持泛型编程过程。也就是说,C++ 的模板机制允许在定义类或函数时,将类型作为参数传递,实现代码的通用性。
  • 模板的两种分类

    • 函数模板
      • 函数模板是用于生成特定类型函数的蓝图。它允许在定义函数时,将参数或返回值的类型作为模板参数,从而实现通用操作(如比较、交换等)。
      • 比如,基于函数模板编写一个比较两个值大小的函数,可以适用于 intdoublestring 等多种类型。
    • 类模板
      • 类模板是用于生成特定类型类的蓝图。它允许在定义类时,将成员变量或成员函数的类型作为模板参数,从而实现通用数据结构或容器(如栈、队列、数组等)。
      • 比如,基于类模板实现一个可存储任意类型元素的栈类,使用时再指定具体类型(如 Stack<int>Stack<string>)。

模板基础

函数模板

函数模板的概述
  • 模的板定义以 template 关键字开头

    • 后面紧跟 <>,其中称为模板参数列表(或模板实参列表)。
  • 模板参数列表规则

    • 至少包含一个模板参数。
    • 多个参数用逗号分隔。
    • 每个模板参数前需使用 typenameclass 关键字(此处 class 非类定义用途)。
  • 常见写法示例

    • 单个模板参数
      • template<class T>
      • 或者 template<typename T>
    • 多个模板参数:
      • template<class T, class Q>
      • 或者 template<typename T, typename Q>
  • 模板参数的含义

    • 表示函数定义中使用的类型或值,类似于函数参数列表的作用。
  • 模板实参的指定方式

    • 显式指定:使用 <> 包裹模板实参,比如 max<int>(3, 5);
    • 隐式推断:编译器根据函数调用上下文自动推断模板实参,无需显式指定。
  • 类型参数 T 的作用

    • T 是模板的类型参数,本身只是一个占位符。
    • 在模板被使用时,编译器会进行模板实例化,根据调用上下文推导出 T 的具体类型。这个过程通常发生在编译期,而不是运行时
函数模板的使用

关键点

函数模板的调用与普通函数的调用区别不大。在调用函数模板时,编译器会根据调用时提供的实参,去推断模板参数列表中的参数(即模板形参)的类型。有些时候,光凭函数实参是无法准确推断出模板参数的,这时候就需要使用 <> 来主动提供模板参数了,比如 max<int>(3, 5);。值得注意的是,模板参数有时是由编译器推断出来的,推断的依据是调用函数时传入的实参。当编译器推断出模板的形参类型后,编译器会实例化一个特定版本的函数。函数模板只有被调用(实例化)时,编译器才会生成对应的代码,仅定义模板编译器是不会生成任何代码的。由于编译器在生成代码的时候,需要找到函数的函数体,所以函数模板的定义通常是写在 .h 头文件中。

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
#include <iostream>

using namespace std;

// 定义函数模板
template <typename T>
T maxNum(T a, T b) {
return (a > b) ? a : b;
}

int main() {
// 调用时,编译器根据实参(10, 20)推断 T 为 int
int result1 = maxNum(10, 20);

// 调用时,编译器根据实参(3.14, 2.71)推断 T 为 double
double result2 = maxNum(3.14, 2.71);

// 如果实参类型不一致,编译器无法唯一确定 T,会导致模板参数推断失败
// double result3 = max(10, 3.14);

// 显式指定模板参数
double result3 = maxNum<double>(10, 3.14);

return 0;
}
非类型模板参数

关键点

在定义函数模板时,因为 T 前边有一个 typename / class,这表示 T 代表一个类型,是一个类型参数。那么在模板参数列表里面,还可以定义非类型参数,这里的非类型参数代表的是一个值。既然非类型参数代表一个值,那么就不能用 typename / class 这种关键字来修饰这个值,而是必须要用具体的类型名称来指定非类型参数。比如,非类型参数 S 如果是个整型,就可以写成 template<typename T, int S>。当函数模板实例化时,这种非类型参数的值通常需要由用户显式指定(一般情况下编译器不会自动推断),并且这些值都必须是常量表达式,因为函数模板的实例化是在编译阶段完成的。另外,无论是函数模板还是类模板,非类型模板参数都可以指定默认值,比如 template <typename T, int A = 5>,但需要注意非类型模板参数的默认值必须从右向左连续定义。特别注意,浮点型不能作为非类型模板参数,比如 floatdouble

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
#include <cstring>
#include <iostream>

using namespace std;

// 定义函数模板(使用非类型模板参数)
template <int A, int B>
int sum() {
return A + B;
}

// 定义函数模板(使用非类型模板参数)
template <unsigned L1, unsigned L2>
int charscomp(const char (&c1)[L1], const char (&c2)[L2]) {
return strcmp(c1, c2);
}

// 定义函数模板(使用类型模板参数和非类型模板参数)
template <typename T, int A = 5>
void repeatPrint(T value) {
for (int i = 0; i < A; ++i) {
cout << value << " ";
}
cout << endl;
}

void test01() {
cout << "========== test01() ======" << endl;
// 显式指定非类型模板参数
int result1 = sum<3, 5>();
cout << result1 << endl;
}

void test02() {
cout << "========== test02() ======" << endl;
// 编译器会自动推断非类型模板参数
int result1 = charscomp("test2", "test");
cout << result1 << endl;
}

void test03() {
cout << "========== test03() ======" << endl;
// T 被推断为 int,而 A 显式指定为 2
repeatPrint<int, 2>(10);

// T 被推断为 double,而 A 显式指定为 3
repeatPrint<double, 3>(3.14);

// T 被推断为 const char*,而 A 显式指定为 4
repeatPrint<const char*, 4>("Hi");

// T 被推断为 float,而 A 默认指定为 5
repeatPrint<float>(6.7f);
}

int main() {
test01();
test02();
test03();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
========== test01() ======
8
========== test02() ======
50
========== test03() ======
10 10
3.14 3.14 3.14
Hi Hi Hi Hi
6.7 6.7 6.7 6.7 6.7

类模板

类模板的概述

类模板可以用来实例化一个特定的类。由于编译器不能为类模板自动推断模板参数类型,因此为了使用类模板,必须在模板名后边使用 <> 来提供额外的信息,这些信息其实就是对应着模板参数的具体类型或值。类模板的核心优势在于:同一套代码,可以应付不同的数据类型,代码精简多了,避免了为每种类型重复编写类似的类。在 C++ 模板中,只要一个名称依赖于模板参数,编译器在第一阶段无法判断它是类型还是变量,默认按非类型解析;因此无论是在类模板还是函数模板中,都必须使用 typename 显式声明它是类型。

对比项函数模板类模板
模板实参推断支持自动推断不支持(C++17 之前),需显式指定
使用方式max(1, 2)vector<int>
实例化时机调用时实例化(编译期实例化)使用类型时实例化(编译期实例化)
函数 / 成员函数生成调用时生成成员函数按需实例化(懒生成)
类模板的使用

关键点

C++ 类模板的成员函数是指在类模板中定义的函数,这些函数同样依赖于模板参数,因此在类外实现类模板的成员函数时,需要保留模板参数声明(比如 template<typename T>)并使用作用域限定符(如 MyVector<T>::func())进行定义。由于模板是在编译期进行实例化的,成员函数的实现通常需要放在头文件中,以便编译器在使用时能够生成具体类型的代码。此外,在成员函数中如果使用了依赖于模板参数的类型(如 MyVector<T>::iterator),编译器默认无法判断其是类型还是变量,此时必须使用 typename 进行显式说明(如 typename MyVector<T>::iterator)。总体来说,类模板的成员函数与普通类成员函数在语法上类似,但需要额外注意模板参数、依赖名称解析以及编译期实例化机制。

  • MyVector.hpp,简单模拟实现 STL 的 vector 容器,不考虑扩容、移动语义等实现
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#pragma once

#include <iostream>

using namespace std;

// 定义类模板
template <typename T>
class MyVector {
public:
// 定义迭代器类型
using iterator = T*;

// 构造函数
MyVector();

// 析构函数
~MyVector();

// 拷贝构造函数
MyVector(const MyVector& v);

// 赋值运算符
MyVector& operator=(const MyVector& v);

// 返回迭代器的起始位置
iterator begin();

// 返回迭代器的最后一个元素的下一个位置
iterator end();

// 往容器末尾添加元素
bool push_back(const T& t);

private:
T* m_data; // 底层数组
size_t m_size; // 元素个数
size_t m_capacity; // 最大容量
};

// 构造函数
template <typename T>
MyVector<T>::MyVector() {
m_size = 0;
m_capacity = 10;

// 申请内存空间
m_data = new T[m_capacity];
}

// 析构函数
template <typename T>
MyVector<T>::~MyVector() {
if (m_data) {
delete[] m_data;
m_data = nullptr;
}
}

// 拷贝构造函数
template <typename T>
MyVector<T>::MyVector(const MyVector<T>& v) {
m_size = v.m_size;
m_capacity = v.m_capacity;

// 深拷贝
m_data = new T[m_capacity];
for (size_t i = 0; i < m_size; ++i) {
m_data[i] = v.m_data[i];
}
}

// 赋值运算符
template <typename T>
MyVector<T>& MyVector<T>::operator=(const MyVector<T>& v) {
// 防止自赋值
if (this == &v) {
return *this;
}

// 申请新的内存空间
T* new_data = new T[v.m_capacity];
for (size_t i = 0; i < v.m_size; ++i) {
new_data[i] = v.m_data[i];
}

// 释放旧的内存空间
delete[] m_data;

m_data = new_data;
m_size = v.m_size;
m_capacity = v.m_capacity;

return *this;
}

// 返回迭代器的起始位置
template <typename T>
typename MyVector<T>::iterator MyVector<T>::begin() {
return m_data;
}

// 返回迭代器的最后一个元素的下一个位置
template <typename T>
typename MyVector<T>::iterator MyVector<T>::end() {
return m_data + m_size;
}

// 往容器末尾添加元素
template <typename T>
bool MyVector<T>::push_back(const T& t) {
if (m_size == m_capacity) {
return false;
}

m_data[m_size] = t;
++m_size;

return true;
}
  • main.hpp
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
#include <iostream>
#include "MyVector.hpp"

using namespace std;

void show(MyVector<int>& v) {
for (MyVector<int>::iterator it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
cout << endl;
}

int main() {
MyVector<int> v1;
v1.push_back(1);
v1.push_back(3);
v1.push_back(5);
show(v1);

MyVector<int> v2 = v1;
show(v2);

v2 = v1;
show(v2);

return 0;
}

程序运行输出的结果如下:

1
2
3
1 3 5 
1 3 5
1 3 5
非类型模板参数

关键点

在定义类模板时,因为 T 前边有一个 typename / class,这表示 T 代表一个类型,是一个类型参数。那么在模板参数列表里面,还可以定义非类型参数,这里的非类型参数代表的是一个值。既然非类型参数代表一个值,那么就不能用 typename / class 这种关键字来修饰这个值,而是必须要用具体的类型名称来指定非类型参数。比如,非类型参数 S 如果是个整型,就可以写成 template<typename T, int S>。当类模板实例化时,这种非类型参数的值通常需要由用户显式提供(一般情况下编译器不会自动推断),并且这些值都必须是常量表达式,因为类模板的实例化是在编译阶段完成的。另外,无论是函数模板还是类模板,非类型模板参数都可以指定默认值,比如 template <typename T, int A = 5>,但需要注意非类型模板参数的默认值必须从右向左连续定义。特别注意,浮点型不能作为非类型模板参数,比如 floatdouble

  • MyVector.hpp,简单模拟实现 STL 的 vector 容器,不考虑扩容、移动语义等实现
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#pragma once

#include <iostream>

using namespace std;

// 定义类模板(使用类型模板参数和非类型模板参数)
template <typename T, int C = 10>
class MyVector {
public:
// 定义迭代器类型
using iterator = T*;

// 构造函数
MyVector();

// 析构函数
~MyVector();

// 拷贝构造函数
MyVector(const MyVector& v);

// 赋值运算符
MyVector& operator=(const MyVector& v);

// 返回迭代器的起始位置
iterator begin();

// 返回迭代器的最后一个元素的下一个位置
iterator end();

// 往容器末尾添加元素
bool push_back(const T& t);

private:
T* m_data; // 底层数组
size_t m_size; // 元素个数
size_t m_capacity; // 最大容量
};

// 构造函数
template <typename T, int C>
MyVector<T, C>::MyVector() {
m_size = 0;
m_capacity = C > 0 ? C : 10;

// 申请内存空间
m_data = new T[m_capacity];
}

// 析构函数
template <typename T, int C>
MyVector<T, C>::~MyVector() {
if (m_data) {
delete[] m_data;
m_data = nullptr;
}
}

// 拷贝构造函数
template <typename T, int C>
MyVector<T, C>::MyVector(const MyVector<T, C>& v) {
m_size = v.m_size;
m_capacity = v.m_capacity;

// 深拷贝
m_data = new T[m_capacity];
for (size_t i = 0; i < m_size; ++i) {
m_data[i] = v.m_data[i];
}
}

// 赋值运算符
template <typename T, int C>
MyVector<T, C>& MyVector<T, C>::operator=(const MyVector<T, C>& v) {
// 防止自赋值
if (this == &v) {
return *this;
}

// 申请新的内存空间
T* new_data = new T[v.m_capacity];
for (size_t i = 0; i < v.m_size; ++i) {
new_data[i] = v.m_data[i];
}

// 释放旧的内存空间
delete[] m_data;

m_data = new_data;
m_size = v.m_size;
m_capacity = v.m_capacity;

return *this;
}

// 返回迭代器的起始位置
template <typename T, int C>
typename MyVector<T, C>::iterator MyVector<T, C>::begin() {
return m_data;
}

// 返回迭代器的最后一个元素的下一个位置
template <typename T, int C>
typename MyVector<T, C>::iterator MyVector<T, C>::end() {
return m_data + m_size;
}

// 往容器末尾添加元素
template <typename T, int C>
bool MyVector<T, C>::push_back(const T& t) {
if (m_size == m_capacity) {
return false;
}

m_data[m_size] = t;
++m_size;

return true;
}
  • main.cpp
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
#include <iostream>
#include "MyVector.hpp"

using namespace std;

template <int C>
void show(MyVector<int, C>& v) {
for (typename MyVector<int, C>::iterator it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
cout << endl;
}

int main() {
MyVector<int, 5> v1;
v1.push_back(1);
v1.push_back(3);
v1.push_back(5);
show(v1);

MyVector<int, 5> v2 = v1;
show(v2);

v2 = v1;
show(v2);

return 0;
}

程序运行输出的结果如下:

1
2
3
1 3 5 
1 3 5
1 3 5

模板进阶

typename 的使用场合

在 C++ 中,typename 常见的三种使用场合:

  • (1) 函数模板的定义:

    1
    2
    3
    4
    5
    // 这里可以将 typename 替换为 class
    template <typename T>
    T maxNum(T a, T b) {
    ......
    }
  • (2) 类模板的定义:

    1
    2
    3
    4
    5
    // 这里可以将 typename 替换为 class
    template <typename T>
    class MyVector {
    ......
    }
  • (3) 类外访问类模板的类型成员:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    template <typename T>
    class MyVector {
    public:
    using iterator = T*;

    iterator begin();

    ......
    }

    template <typename T>
    // 这里访问类模板的类型成员 iterator 时,必须在最前面加上 typename,且不能替换成 class,否则编译器会误将 iterator 解析成类的静态成员
    typename MyVector<T>::iterator MyVector<T>::begin() {
    ......
    }

函数指针的使用方式

函数指针作为其他函数的参数

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
#include <iostream>

using namespace std;

// 定义函数指针类型(第一种写法,C 语义风格)
typedef int (*FunType)(int, int);

// 定义函数指针类型(第二种写法,现代 C++ 的写法)
// using FunType = int(*)(int, int);

// 定义普通函数
int sum(int a, int b) {
return a + b;
}

// 函数指针作为其他函数的参数
int testFunc(int i, int j, FunType func) {
// 通过调用函数指针调用函数
return func(i, j);
}

int main() {
int result = testFunc(3, 5, sum);
cout << result << endl;
return 0;
}

程序运行输出的结果如下:

1
8

函数模板的趣味写法

在函数模板中,使用函数指针和函数对象

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
71
72
73
74
75
76
77
78
#include <iostream>

using namespace std;

// 定义函数指针类型(第一种写法,C 语义风格)
typedef int (*FunType)(int, int);

// 定义函数指针类型(第二种写法,现代 C++ 的写法)
// using FunType = int(*)(int, int);

// 定义函数模板(支持使用函数指针或函数对象作为其他函数的参数)
template <typename T, typename F>
int testFunc(const T &i, const T &j, F func) {
return func(i, j);
}

// 定义普通函数
int sum(int a, int b) {
return a + b;
}

// 定义函数对象(仿函数),重载了 operator(),可以像函数一样使用对象
class Test {
public:
// 构造函数
Test() {
cout << "Test()" << endl;
}

// 析构函数
~Test() {
cout << "~Test()" << endl;
}

// 拷贝构造函数
Test(const Test &t) {
cout << "Test(const Test&)" << endl;
}

// 重载圆括号操作符
int operator()(int i, int j) const {
return i + j;
}
};

void test01() {
cout << "------------ test01() ---------------" << endl;

// 函数指针作为其他函数的参数
int result = testFunc(1, 3, sum);
cout << result << endl;
}

void test02() {
cout << "------------ test02() ---------------" << endl;

// 定义函数对象(可调用对象)
Test obj;

// 函数对象作为其他函数的参数(会额外调用 Test 类的拷贝构造函数)
int result2 = testFunc(5, 6, obj);
cout << result2 << endl;
}

void test03() {
cout << "------------ test03() ---------------" << endl;

// 函数对象作为其他函数的参数(更高效的写法,直接使用临时对象,不会额外调用 Test 类的拷贝构造函数)
int result3 = testFunc(7, 8, Test());
cout << result3 << endl;
}

int main() {
test01();
test02();
test03();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
------------ test01() ---------------
4
------------ test02() ---------------
Test()
Test(const Test&)
~Test()
11
~Test()
------------ test03() ---------------
Test()
~Test()
15

函数指针与函数对象的简单介绍

  • 函数指针(Function Pointer)

    • 定义:指向函数地址的指针
      • C 语言的定义风格:typedef int (*FunType)(int, int);
      • 现代 C++ 的定义风格:using FunType = int(*)(int, int);
    • 本质:存储函数入口地址,可用于间接调用函数
    • 调用方式:通过指针调用,例如 fp(a, b)(*fp)(a, b)
    • 特点:不具备状态、语法较复杂、灵活性较低但开销小,常用于 C 风格接口或回调函数
  • 函数对象(Function Object / 仿函数)

    • 定义:重载了 operator() 的类或结构体对象
    • 本质:一个 “可调用的对象”(对象 + 函数行为)
    • 调用方式:通过对象名加 () 调用,例如 obj(a, b)
    • 特点:可以携带状态(成员变量)、支持内联优化、可用于模板参数(STL 中广泛使用,比如 std::lessstd::greater
  • 函数指针与函数对象主要区别

对比函数指针函数对象
是否能存状态
是否可内联优化一般不行
灵活性

默认模板参数的使用

函数模板使用默认模板参数

  • 模板参数设置默认类型(typename T = int
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

// 定义函数模板(使用模板参数默认值)
template <typename T = int>
T add(T a, T b) {
return a + b;
}

int main() {
cout << add(1, 2) << endl; // 使用默认 T=int
cout << add<double>(1.5, 2.3); // 指定 T=double
}
  • 函数参数设置默认值(T b = 10
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

template <typename T>
T add(T a, T b = 10) {
return a + b;
}

int main() {
cout << add(5, 3) << endl; // 8
cout << add(5) << endl; // 15(b 使用默认值 10)
}
  • 两者结合使用
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

template <typename T = int>
T add(T a, T b = 10) {
return a + b;
}

int main() {
cout << add(5) << endl; // T=int, b=10 → 15
cout << add<double>(5.5) << endl; // T=double, b=10 → 15.5
}

类模板使用默认模板参数

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
#include <exception>
#include <iostream>

using namespace std;

// 定义类模板(使用默认模板参数)
template <typename T, size_t size = 10>
class MyArray {
public:
// 重载 [] 操作运算符,返回非 const 对象
T& operator[](size_t index) {
if (index < 0 || index > size - 1) {
throw out_of_range("index out of range");
}
return array[index];
}

// 重载 [] 操作运算符,返回 const 对象
const T& operator[](size_t index) const {
if (index < 0 || index > size - 1) {
throw out_of_range("index out of range");
}
return array[index];
}

private:
T array[size];
};

int main() {
MyArray<int> array; // size = 10
array[0] = 10;
array[1] = 20;
cout << array[0] << ", " << array[1] << endl;


MyArray<int, 5> array2; // size = 5
array2[0] = 40;
array2[1] = 50;
cout << array2[0] << ", " << array2[1] << endl;

return 0;
}

程序运行输出的结果如下:

1
2
10, 20
40, 50

成员函数模板的使用

在 C++ 中,无论是普通类还是类模板,其成员函数(包括构造函数)都可以声明为函数模板(称为成员函数模板)。但由于虚函数(virtual)机制依赖于运行时多态,而函数模板是在编译期间实例化的,因此成员函数模板不能被声明为虚函数(virtual)。

特别注意

对于类模板,其成员函数(包括普通成员函数和成员函数模板)通常采用 "按需实例化"(Lazy Instantiation)机制。只有当程序中实际使用(例如发生函数调用)某个成员函数时,编译器才会对该函数进行实例化。如果某个成员函数从未被使用,则不会被实例化。

普通类的成员函数模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

using namespace std;

// 普通类
class Test {
public:
// 成员函数模板
template <typename T>
T sum(T v1, T v2) {
return v1 + v2;
}
};

int main() {
Test obj;

// 函数模板支持自动推断模板参数类型
int result = obj.sum(3, 5);

cout << result << endl; // 输出 8

return 0;
}
类模板的成员函数模板

第一种写法(类的声明与定义写在一起)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

using namespace std;

// 类模板
template <typename A>
class Test {
public:
// 成员函数模板
template <typename B>
void func(A v1, B v2) {
cout << v1 << ", " << v2 << endl;
}
};

int main() {
// 类模板不支持自动推断模板参数类型
Test<int> obj;

// 函数模板支持自动推断模板参数类型
obj.func(3, 5.3);

return 0;
}

第二种写法(类的声明与定义分开写)

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
#include <iostream>

using namespace std;
template <typename A>
class Test {
public:
// 成员函数模板
template <typename B>
void func(A v1, B v2);
};

template <typename A>
template <typename B>
void Test<A>::func(A v1, B v2) {
cout << v1 << ", " << v2 << endl;
}

int main() {
// 类模板不支持自动推断模板参数类型
Test<int> obj;

// 函数模板支持自动推断模板参数类型
obj.func(3, 5.3);

return 0;
}

模板显式实例化的使用

为了避免在多个 .cpp 文件中重复实例化同一个模板(包括类模板和函数模板),C++ 提供了 “显式实例化”(Explicit Instantiation)机制。通过在某一个 .cpp 源文件中进行显式实例化,可以将模板实例化集中到单一个翻译单元,从而避免重复生成代码并减少编译和链接开销。实际上,编译器和链接器通常会对重复的模板实例进行合并(如 COMDAT 机制),但显式实例化仍然可以用于控制模板实例化的位置,并在一定程度上减少编译时间和代码膨胀。

特别注意

  • 显式实例化(Explicit Instantiation)既适用于类模板,也适用于函数模板,其本质是为指定类型生成对应的模板实例代码。
  • 函数模板的显式实例化:其本质是在某个 .cpp 源文件中为指定类型(如 int)生成对应的函数实现,其他翻译单元通过 extern template 声明避免函数模板重复实例化。
  • 类模板的显式实例化:其本质是在某个 .cpp 源文件中为某个具体类型(如 MyClass<int>)生成完整的实例代码(包括类结构及其所有已定义的成员函数实现),其他翻译单元通过 extern template 声明避免类模板重复实例化。
类模板的显式实例化
  • MyClass.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef MYCLASS_H
#define MYCLASS_H

#include <iostream>

using namespace std;

// 类模板声明
template <typename T>
class MyClass {
public:
void print(T value);
};

// 声明:显式实例化,可以写在多个地方(告诉编译器,MyClass<int> 这个实例已经在别处生成,当前翻译单元不要再实例化)
extern template class MyClass<int>;

#endif
  • MyClass.cpp
1
2
3
4
5
6
7
8
9
10
#include "MyClass.h"

// 类模板声明定义
template <typename T>
void MyClass<T>::print(T value) {
cout << "value: " << value << endl;
}

// 定义:显式实例化,只写在一个地方(告诉编译器,在当前 .cpp 源文件中,为 MyClass<int> 这个具体类型生成完整的实例代码)
template class MyClass<int>;
  • main.cpp
1
2
3
4
5
6
7
#include "MyClass.h"

int main() {
MyClass<int> obj;
obj.print(42);
return 0;
}
函数模板的显式实例化
  • func.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef FUNC_H
#define FUNC_H

#include <iostream>

using namespace std;

// 函数模板声明
template <typename T>
void printValue(T value);

// 声明:显式实例化,可以写在多个地方(告诉编译器,printValue<int>(int) 这个实例已经在别处生成,当前翻译单元不要再实例化)
extern template void printValue<int>(int);

#endif
  • func.cpp
1
2
3
4
5
6
7
8
9
10
#include "func.h"

// 函数模板定义
template <typename T>
void printValue(T value) {
cout << "value: " << value << endl;
}

// 定义:显式实例化,只写在一个地方(告诉编译器,在当前 .cpp 源文件中,为 printValue<int>(int) 这个具体类型生成完整的实例代码)
template void printValue<int>(int);
  • main.cpp
1
2
3
4
5
6
#include "func.h"

int main() {
printValue(42); // 使用 printValue<int>
return 0;
}

使用 using 定义别名模板

在 C++ 中,使用 using 定义别名模板(也叫模板别名)是一种比 typedef 更强大且语法更清晰的特性。其核心形式为 template<typename ...> using 别名 = 原类型;,它允许为一个依赖于模板参数的类型表达式创建一个新的名称。与只能定义具体类型别名的 typedef 不同,using 可以直接定义别名模板(Alias Template),从而简化复杂的模板类型书写。例如,template<typename T> using Vec = std::vector<T>;std::vector 创建了别名,之后 Vec<int> 就等价于 std::vector<int>。这一特性极大地提升了泛型编程中类型代码的可读性和可维护性。

特别注意

在 C++ 中,using 除了可以用于定义类型别名,还可以定义别名模板,它包含了 typedef 的所有功能。

定义类型别名
  • 使用 typedef 定义类型别名的写法(C 语言风格)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <map>

using namespace std;

// 使用 typedef 定义类型别名
int main() {
typedef unsigned int uint_t;
uint_t a;

typedef map<string, int> map_s_i;
map_s_i m;
m.insert({"first", 1});
m.insert({"second", 2});

typedef map<string, string> map_s_s;
map_s_s m2;
m2.insert({"first", "one"});
m2.insert({"second", "two"});

return 0;
}
  • 使用 using 定义类型别名的写法(现代 C++ 风格)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <map>

using namespace std;

// 使用 typedef 定义类型别名
int main() {
using uint_t = unsigned int;
uint_t a;

using map_s_i = map<string, int>;
map_s_i m;
m.insert({"first", 1});
m.insert({"second", 2});

using map_s_s = map<string, string>;
map_s_s m2;
m2.insert({"first", "one"});
m2.insert({"second", "two"});

return 0;
}
定义别名模板

在 C++ 中,使用 using 定义别名模板

这里假设需要定义一个 map 类型,其中 key 的类型固定不变,但 value 的类型支持自定义,实现方式有两种:

  • C++ 98 的写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <map>

using namespace std;

// C++ 98 的写法,定义一个 map 类型,key 的类型固定不变,但 value 的类型可以自定义
template <typename st>
struct map_s {
typedef map<string, st> type;
};

int main() {
map_s<int>::type m;
m.insert({"first", 1});
return 0;
}
  • C++ 11 的写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <map>

using namespace std;

// C++ 11 的写法(使用别名模板),定义一个 map 类型,key 的类型固定不变,但 value 的类型可以自定义
template <typename T>
using str_map_t = std::map<string, T>; // str_map_t 是类型别名

int main() {
str_map_t<int> m;
m.insert({"first", 1});
return 0;
}

在 C++ 中,使用 using 定义别名模板 + 函数指针

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
#include <iostream>

using namespace std;

// 定义别名模板 + 函数指针
template <typename T>
using funcType = T (*)(T, T);

int sum(int a, int b) {
return a + b;
}

int main() {
// 定义函数指针
funcType<int> func; // funcType 是一个函数指针类型,func 是函数指针变量

// 将函数地址赋值给函数指针变量
func = sum;

// 调用函数指针
int result = func(1, 3);
cout << result << endl; // 输出 4

return 0;
}

显式指定模板实参

在 C++ 中,” 显式指定模板实参” 是指在调用函数模板或实例化类模板时,由程序员在模板名后紧跟的尖括号内明确写出模板参数所对应的具体类型或值,而不是依赖编译器进行模板实参推导。这一语法通常用于解决编译器无法自动推导实参的场景,例如函数模板的返回类型与参数列表无关,或者存在多个模板参数但仅有部分能推导时,也可以显式指定部分实参,其余由编译器补全。通过显式指定,程序员可以精确控制模板实例化的行为,消除因类型推导可能产生的歧义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

using namespace std;

// 定义函数模板
template <typename T1, typename T2, typename T3>
T1 sum(T2 i, T3 j) {
T1 result = i + j;
return result;
}

int main() {
// 显式指定模板实参,否则计算结果会出错(由于模板实参类型自动推断,导致整数溢出)
auto result = sum<double, double, double>(2000000000, 2000000000);
cout << result << endl;

return 0;
}

程序运行输出的结果如下:

1
4e+09

模板高级

模板特例化

类模板特例化

C++ 的模板特例化(Template Specialization)是一种允许为特定类型或特定值提供模板自定义实现的机制,用于处理泛型模板无法满足特殊需求的场景。它主要分为全特化(Full Specialization)和偏特化(Partial Specialization)两种形式:全特化为所有模板参数指定具体类型或值,生成一个完全确定的独立版本;偏特化则仅针对部分参数进行限制(如将泛型指针特化为特定指针类型),从而生成更特殊的模板版本。通过模板特例化,程序员可以在保持泛型接口统一的同时,为某些特定类型优化算法效率或修复语义问题,是模板元编程中实现条件分支和类型萃取的核心手段之一。值得注意的是,类模板全特化后是一个具体类(不再接受模板参数),而类模板偏特化后本质上仍然是类模板(仍然需要实例化)

特别注意

在 C++ 中,类模板必须先存在泛化版本(至少是声明),才能为其定义任何特化版本(全特化或偏特化);特化是建立在泛化基础之上的特殊实现,不能独立存在。泛化版本是指定义模板时最通用、最基础的版本,其模板参数保持抽象(如 typename T),不针对任何具体类型或值进行特化;它是所有特化版本的原始模板,为后续可能的特例化提供了基础框架。

类模板全特化

常规全特化(特化类模板)

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
#include <iostream>

using namespace std;

// 定义类模板(泛化版本)
template <typename T, typename U>
class TestClass {
public:
TestClass() {
cout << "TestClass() 泛化版本" << endl;
}

void print(T t, U u) {
cout << "printf(T, U) 泛化版本" << endl;
}
};

// 当 T 和 U 这两个模板参数都为 int 类型时,实现一个类模板的全特化版本
template <>
class TestClass<int, int> {
public:
TestClass() {
cout << "TestClass() 全特化版本" << endl;
}

void print(int t, int u) {
cout << "printf(int, int) 全特化版本" << endl;
}
};

// 当 T 和 U 这两个模板参数都为 double 类型时,实现一个类模板的全特化版本
template <>
class TestClass<double, double> {
public:
TestClass() {
cout << "TestClass() 全特化版本" << endl;
}

void print(double t, double u) {
cout << "printf(double, double) 全特化版本" << endl;
}
};

int main() {
// 使用泛化版本
TestClass<int, double> t; // 这是泛化对象
t.print(3, 5.61);

// 使用 <int, int> 全特化版本
TestClass<int, int> t2; // 这是全特化对象
t2.print(7, 9);

// 使用 <double, double> 全特化版本
TestClass<double, double> t3; // 这是全特化对象
t3.print(2.45, 6.7);

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
TestClass() 泛化版本
printf(T, U) 泛化版本
TestClass() 全特化版本
printf(int, int) 全特化版本
TestClass() 全特化版本
printf(double, double) 全特化版本

特化成员函数,而不是类模板

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
#include <iostream>

using namespace std;

// 定义类模板(泛化版本)
template <typename T, typename U>
class TestClass {
public:
TestClass() {
cout << "TestClass() 泛化版本" << endl;
}
void print(T t, U u) {
cout << "printf(T, U) 泛化版本" << endl;
}
};

// 当 T 和 U 这两个模板类型参数都为 int 类型时,实现成员函数的特例化
template <>
void TestClass<int, int>::print(int t, int u) {
cout << "printf(int, int) 全特化版本" << endl;
}

// 当 T 和 U 这两个模板类型参数都为 double 类型时,实现成员函数的特例化
template <>
void TestClass<double, double>::print(double t, double u) {
cout << "printf(double, double) 全特化版本" << endl;
}

int main() {
// 使用泛化版本
TestClass<int, double> t; // 这是泛化对象
t.print(3, 5.61);

// 使用 <int, int> 全特化版本
TestClass<int, int> t2; // 这是泛化对象
t2.print(7, 9);

// 使用 <double, double> 全特化版本
TestClass<double, double> t3; // 这是泛化对象
t3.print(2.45, 6.7);

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
TestClass() 泛化版本
printf(T, U) 泛化版本
TestClass() 泛化版本
printf(int, int) 全特化版本
TestClass() 泛化版本
printf(double, double) 全特化版本
类模板偏特化

特别注意

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
#include <iostream>

using namespace std;

// 定义类模板(泛化版本)
template <typename T, typename U, typename W>
class TestClass {
public:
TestClass() {
cout << "TestClass() 泛化版本" << endl;
}

void print(T t, U u, W w) {
cout << "printf(T, U, W) 泛化版本" << endl;
}
};

// 类模板偏特化,从参数数量上进行偏特化(有两个模板参数绑定了具体类型,剩下一个模板参数没绑定具体类型)
template <typename U>
class TestClass<int, U, int> {
public:
TestClass() {
cout << "TestClass() 偏特化版本" << endl;
}

void print(int t, U u, int) {
cout << "printf(int, U, int) 偏特化版本" << endl;
}
};

// 类模板偏特化,从参数数量上进行偏特化(有两个模板参数绑定了具体类型,剩下一个模板参数没绑定具体类型)
template <typename U>
class TestClass<double, U, double> {
public:
TestClass() {
cout << "TestClass() 偏特化版本" << endl;
}

void print(double t, U u, double) {
cout << "printf(double, U, double) 偏特化版本" << endl;
}
};

int main() {
// 使用泛化版本
TestClass<int, int, double> t1; // 这是泛化对象
t1.print(1, 2, 3.14);

// 使用 <int, U, int> 偏特化版本
TestClass<int, double, int> t2; // 这是偏特化对象
t2.print(1, 2.55, 3);

// 使用 <double, U, double> 偏特化版本
TestClass<double, int, double> t3; // 这是偏特化对象
t3.print(1.0, 2, 3.14);

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
TestClass() 泛化版本
printf(T, U, W) 泛化版本
TestClass() 偏特化版本
printf(int, U, int) 偏特化版本
TestClass() 偏特化版本
printf(double, U, double) 偏特化版本

类模板从参数范围上进行偏特化

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <iostream>

using namespace std;

// 定义类模板(泛化版本)
template <typename T>
class TestClass {
public:
TestClass() {
cout << "TestClass() 泛化版本" << endl;
}

void print(T t) {
cout << "printf(T) 泛化版本" << endl;
}
};

// 类模板偏特化,从参数范围上进行偏特化(const T 的偏特化版本)
template <typename T>
class TestClass<const T> {
public:
TestClass() {
cout << "TestClass() 偏特化版本" << endl;
}

void print(T t) {
cout << "printf(const T) 偏特化版本" << endl;
}
};

// 类模板偏特化,从参数范围上进行偏特化(T * 的偏特化版本)
template <typename T>
class TestClass<T *> {
public:
TestClass() {
cout << "TestClass() 偏特化版本" << endl;
}

void print(T *t) {
cout << "printf(T *) 偏特化版本" << endl;
}
};

// 类模板偏特化,从参数范围上进行偏特化(T & 的偏特化版本)
template <typename T>
class TestClass<T &> {
public:
TestClass() {
cout << "TestClass() 偏特化版本" << endl;
}

void print(T &t) {
cout << "printf(T &) 偏特化版本" << endl;
}
};

// 类模板偏特化,从参数范围上进行偏特化(T && 的偏特化版本)
template <typename T>
class TestClass<T &&> {
public:
TestClass() {
cout << "TestClass() 偏特化版本" << endl;
}

void print(T &&t) {
cout << "printf(T &&) 偏特化版本" << endl;
}
};

int main() {
int num1 = 10;
const int num2 = 20;
int &num3 = num1; // 左值引用

// 使用泛化版本
TestClass<int> t1; // 这是泛化对象
t1.print(1);

// 使用 <const T> 偏特化版本
TestClass<const int> t2; // 这是偏特化对象
t2.print(num2);

// 使用 <T *> 偏特化版本
TestClass<int *> t3; // 这是偏特化对象
t3.print(&num1);

// 使用 <T &> 偏特化版本
TestClass<int &> t4; // 这是偏特化对象
t4.print(num3);

// 使用 <T &&> 偏特化版本
TestClass<int &&> t5; // 这是偏特化对象
t5.print(move(num3));

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
TestClass() 泛化版本
printf(T) 泛化版本
TestClass() 偏特化版本
printf(const T) 偏特化版本
TestClass() 偏特化版本
printf(T *) 偏特化版本
TestClass() 偏特化版本
printf(T &) 偏特化版本
TestClass() 偏特化版本
printf(T &&) 偏特化版本

类模板从参数范围上进行偏特化,这里的参数范围是指什么?

  • 偏特化允许对类模板参数施加更具体的模式或约束(即缩小模板参数的范围),而不是仅仅减少参数个数。常见形式包括:
  • 指针类型:template <typename T> struct A<T*>
  • 引用类型:template <typename T> struct A<T&>
  • 常量类型:template <typename T> struct A<const T>
  • 数组类型:template <typename T, int N> struct A<T[N]>
  • 模板的模板参数:template <typename T, template<typename> class C> struct A<C<T>>
  • 常量值限制:template <int N> struct A<N>; 再偏特化 template <> struct A<0>
函数模板特例化

特别注意

函数模板的全特化,本质上是为该模板的某个特定参数组合提供一个显式的、独立的定义;它并不是一个新模板,也不是一个普通函数,但它参与重载决议时与普通函数有本质区别。全特化不引入重载,而是替换模板实例化时生成的版本。值得一提的是,编译器优先调用普通函数;没有普通函数时,才考虑函数模板。在同一个函数模板中,优先调用全特化版本,最后才用模板自动生成的通用版本(泛化版本)

特性函数重载函数模板全特化
引入新函数✅ 是❌ 否,依附于泛化版本(主模板)
独立参与重载决议✅ 是❌ 否
参数类型可完全不同✅ 是❌ 必须匹配泛化版本(主模板)的模板参数模式
可被显式实例化引用❌ 否✅ 是
函数模板全特化
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
#include <iostream>

using namespace std;

// 定义函数模板(泛化版本)
template <typename T, typename U>
void func(T t, U u) {
cout << "func(T, U) 泛化版本" << endl;
}

// 定义函数模板(全特化版本)
template <>
void func(int t, int u) {
cout << "func(int, int) 全特化版本" << endl;
}

// 定义函数模板(全特化版本)
template <>
void func(double t, double u) {
cout << "func(double, double) 全特化版本" << endl;
}

int main() {
// 使用泛化版本
func(3, 5.6);

// 使用 <int, int> 全特化版本
func(5, 8);

// 使用 <double, double> 全特化版本
func(6.5, 3.5);

return 0;
}

程序运行输出的结果如下:

1
2
3
func(T, U) 泛化版本
func(int, int) 全特化版本
func(double, double) 全特化版本
函数模板偏特化

特别注意

函数模板没有偏特化的概念,只有类模板、变量模板(C++ 14 起)支持偏特化。函数模板的 "类似偏特化" 效果通常是通过函数重载来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

using namespace std;

// 定义函数模板(泛化版本)
template <typename T, typename U>
void func(T t, U u) {
cout << "func(T, U) 泛化版本" << endl;
}

// 错误写法,编译不通过,因为函数模板不支持偏特化
template <typename U>
void func<int, U>(int i, U u) {
cout << "func(int, U) 偏特化版本" << endl;
}

int main() {
return 0;
}
模板特例化使用建议

在 C++ 中,模板特化版本的书写位置建议:

  • 模板的声明与实现都放在该 .h 文件中,不分离到 .cpp 文件中
  • 模板的泛化版本(主模板)与特化版本应放在同一个 .h 文件
  • .h 文件内的顺序:先写泛化版本,后写特化版本

可变参模板

可变参函数模板
可变参函数模板的概述

在 C++ 中,可变参函数模板允许函数模板接受任意数量、任意不同类型的模板参数,其核心特点如下:

1
2
3
4
5
6
// 定义可变参函数模板
template <typename... Args>
void print(Args... args) {
// 获取模板参数的数量
cout << sizeof...(args) << endl;
}
  • 语法:
    • template <typename... Args>template <class... Args> 声明参数包
  • 参数数量:
    • 参数包可容纳 0 到多个任意类型的参数
    • 可以通过 sizeof... 在编译期获取参数包大小(即参数数量),如 sizeof...(args)
  • 参数包展开:
    • 使用 ... 展开参数包,如 args...
  • 递归实例化:
    • 典型实现方式是通过递归 + 特化终止
  • 常见用途:
    • 完美转发、元组、绑定器等场景
  • 注意事项:
    • 对于 template <typename... Args>
      • Args(可变参数类型)代表一包不同的类型(不是单一的类型)
      • Args 不是表示某种类型,而是表示 0 到多种不同的类型
    • 对于 void print(Args... args) {}
      • args(可变参数)代表一包形参
      • 各个形参的类型可以各不相同
可变参函数模板的使用

可变参函数模板的使用案例一(获取可变参数的数量)

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
#include <iostream>

using namespace std;

// 定义可变参函数模板
template <typename... Args>
void print1(Args... args) {
// 获取模板参数的数量(第一种写法)
cout << sizeof...(args) << endl;
}

// 定义可变参函数模板
template <typename T, typename... Args>
void print2(T first, Args... args) {
// 获取模板参数的数量(第二种写法)
cout << sizeof...(Args) << endl;
}

int main() {
print1();
print1(1, 33, 44.5);

print2(1.2, "hello");
print2(2.3, "hello", 5);

return 0;
}

程序运行输出的结果如下:

1
2
3
4
0
3
1
2

可变参函数模板的使用案例二(通过递归调用展开参数包,即获取参数包里的参数值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

using namespace std;

// 递归终止函数
void print() {
cout << "递归终止" << endl;
}

// 定义可变参函数模板
template <typename T, typename... Args>
void print(T first, Args... args) {
cout << first << endl;
// 递归调用,展开参数包
print(args...);
}

int main() {
print(2, 3.14, "hello");
return 0;
}

程序运行输出的结果如下:

1
2
3
4
2
3.14
hello
递归终止

可变参函数模板的使用案例三(通过数组初始化时的逗号表达式配合 { } 来展开参数包,即获取参数包里的参数值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

using namespace std;

// 定义可变参函数模板
template <typename... Args>
void print(Args... args) {
// 使用数组初始化,利用逗号运算符依次执行并丢弃结果
using expander = int[];
(void)expander{0, (std::cout << args << std::endl, 0)...};
}

int main() {
print(2, 3.14, "hello");
return 0;
}

程序运行输出的结果如下:

1
2
3
2
3.14
hello
  • (std::cout << args << std::endl, 0) 输出后返回 0
  • expander{0, 返回值1, 返回值2...} 保证参数包的展开顺序
  • (void) 用于避免编译器警告

可变参函数模板的使用案例四(通过数组初始化时的逗号表达式 + 完美转发来展开参数包,即获取参数包里的参数值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

using namespace std;

// 定义可变参函数模板
template <typename... Args>
void print(Args&&... args) {
using expander = int[];
// 使用数组初始化 + 完美转发,适合需要转发参数的场景
(void)expander{0, (std::cout << std::forward<Args>(args) << std::endl, 0)...};
}

int main() {
print(2, 3.14, "hello");
return 0;
}

程序运行输出的结果如下:

1
2
3
2
3.14
hello
可变参类模板
可变参类模板的概述

在 C++ 中,可变参类模板允许类模板接受任意数量、任意不同类型的模板参数。

1
2
3
4
5
6
// 定义可变参类模板
template <typename... Types>
class MyClass {
// 获取模板参数的数量
static constexpr std::size_t size = sizeof...(Types);
};
  • 语法:

    • template <typename... Types>template <class... Types> 声明参数包
  • 参数数量:

    • 参数包可容纳 0 到多个任意类型的参数
    • 可以通过 sizeof... 在编译期获取参数包大小(即参数数量),如 sizeof...(Types)
  • 参数包展开:

    • 使用 ... 展开参数包,常用于继承或成员变量声明,如:std::tuple<Types...>
  • 递归继承:

    • 典型实现方式是通过递归继承 + 特例化终止
  • 常见用途:

    • 元组:std::tuple<int, double, string>
    • 变体:std::variant<int, double, string>
    • 类型容器:存储或操作多个类型
    • 继承展开:多重继承参数包中的每个类型
  • 注意事项:

    • 对于 template <typename... Types>
      • Types(可变参数类型)代表一包不同的类型(不是单一的类型)
      • Types 不是表示某种类型,而是表示 0 到多种不同的类型
    • 与函数模板区别:
      • 类模板的参数包通常放在模板参数列表末尾(函数模板无此限制)
可变参类模板的使用

可变参类模板的使用案例一(通过递归继承方式展开参数包,即获取参数包里的参数值)

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
#include <iostream>

using namespace std;

// 定义可变参类模板(主模板)
template <typename... Others>
class myclasst;

// 特例化终止
template <>
class myclasst<> {
public:
// 默认构造函数
myclasst() {
cout << "特例化终止, address = " << this << endl;
}
};

// 使用递归继承展开参数包
template <typename First, typename... Others>
class myclasst<First, Others...> : private myclasst<Others...> { // 继承关系
public:
// 默认构造函数
myclasst() {
cout << "address = " << this << endl;
}

// 有参构造函数
myclasst(First first, Others... others) : m_first(first), myclasst<Others...>(others...) {
cout << "address = " << this << endl;
// 获取模板参数的数量
cout << "size = " << sizeof...(others) << endl;
// 获取模板参数的值
cout << "value = " << m_first << endl;
}

public:
First m_first;
};

int main() {
myclasst<int, float, double> myc(2, 1.2f, 3.45);
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
特例化终止, address = 0x7ffef2def920
address = 0x7ffef2def920
size = 0
value = 3.45
address = 0x7ffef2def920
size = 1
value = 1.2
address = 0x7ffef2def920
size = 2
value = 2

可变参类模板的使用案例二(通过递归组合方式展开参数包,即获取参数包里的参数值)

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
/*
* 可变参模板续、模板模板参数
*
* (a) 可变参类模板:通过递归组合方式展开参数包
*/

#include <iostream>

using namespace std;

// 定义可变参类模板(主模板)
template <typename... Others>
class myclasst;

// 特例化终止
template <>
class myclasst<> {
public:
// 默认构造函数
myclasst() {
cout << "特例化终止, address = " << this << endl;
}
};

// 使用递归继承展开参数包
template <typename First, typename... Others>
class myclasst<First, Others...> {
public:
// 默认构造函数
myclasst() {
cout << "address = " << this << endl;
}

// 有参构造函数
myclasst(First first, Others... others) : m_first(first), m_obj(others...) {
cout << "address = " << this << endl;
// 获取模板参数的数量
cout << "size = " << sizeof...(others) << endl;
// 获取模板参数的值
cout << "value = " << m_first << endl;
}

public:
First m_first;
myclasst<Others...> m_obj; // 组合关系
};

int main() {
myclasst<int, float, double> myc(2, 1.2f, 3.45);
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
特例化终止, address = 0x7fff0b10b308
address = 0x7fff0b10b300
size = 0
value = 3.45
address = 0x7fff0b10b2f8
size = 1
value = 1.2
address = 0x7fff0b10b2f0
size = 2
value = 2

可变参类模板的使用案例三(通过 std::tuple 和递归调用展开参数包,即获取参数包里的参数值)

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
#include <iostream>
#include <tuple>

using namespace std;

// 定义可变参类模板
template <int curCount, int argsTotal, typename... T>
class myclasst {
public:
static void print(const tuple<T...>& t) {
cout << get<curCount>(t) << endl;
// 递归调用
myclasst<curCount + 1, argsTotal, T...>::print(t);
}
};

// 特化终止,用于结束递归调用
template <int curCount, typename... T>
class myclasst<curCount, curCount, T...> {
public:
static void print(const tuple<T...>& t) {
cout << "特化终止" << endl;
}
};

// 定义可变参函数模板
template <typename... T>
void print(const tuple<T...>& t) {
myclasst<0, sizeof...(T), T...>::print(t);
}

int main() {
tuple<int, float, double> t(2, 2.5f, 3.14);
print(t);
return 0;
}

程序运行输出的结果如下:

1
2
3
4
2
2.5
3.14
特化终止
模板模板参数

模板模板参数的概述

  • template <typename T> 中,T 叫做模板参数,因为前面都有 typename 修饰,所以又称为类型模板参数
  • 模板模板参数是指:模板的参数本身又是一个模板,也就是说 T 这模板参数又是一个模板
    • 第一种写法:template <template <typename> class Container>
    • 第二种写法:template <template <typename> typename Container>
  • 模板模板参数的特点:
    • template <typename> 取代 typenameclass
    • 参数名(如 Container)本身是一个模板,需要进一步实例化
    • 从 C++ 17 开始可以用 typename 替代 class,比如:template <template <typename> typename Container>
1
2
3
4
5
6
7
8
9
10
11
// 类型模板参数
template <typename T> // T 是类型
class A {};

// 模板模板参数
template <template <typename> class Container> // Container 是一个模板
class MyClass {

public:
Container<int> c; // Container 实例化为具体类型
};

模板模板参数的使用

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
#include <iostream>
#include <list>
#include <vector>

using namespace std;

// 定义类模板(使用了模板模板参数)
template <typename T, template <typename> typename Container>
class MyClass {
public:
MyClass() {
for (int i = 0; i < 10; ++i) {
// 插入数据
m_c.push_back(i);
}
}

public:
Container<T> m_c; // Container 作为一个类模板来使用,T 作为一个类型来使用
};

// 通过 using 定义别名模板
template <typename T>
using MyList = list<T, allocator<T>>;

// 通过 using 定义别名模板
template <typename T>
using MyVector = vector<T, allocator<T>>;

int main() {
MyClass<int, MyList> m1;
MyClass<int, MyVector> m2;
return 0;
}