这一部分先整理 C++ 模板基础。

模板是 C++ 泛型编程的核心。它让我们写一份逻辑,适配多种类型;但模板不是运行时的“万能函数”,而是编译期根据实际类型生成对应代码。理解这一点,后面看 STL、容器、迭代器和模板错误信息会轻松很多。

1.0 泛型编程概念

泛型编程关注的是“算法和类型分离”。同样的逻辑,不希望为 intdoublestd::string 分别写一遍。

比如交换两个变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
void swap_int(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}

void swap_double(double &a, double &b)
{
double temp = a;
a = b;
b = temp;
}

这两个函数逻辑完全一样,只是类型不同。模板可以把类型抽象出来。

1
2
3
4
5
6
7
template <typename T>
void my_swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}

泛型编程解决什么

问题 模板的作用
类型重复 一份逻辑适配多个类型
代码复用 减少重复函数和类
编译期检查 类型不合适时编译期报错
性能 通常不需要运行时类型分发

模板的核心是“把类型当参数”。普通函数的参数是值,模板参数可以是类型。

1.1 函数模板实例

函数模板的基本写法:

1
2
3
4
5
template <typename T>
T add(T left, T right)
{
return left + right;
}

使用示例:

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

template <typename T>
T add(T left, T right)
{
return left + right;
}

int main()
{
std::cout << add<int>(1, 2) << std::endl;
std::cout << add<double>(1.5, 2.5) << std::endl;
std::cout << add(10, 20) << std::endl;

return 0;
}

add<int>add<double> 会生成不同类型版本的函数。

显式指定与自动推导

写法 说明
add<int>(1, 2) 显式指定 T = int
add<double>(1.5, 2.5) 显式指定 T = double
add(10, 20) 编译器根据实参推导 T = int

模板参数推导不是“猜”,而是根据函数调用时的实参类型做规则匹配。

1.2 模板底层原理

模板本身不是最终执行的函数。编译器会在使用模板时,根据实际类型生成对应代码,这叫模板实例化。

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
T max_value(T a, T b)
{
return a > b ? a : b;
}

int main()
{
int x = max_value(1, 2);
double y = max_value(1.5, 2.5);
return 0;
}

可以粗略理解为,编译器生成了两个版本:

1
2
3
4
5
6
7
8
9
int max_value_int(int a, int b)
{
return a > b ? a : b;
}

double max_value_double(double a, double b)
{
return a > b ? a : b;
}

模板的几个特点

特点 说明
编译期生成 不是运行时再决定类型
按需实例化 只有用到某个类型时才生成对应版本
错误可能很长 模板错误常常在实例化时爆出来
类型要求隐式存在 模板里用了 +,类型就必须支持 +

模板不是万能的。如果模板体里写了 a + ba > bstd::cout << a,那么传入的类型就必须支持这些操作。

1.3 函数模板特化

模板特化用于给某个特定类型写专门版本。

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 <cstring>

template <typename T>
bool is_equal(T left, T right)
{
return left == right;
}

template <>
bool is_equal<const char *>(const char *left, const char *right)
{
return std::strcmp(left, right) == 0;
}

int main()
{
std::cout << is_equal(10, 10) << std::endl;
std::cout << is_equal("abc", "abc") << std::endl;

return 0;
}

普通类型可以用 == 比较内容,但 C 字符串用 == 比较的是地址,不是字符串内容。所以对 const char * 做特化。

特化适合什么

场景 说明
某个类型不能用通用逻辑 C 字符串比较
某个类型需要优化 特定结构有更快算法
某个类型语义特殊 需要专门处理资源或边界

特化不要滥用。能用统一接口解决时,尽量保持模板逻辑简单。

1.4 默认模板参数

模板参数可以有默认值。

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

template <typename T = int>
T zero()
{
return T{};
}

int main()
{
std::cout << zero<>() << std::endl;
std::cout << zero<double>() << std::endl;

return 0;
}

