模板 (template)

模板 (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* 不可进行 > 比较

类模板#

我们除了可以为函数编写模板, 也可以为类编写模板.

例如要实现数学上的复数, 我们可能需要不同的精度.

 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}

相关解答#