模板 (template)#
函数模板#
通过 函数, 我们可以用不同的值运行同一段代码:
1int max(int lhs, int rhs) {
2 return lhs > rhs ? lhs : rhs;
3}
4
5max(5, 4) == 5;
6max(1, 4) == 4;
但仅仅是允许同一类型不同的值复用代码是不够的:
1char max(char lhs, char rhs);
2int max(int lhs, int rhs);
3double max(double lhs, double rhs);
4// ...
模板的目的即是为 任意类型 或 编译时可确定的值 生成可用的代码:
1template <typename T>
2T max(T lhs, T rhs) {
3 return lhs > rhs ? lhs : rhs;
4}
5
6max(5, 4) == 5; // 生成 max<int> 函数, 然后调用
7max(1.0, 2.0) == 2.0; // 生成 max<double> 函数, 然后调用
此时的传参和传返回值同样可以当作变量声明来理解, 则类型模板参数 T
对应于 auto:
1auto lhs = a;
2auto rhs = b;
3auto result = lhs > rhs ? lhs : rhs;
1template <typename T>
2T max(T lhs, T rhs) {
3 return lhs > rhs ? lhs : rhs;
4}
5auto result = max(a, b);
与 auto
一样, T
可以添加 const
、&
和 *
等修饰符:
1template <typename T>
2T max(T const& lhs, T const& rhs) {
3 return lhs > rhs ? lhs : rhs;
4}
5max(5, 4); // 推导 T 为 int, 则 lhs 为 int const&
但要注意, 由于我们只写了 <typename T>
一个模板参数, T
只能被推导为一种类型:
1template <typename T>
2T max(T const& lhs, T const& rhs) {
3 return lhs > rhs ? lhs : rhs;
4}
5
6max(1.0, 4); // 错误: T 类型推导发生冲突
7 // - 1.0 -> double
8 // - 4 -> int
这没什么, 我们可以要求多个模板参数:
1template <typename Return, typename T, typename U>
2Return max(T const& lhs, U const& rhs) {
3 return lhs > rhs ? lhs : rhs;
4}
可这样一来, 返回值呢? 调用函数时没有参数对应它, 它没法自动推导:
1max(5.0, 4); // T -> double, U -> int
2 // 错误: 无法推导模板参数 Return
我们可以显式地指明它:
1max<double>(5.0, 4); // Return -> double
或者我们其实不是想让返回值作为独立的模板参数, 只是不知道它的类型到底是什么. 还记得 auto
吗?
1template <typename T, typename U>
2auto max(T const& lhs, U const& rhs) {
3 return lhs > rhs ? lhs : rhs;
4}
5
6max(5.0, 4);
当然了, 模板的生成要求类型确实支持相应的操作:
1int value = 5;
2int* pointer = &value;
3max(value, pointer); // 错误: int 和 int* 不可进行 > 比较
解释上的遗憾
你可能会想, 既然返回值能使用 auto
, 那参数呢?
1auto max(auto const& lhs, auto const& rhs) {
2 return lhs > rhs ? lhs : rhs;
3}
4
5// 相当于:
6template <typename T, typename U>
7auto max(T const& lhs, U const& rhs) {
8 return lhs > rhs ? lhs : rhs;
9}
我更倾向的解释思路其实是先使用 auto
让新手能使用模板, 再在合适时候引入 template <...>
这样的语法. 遗憾的是, 这是 C++20 才有的语法, 与教学使用的 C++98 和你通过 Visual Studio 默认能用的 C++11 相比太超前了.
类模板#
我们除了可以为函数编写模板, 也可以为类编写模板.
例如要实现数学上的复数, 我们可能需要不同的精度.
1template <typename T>
2class Complex {
3 public:
4 Complex(T real, T imaginary);
5 // ...
6
7 private:
8 T real_;
9 T imaginary_;
10};
11
12Complex<double> value;
遗憾的是, 在 C++17 以前, 我们没办法根据构造函数推导类模板的模板参数:
1Complex value(10.0, 5.0); // C++17 以前错误: 无法推导类模板的参数类型
2 // C++17 及以后正确: T -> double
为此你可以编写一个函数模板来进行推导:
1template <typename T>
2Complex make_complex(T real, T imaginary) {
3 return Complex<T>(real, imaginary);
4}
5
6auto value1 = make_complex(1, 0); // Complex<int>
7auto value2 = make_complex(10.0, 5.0); // Complex<double>
而模板参数除了是类型外, 还可以是编译时可确定的值.
1template <typename T, int Size>
2class Array {
3 public:
4 T& operator[](int index) {
5 return data_[index];
6 }
7 T const& operator[](int index) const {
8 return data_[index];
9 }
10 int size() const {
11 return Size;
12 }
13
14 T data_[Size];
15};
这不就是包装了一下数组吗, 能有什么用? Array
是一个新的类型而不是数组类型, 它不会隐式类型转换为首元素的指针.
- 不会隐式类型转换为首元素的指针, 所以跟
int
没什么区别, 是值语义. 1void by_copy(Array<int, 3> array); 2void by_reference(Array<int, 3>& array); 3 4Array<int, 3> array = {0, 1, 2}; 5Array<int, 3> another = array; // 拷贝得一个新的对象 6Array<int, 3>& reference = array; // 引用对象 7 8by_copy(array); // 拷贝传参 9by_reference(array); // 引用传参
- 不会隐式类型转换为首元素的指针, 所以始终知道自己的长度.
1template <typename T, int Size> 2void print(Array<T, Size> const& array) { 3 for (int i = 0; i < array.size(); ++i) { 4 std::cout << array[i] << ' '; 5 } 6 std::cout << '\n'; 7}
- 不会隐式类型转换为首元素的指针, 所以没有
array + 0
这种恶心的运算. 1Array<int, 3> array = {0, 1, 2}; 2auto value = array + 0; // 错误: 不支持 Array<int, 3> + int
事实上, C++11 后即在 <array>
中提供了这样的一个类 std::array<T, Size>
.
1#include <array>
2
3template <typename T, int Size>
4void print(std::array<T, Size> const& array) {
5 for (int i = 0; i < array.size(); ++i) {
6 std::cout << array[i] << ' ';
7 }
8 std::cout << '\n';
9}
10
11int main() {
12 std::array<int, 3> array = {0, 1, 2};
13 print(array);
14}