类模板中默认模板参数更常见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T = int, int Size = 10>
class Buffer
{
public:
T data[Size]{};
};

int main()
{
Buffer<> default_buffer;
Buffer<char, 32> char_buffer;

return 0;
}

默认参数的意义

作用 说明
降低使用成本 常见场景不用每次都写参数
保留扩展能力 特殊场景可以显式指定
表达默认策略 比如默认类型、默认大小、默认比较器

1.5 多个模板参数

模板可以有多个类型参数。

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

template <typename Key, typename Value>
void print_pair(const Key &key, const Value &value)
{
std::cout << key << " => " << value << std::endl;
}

int main()
{
print_pair(1, "admin");
print_pair(std::string("port"), 8080);

return 0;
}

也可以混合类型参数和非类型参数。

1
2
3
4
5
6
template <typename T, int Size>
class StaticArray
{
public:
T data[Size]{};
};

多模板参数复盘点

  1. 每个模板参数承担什么含义。
  2. 哪些参数可以推导,哪些需要显式指定。
  3. 非类型模板参数通常在编译期确定。
  4. 参数越多,可读性和错误信息也可能越复杂。

1.6 函数模板拓展

函数模板可以结合引用、const、返回类型推导等写法。

使用引用避免复制

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

template <typename T>
const T &max_ref(const T &left, const T &right)
{
return left > right ? left : right;
}

int main()
{
std::string a = "alice";
std::string b = "bob";

std::cout << max_ref(a, b) << std::endl;
return 0;
}

传大型对象时,const T & 可以避免复制,也能避免函数修改外部对象。

返回类型推导

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

template <typename T, typename U>
auto add(T left, U right)
{
return left + right;
}

int main()
{
auto result = add(1, 2.5);
std::cout << result << std::endl;
return 0;
}

auto 返回值由 left + right 的结果推导出来。

函数模板写得越通用,越要清楚它对类型有什么要求。比如 max_ref 要求类型支持 >add 要求类型支持 +

1.7 模板参数限定

早期 C++ 中,模板参数的限制通常靠模板体里的表达式自然触发编译错误。

1
2
3
4
5
template <typename T>
T add(T left, T right)
{
return left + right;
}

如果某个类型不支持 +,实例化时就会报错。

C++20 引入 concepts 后,可以更清楚地表达模板参数要求。

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

template <typename T>
requires std::integral<T>
T add_integer(T left, T right)
{
return left + right;
}

int main()
{
std::cout << add_integer(1, 2) << std::endl;
return 0;
}

复盘时先抓住这个点

模板参数不是随便传什么都行。模板里使用了什么操作,类型就必须满足什么能力。

模板代码 类型必须支持
a + b 加法
a > b 比较
std::cout << a 输出运算符
a.size() 成员函数 size
a[i] 下标访问

1.8 模板调用规则

模板调用时,编译器会先尝试推导模板参数。如果推导失败,可能需要显式指定。

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

template <typename T>
T add(T left, T right)
{
return left + right;
}

int main()
{
std::cout << add(1, 2) << std::endl;
std::cout << add<double>(1, 2.5) << std::endl;

return 0;
}

add(1, 2.5) 对单一模板参数 T 来说,一个参数是 int,一个参数是 double,推导会有冲突。显式写 add<double> 可以让两个参数都转换到 double

普通函数和模板函数

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

void print(int value)
{
std::cout << "normal: " << value << std::endl;
}

template <typename T>
void print(T value)
{
std::cout << "template: " << value << std::endl;
}

int main()
{
print(10);
print(3.14);
return 0;
}

通常情况下,如果普通函数和模板函数都能匹配,编译器会优先选择更合适的普通函数。

调用规则复盘

  1. 能自动推导时,可以省略模板参数。
  2. 推导冲突时,可以显式指定模板参数。
  3. 普通函数与模板函数同时存在时,要看匹配优先级。
  4. 模板错误往往要从“本次实例化用了什么类型”开始查。

