这一部分先整理 C++ 模板基础。
模板是 C++ 泛型编程的核心。它让我们写一份逻辑,适配多种类型;但模板不是运行时的“万能函数”,而是编译期根据实际类型生成对应代码。理解这一点,后面看 STL、容器、迭代器和模板错误信息会轻松很多。
1.0 泛型编程概念 泛型编程关注的是“算法和类型分离”。同样的逻辑,不希望为 int、double、std::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 + b、a > b、std::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.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.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 ; }
复盘时重点看:
max_value(1, 2) 推导出什么类型。
max_value<double>(1, 2.5) 为什么需要显式指定。
StaticBuffer<> 使用了哪些默认模板参数。
StaticBuffer<std::string, 2> 生成的是另一个类模板实例。
模板内部的 >、赋值、数组存储对类型有什么要求。
常见错误整理
错误
示例
问题
以为模板是运行时泛型
把模板当虚函数用
模板主要在编译期实例化
模板参数推导冲突
add(1, 2.5)
单一 T 无法同时推导为 int 和 double
类型不支持操作
模板里用了 >,类型没有重载
实例化时报错
滥用特化
每个类型都写特化
模板设计失去统一性
忘记默认模板参数含义
Buffer<> 看不懂类型
要回看模板定义
类模板忘记指定类型
Box box(10); 在旧标准下不成立
类模板通常要写 Box<int>
模板错误只看最后一行
编译器报错很长
要从实例化类型和模板调用点往回看
学习检查清单
检查项
状态
能解释泛型编程为什么要把类型参数化
待复盘
能写出一个函数模板
待复盘
能说明模板实例化的大致过程
待复盘
能写出一个函数模板特化示例
待复盘
能使用默认模板参数
待复盘
能写出多个模板参数的函数或类
待复盘
能解释模板参数对类型能力的要求
待复盘
能区分自动推导和显式指定模板参数
待复盘
能说明普通函数和模板函数同时存在时的大致匹配关系
待复盘
能定义类模板并实例化不同类型版本
待复盘
能把 vector<T>、map<K,V> 看成类模板实例
待复盘
这一部分的核心是:模板让类型变成参数,编译器根据实际类型生成代码。后面学 STL 容器时,先记住它们本质上都是围绕模板展开的类型工具。