1. 概述
C++模板是一种支持参数化多态的工具,允许编写与类型无关的代码(泛型代码)。模板是泛型编程的基础。
示例
1
2
3
4
5
6
7
8
template <typename T> // 定义模板类型名称 T
T max_(T a, T b) { // 函数接受T类型参数,返回T类型
return (a>b) ? a : b;
}
int main() {
cout << max_(3.14, 2.14);
}
在这段代码中,我们定义了一个函数max_。这是一个运用了模板语法的泛型函数,它接受两个类型为T的参数,并返回两者之间的较大值。观看max_的实现,我们可以发现,类型T必须支持二元运算>,从而进行a和b之间的比较;同时,任何支持>运算的类型T,如整数、浮点数甚至自定义迭代器对象等等,均可以输入到max_中求出较大值。
泛型编程思想的引入极大地减少了因不同类型所需重载函数的代码量(请参见Golang泛型梗图),在不影响类型安全的情况下提高了程序的开发效率。
2. 函数模板的语法
如上例,我们使用template <typename T>关键字置于模板函数上方来指定类型在函数内部的名称。一般来说,这一名称应当用简短的大写字母与数字表示。
示例
1
2
3
4
template<typename T1, typename T2>
void print_one_by_one(T1 item1, T2 item2) {
std::cout << item1 << " " << item2 << std::endl;
}
我们在这里定义了两个类型模板T1和T2,并将其作为两个参数的类型,将两个参数一一打印出来。观看其实现,可知任何实现了friend std::ostream& operator<<(ostream& out, const List& self),也就是可以和std::out进行«运算的类型均可以输入到函数中进行打印。
类型T1和T2具体是什么,编译器会在函数得到具体输入时进行确认。我们也可以手动确定T1和T2的类型,更快地找出实际类型与预期类型的冲突,增强类型安全性,如:
1
2
3
int main() {
print_one_by_one<int, float>(3, 3.14);
}
这一语法称为函数模板的全特化,意思是把所有不确定的类型全部进行特殊化处理
3. 类模板的语法
把template <typename T>至于模板类的上方,即可指定类型在类内部的名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T>
class Stack {
private:
vector<T> elements; // vector类本身是模板类,需设定其元素为T
public:
void push(const T& elem) { // T 作为参数的例子
elements.push_back(elem);
}
T pop() { // T 作为返回值的例子
T elem = elements.back();
elements.pop_back();
return elem;
}
};
int main() {
Stack<int> intStack; // 使用 int 来作为 T
Stack<string> stringStack; // 使用 string 来作为 T
}
在类的实现中,模板参数T可以作为方法的参数、方法的返回值、属性的类型,甚至注入到其它模板类中,使用方法极其灵活。
4. 类模板的全特化和偏特化
与函数模板不同的是,类模板支持只将部分模板参数进行特化,这被称为类模板的偏特化
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
// 通用模板
template <typename T1, typename T2>
class Pair {
public:
void describe() {
cout << "通用Pair: (" << typeid(T1).name()
<< ", " << typeid(T2).name() << ")" << endl;
}
};
// 偏特化1:当两个类型相同时
template <typename T>
class Pair<T, T> {
public:
void describe() {
cout << "相同类型Pair: 两个" << typeid(T).name() << "类型" << endl;
}
};
// 偏特化2:当第二个类型是int时
template <typename T>
class Pair<T, int> {
public:
void describe() {
cout << "特殊Pair: " << typeid(T).name() << " 和 int" << endl;
}
};
// 使用示例
void demoPartialSpecialization() {
Pair<int, double> p1; // 使用通用模板
p1.describe();
Pair<float, float> p2; // 使用相同类型偏特化
p2.describe();
Pair<string, int> p3; // 使用第二个是int的偏特化
p3.describe();
}
如此,将部分不确定的模板参数进行特化的语法,称为偏特化。偏特化可以让我们识别特定的类型组合,并进行灵活的、有区分度的后续操作。
在上面的偏特化过程中,编译器会自动识别使用的参数类型并匹配相应的偏特化实现。更进一步地,编译器可以识别比上述int等更加复杂的类型,如指针T*、引用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
// 通用模板
template <typename T>
class Container {
public:
static const char* name() { return "通用容器"; }
};
// 模式1:T[N] 数组
template <typename T, size_t N>
class Container<T[N]> {
public:
static const char* name() { return "静态数组容器"; }
};
// 模式2:T* 指针
template <typename T>
class Container<T*> {
public:
static const char* name() { return "指针容器"; }
};
// 模式3:const T
template <typename T>
class Container<const T> {
public:
static const char* name() { return "常量容器"; }
};
// 模式4:T& 引用
template <typename T>
class Container<T&> {
public:
static const char* name() { return "引用容器"; }
};
// 使用
void demoPatternMatching() {
Container<int> c1; // 通用
Container<int[5]> c2; // 数组模式
Container<int*> c3; // 指针模式
Container<const int> c4; // 常量模式
Container<int&> c5; // 引用模式
cout << c1.name() << endl;
cout << c2.name() << endl;
cout << c3.name() << endl;
cout << c4.name() << endl;
cout << c5.name() << endl;
}
5. 结语
C++模板是泛型编程的基石,它通过参数化多态机制,将算法与数据类型解耦,从而实现了代码的高度复用与类型安全。从简单的函数模板到复杂的类模板及其特化,模板系统展现了C++在抽象与效率之间寻求平衡的强大能力。
回顾本文的核心要点:
- 函数模板 允许我们编写与类型无关的通用算法,只需一次定义,即可适用于多种支持所需操作的类型。
- 类模板 将泛型思想扩展到数据结构,可以构建如
Stack<T>、Pair<T1, T2>等通用容器。 - 特化机制(全特化与偏特化)为模板提供了必要的灵活性。它允许我们为特定的类型或类型组合提供定制化的实现,这是模板元编程和编译期逻辑判断的关键。
模板的威力不仅在于减少代码重复。通过模式匹配(如匹配指针 T*、引用 T&、数组 T[N] 等),编译器能在编译期选择最合适的模板版本,这为编写高性能、类型安全的库(如STL)奠定了坚实基础。
掌握模板语法,意味着你能更深入地理解现代C++标准库的设计哲学,并能够自己设计出灵活、高效且健壮的泛型组件。从 max_ 函数到可匹配各种类型模式的 Container,模板的世界充满了挑战与乐趣,是每一位进阶C++开发者必须探索的领域。