1.9 类模板定义与实例化

类模板用于定义可适配多种类型的类。

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

template <typename T>
class Box
{
public:
explicit Box(const T &value)
: value_(value)
{
}

const T &get() const
{
return value_;
}

private:
T value_;
};

int main()
{
Box<int> int_box(10);
Box<double> double_box(3.14);

std::cout << int_box.get() << std::endl;
std::cout << double_box.get() << std::endl;

return 0;
}

类模板使用时通常需要显式指定类型参数,比如 Box<int>

类模板实例化

Box<int>Box<double> 是两个不同的类型。

写法 含义
Box<int> 保存 int 的 Box 类型
Box<double> 保存 double 的 Box 类型
Box<std::string> 保存字符串对象的 Box 类型

类模板和安全开发

STL 容器本质上就是大量类模板,例如:

1
2
3
std::vector<int>
std::vector<std::string>
std::map<std::string, int>

理解类模板后,再看 vector<T>list<T>map<K, V> 就不只是“会用容器”,而是能理解它们为什么可以适配不同类型。

本章最小复盘代码

这段代码把函数模板、类模板、默认模板参数和模板调用规则放在一起。

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

template <typename T>
T max_value(T left, T right)
{
return left > right ? left : right;
}

template <typename T = int, int Size = 4>
class StaticBuffer
{
public:
void set(int index, const T &value)
{
if (index < 0 || index >= Size)
{
return;
}

data_[index] = value;
}

const T &get(int index) const
{
return data_[index];
}

int size() const
{
return Size;
}

private:
T data_[Size]{};
};

int main()
{
std::cout << max_value(1, 2) << std::endl;
std::cout << max_value<double>(1, 2.5) << std::endl;

StaticBuffer<> int_buffer;
int_buffer.set(0, 100);

StaticBuffer<std::string, 2> name_buffer;
name_buffer.set(0, "Ruiqy");

std::cout << int_buffer.get(0) << std::endl;
std::cout << name_buffer.get(0) << std::endl;

return 0;
}

复盘时重点看:

  1. max_value(1, 2) 推导出什么类型。
  2. max_value<double>(1, 2.5) 为什么需要显式指定。
  3. StaticBuffer<> 使用了哪些默认模板参数。
  4. StaticBuffer<std::string, 2> 生成的是另一个类模板实例。
  5. 模板内部的 >、赋值、数组存储对类型有什么要求。

常见错误整理

错误 示例 问题
以为模板是运行时泛型 把模板当虚函数用 模板主要在编译期实例化
模板参数推导冲突 add(1, 2.5) 单一 T 无法同时推导为 intdouble
类型不支持操作 模板里用了 >,类型没有重载 实例化时报错
滥用特化 每个类型都写特化 模板设计失去统一性
忘记默认模板参数含义 Buffer<> 看不懂类型 要回看模板定义
类模板忘记指定类型 Box box(10); 在旧标准下不成立 类模板通常要写 Box<int>
模板错误只看最后一行 编译器报错很长 要从实例化类型和模板调用点往回看

学习检查清单

检查项 状态
能解释泛型编程为什么要把类型参数化 待复盘
能写出一个函数模板 待复盘
能说明模板实例化的大致过程 待复盘
能写出一个函数模板特化示例 待复盘
能使用默认模板参数 待复盘
能写出多个模板参数的函数或类 待复盘
能解释模板参数对类型能力的要求 待复盘
能区分自动推导和显式指定模板参数 待复盘
能说明普通函数和模板函数同时存在时的大致匹配关系 待复盘
能定义类模板并实例化不同类型版本 待复盘
能把 vector<T>map<K,V> 看成类模板实例 待复盘

这一部分的核心是:模板让类型变成参数,编译器根据实际类型生成代码。后面学 STL 容器时,先记住它们本质上都是围绕模板展开的类型工具。