template:泛型的蓝图
template 关键字用来声明一个模板,你可以把它想象成一个“配方”或“蓝图”,它定义了一类函数或类,但其中某些部分(通常是数据类型)是“待定”的,需要在实际使用时才能确定。

(图片来源网络,侵删)
a) 函数模板
当你想要编写一个函数,这个函数的逻辑对多种数据类型都适用时,就可以使用函数模板。
示例:一个通用的 swap 函数
// T 是一个模板参数,代表一个待定的类型
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
swap(x, y); // 编译器在这里实例化一个 swap<int>(int&, int&)
std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
std::string s1 = "hello", s2 = "world";
std::cout << "Before swap: s1 = " << s1 << ", s2 = " << s2 << std::endl;
swap(s1, s2); // 编译器在这里实例化一个 swap<std::string>(std::string&, std::string&)
std::cout << "After swap: s1 = " << s1 << ", s2 = " << s2 << std::endl;
return 0;
}
工作原理:
- 当编译器遇到
swap(x, y)时,它会看到x和y是int类型。 - 它根据
swap的模板“蓝图”,自动生成一个专门处理int类型的具体函数,这个过程称为模板实例化。 - 同理,当遇到
swap(s1, s2)时,编译器又会生成一个专门处理std::string类型的具体函数。
T 被称为模板参数,它必须在尖括号 <...> 中声明。

(图片来源网络,侵删)
b) 类模板
当你想要定义一个类,这个类的数据成员或成员函数的实现依赖于一个或多个数据类型时,就需要使用类模板。
示例:一个简单的栈容器
#include <vector>
#include <stdexcept>
template <typename T>
class Stack {
private:
std::vector<T> elements; // 使用 std::vector 作为底层容器
public:
void push(const T& element) {
elements.push_back(element);
}
void pop() {
if (elements.empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
elements.pop_back();
}
T top() const {
if (elements.empty()) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elements.back();
}
bool empty() const {
return elements.empty();
}
};
int main() {
Stack<int> intStack; // 实例化一个 Stack<int> 类
intStack.push(10);
intStack.push(20);
std::cout << "Top of intStack: " << intStack.top() << std::endl; // 输出 20
Stack<std::string> stringStack; // 实例化一个 Stack<std::string> 类
stringStack.push("Hello");
stringStack.push("Templates!");
std::cout << "Top of stringStack: " << stringStack.top() << std::endl; // 输出 "Templates!"
return 0;
}
工作原理:
Stack<int>告诉编译器,请基于Stack模板创建一个类,其中所有的T都被替换为int。Stack<std::string>则创建另一个类,其中所有的T都被替换为std::string。Stack<int>和Stack<std::string>是两个完全不同的类型,它们之间不能相互赋值或操作。
typename:模板参数的声明符
在模板的声明中,typename 和 class 几乎可以互换使用,都用来声明一个类型参数。

(图片来源网络,侵删)
// 以下两种写法是等价的
template <typename T> // 使用 typename
class MyClass { ... };
template <class T> // 使用 class
class MyClass { ... };
为什么会有两个关键字?
class是 C++ 早期版本引入的,因为它更符合“类模板”的概念。typename是后来为了更清晰、更准确地表达“这是一个类型”而引入的,在模板参数列表中,推荐使用typename,因为它在更复杂的上下文中具有唯一的意义。
typename 的核心用途:在模板内部指定“这是一个类型”
这是 typename 最重要、也是最容易被忽略的用法,在模板定义的内部,编译器有时无法确定一个名字(MyClass<T>::value_type)是一个类型还是一个静态成员变量。
示例:一个迭代器适配器
假设我们想写一个模板,它能接受一个迭代器,并获取该迭代器指向的值的类型。
#include <vector>
#include <list>
#include <iostream>
// 假设我们有一个自定义迭代器
template <typename T>
struct MyIterator {
using value_type = T; // C++11 推荐的内部类型别名写法
// ... 其他迭代器成员 ...
};
// 模板函数,打印容器中元素的类型
template <typename Iterator>
void print_element_type(Iterator it) {
// 错误写法:
// Iterator::value_type x;
// 在模板内部,编译器看到 Iterator::value_type,它不知道 value_type 是一个类型还是一个静态成员变量。
// C++ 规定,当编译器无法确定时,默认它是一个对象(非类型)。
// 必须用 typename 明确告诉编译器:“嘿,这里的 Iterator::value_type 是一个类型!”
// 正确写法:
typename Iterator::value_type x;
std::cout << "Element type is: " << typeid(x).name() << std::endl;
}
int main() {
std::vector<int> v;
print_element_type(v.begin()); // std::vector<int>::iterator 的 value_type 是 int
std::list<double> l;
print_element_type(l.begin()); // std::list<double>::iterator 的 value_type 是 double
MyIterator<std::string> my_it;
print_element_type(my_it); // MyIterator<std::string> 的 value_type 是 std::string
return 0;
}
规则:
在模板定义中,如果依赖于模板参数的名称是一个类型,并且它被用作一个类型限定符()的前缀,那么这个类型名称前必须加上 typename。
例外情况:
- 在基类列表中或成员初始化列表中,
typename是可选的。template <typename T> class Derived : public Base<T> { // 这里的 Base<T> 不需要 typename public: Derived() : Base<T>() {} // 这里的 Base<T> 也不需要 typename }; - 在构造函数的成员初始化列表中,
typename是可选的。 - 如果类型名是
template和typename的组合,情况会更复杂,但核心思想相同。
template 和 typename 的高级用法
a) 非类型模板参数
模板参数不仅可以是类型,还可以是常量表达式(整数、指针、引用等)。
// N 是一个非类型模板参数,必须在编译时确定
template <typename T, int N>
class Array {
private:
T data[N]; // 使用 N 来声明数组大小
public:
T& operator[](int index) {
if (index < 0 || index >= N) {
throw std::out_of_range("Index out of bounds");
}
return data[index];
}
int size() const { return N; }
};
int main() {
Array<int, 100> intArray; // 一个可以存储100个int的数组
intArray[0] = 42;
std::cout << "Array size: " << intArray.size() << std::endl; // 输出 100
// Array<double, 10.5> doubleArray; // 错误!10.5 不是整数常量表达式
// Array<int, dynamic_size> dynamic; // 错误!dynamic_size 不是编译时常量
return 0;
}
b) 默认模板参数
可以为模板参数提供默认值。
template <typename T = int, int N = 100>
class DefaultArray {
// ...
};
int main() {
DefaultArray<> arr1; // 使用默认参数 T=int, N=100
DefaultArray<double> arr2; // T=double, N=100 (N 使用默认值)
DefaultArray<double, 50> arr3; // T=double, N=50
return 0;
}
| 关键字 | 主要用途 | 示例 | 核心思想 |
|---|---|---|---|
template |
声明一个泛型函数或类,定义一个“蓝图”。 | template <typename T> void foo(T t); |
编写与数据类型无关的通用代码。 |
typename |
在模板参数列表中声明一个类型参数(与 class 等价)。在模板定义内部,明确指出一个依赖于模板参数的名字是一个类型。 |
template <typename T>typename T::iterator it; |
定义模板的“类型占位符”。 消除编译器在模板内部对“类型”和“变量”的歧义。 |
简单记忆:
template <...>是写在函数或类定义的最前面,告诉编译器“我要开始写一个泛型了”。typename是写在模板函数或类的内部,用来解决“这个依赖模板参数的名字到底是不是个类型?”的问题,在模板参数列表里,它只是用来声明一个类型参数。
