一、引言
C++ 通过函数重载可以实现对不同数据类型完成同一功能,但是这样写重复的代码就显得很臃肿,且代码的复用率较低,每当出现一个 新类型,就需要增加对应的函数。
由此,引入了模板。所谓模板,顾名思义就是一个通用的描述。也就是使用泛型来定义函数,就是编写与类型无关的代码,其中泛型可通过具体的类型来(如 int 或 double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的的函数。
二、函数模板
2.1 函数模板格式
函数模板的基本格式如下:
1 2 3 4 5 6 7
| template<typename T1, typename T2, ... , typename Tn> return_type function_name(argument-list) { }
|
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| template <typename anyType> void Swap(anyType &a, anyType &b) { anyType temp; temp = a; a = b; b = temp; }
int main() { int i = 10, j = 20; Swap(i, j); cout << "i:" << i << " j:" << j << endl;
double a = 3.14, b = 2.718; Swap(a, b); cout << "a:" << a << " b:" << b << endl;
return 0; }
|
模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换 int 的函数时,编译器将按模板模式创建这样的函数,并用 int 代替 anyType。
这就是相当于创建了两个函数 void Swap(int &a, int &b) 和 void Swap(double &a, double &b)。
2.2 实例化
2.2.1 隐式实例化
隐式实例化(Implicit Instantiation)就是让编译器根据实参推演模板参数的实际类型。就像上面的例子那样,只管调用 Swap(i, j) 即可,具体的 i 和 j 的类型由编译器自行推导。
但若是这样调用:Swap(i, a) 编译就会报错,因为编译器无法辨认这里的 ansType 到底是 int 类型还是 double 类型。一个方法就是我们自己来强制转换,另一个方法就是下面要提到的显示实例化。
2.2.2 显式实例化
显示实例化(Explicit Instantiation)就是在函数名后的 <>中指定模板参数的实际类型,例如:
1 2
| Swap<int>(i, j); Swap<double>(a, b);
|
或者直接声明:
1 2
| template void Swap(int a, int b); template void Swap(double a, double b);
|
例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| template<typename T> T add(T x, T y) { return x + y; }
template int add(int, int); template double add(double, double);
int main() { cout << add(1, 2) << endl; cout << add(1.0, 2.0) << endl; return 0; }
|
这里会将 x 强制转换为 int 类型。那如果对 Swap 做类似的处理呢?
这里如果调用 Swap<int>(i, a) 则会报错。注意,这里的形参类型为 double &,所以不能指向 int 类型的变量 i。
2.3 特化
特化是指为模板提供一个或多个针对特定类型或特定类型组合的“特殊版本”的实现。当编译器需要实例化模板时,它会优先查找是否有更匹配的特化版本。如果有,就使用特化版本;如果没有,就使用通用模板。
特化是为了解决一个问题:通用模板的实现可能不适用于所有类型,或者对于某些特定类型,我们可以提供更优化、更高效、或具有特定行为的实现。
2.3.1 全特化
全特化(Full Specialization / Complete Specialization)是指为模板的所有模板参数都指定了具体的类型或值。它实际上是一个完全独立于通用模板的、同名的类或函数。
语法规则:
template <>:表示这是一个完全特化的版本,不再接受任何模板参数。
- 后面紧跟模板名和所有具体的类型参数。
例:
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 54 55 56 57 58 59
| template <typename T, typename U> class MyPair { public: T first; U second; MyPair(T f, U s) : first(f), second(s) { std::cout << "通用 MyPair 构造" << std::endl; } };
template <> class MyPair<int, double> { public: int first; double second; MyPair(int f, double s) : first(f), second(s) { std::cout << "MyPair<int, double> 全特化构造" << std::endl; } void specialMethod() { std::cout << "<int, double> 全特化处理!" << std::endl; } };
template <typename T> void printType(T arg) { std::cout << "通用版本: " << typeid(arg).name() << std::endl; }
template <> void printType<const char*>(const char* arg) { std::cout << "C 字符串特化版本: " << arg << std::endl; }
int main() { MyPair<char, bool> p1('A', true); MyPair<int, double> p2(1, 3.14); p2.specialMethod();
printType(10); printType(3.14f); printType("Hello C++");
return 0; }
|
全特化版本不再是模板,它是一个具体的类或函数。它拥有与通用模板相同的名称,但在模板参数列表处指定了具体的类型。
注意:编译器在实例化时,如果找到一个完全匹配的全特化版本,会优先选择它。
2.3.2 偏特化
偏特化(Partial Specialization)是指为模板的部分模板参数指定了具体的类型,或者对模板参数进行了限制。它本身仍然是一个模板,只是比通用模板更具体。
注意:偏特化只能应用于类模板,不能应用于函数模板。 函数模板可以通过函数重载来实现类似偏特化的效果。
语法规则:
template <...>:仍然接受模板参数,但参数列表会比通用模板的更少或更受限制。
- 后面紧跟模板名和部分具体的类型参数,或者带有约束的类型参数。
例:
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
| template <typename T, typename U> class MyPair { public: MyPair(T f, U s) { std::cout << "通用 MyPair 构造 (T, U)" << std::endl; } };
MyPair<T, int> 的偏特化版本:第二个模板参数固定为 int template <typename T> class MyPair<T, int> { public: MyPair(T f, int s) { std::cout << "MyPair<T, int> 偏特化构造" << std::endl; } void processInt() { std::cout << "对第二个参数为 int 的特殊处理." << std::endl; } };
template <typename T, typename U> class MyPair<T*, U> { public: MyPair(T* f, U s) { std::cout << "MyPair<T*, U> 偏特化构造" << std::endl; } void processPointer() { std::cout << "对第一个参数为指针的特殊处理." << std::endl; } };
int main() { MyPair<char, double> p1('A', 3.14); MyPair<float, int> p2(1.23f, 42); p2.processInt(); MyPair<double*, char> p3(nullptr, 'X'); p3.processPointer(); return 0; }
|
C++ 标准有明确的“偏序规则”来决定哪个特化更具体,见下例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| int main() { MyPair<int*, int> p5(new int(10), 20); p5.processPointer();
return 0; }
|
我在测试的时候会报错:类"MyPair<int *, int>" 没有成员 "processPointer",由此可见这个的规则匹配的是 class MyPair<T, int>。
偏特化版本仍然是模板,它有自己的模板参数列表。它通过指定部分参数、参数类型(如指针、引用、数组)、或参数数量来比通用模板更具体。编译器在实例化时,会根据一个称为“模板参数推导和重载解析”的复杂规则,选择“最具体”的模板版本。匹配优先级为:全特化版本 > 任何偏特化版本 > 通用模板。
2.3 模板参数的匹配规则
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。大致过程如下。
- 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用 float 参数的函数调用可以将该参数转换为 double,从而与 double 形参匹配,而模板可以为 float 生成一个实例
- 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错
对类模板而言,优先级排列为:显式实例化 > 全特化 > 偏特化 > 隐式实例化
对于函数模板,匹配规则为:普通函数 > 全特化 > 显式实例化 > 隐式实例化
参数匹配过程中,从最佳到最差的顺序如下所述:
- 完全匹配,但常规函数优先于模板
- 提升转换(例如,char 和 short 自动转换为int,float 自动转换为 double)
- 标准转换(例如,int 转换为 char,long 转换为 double)
- 用户定义的转换,如类声明中定义的转换
其中完全匹配允许的无关紧要转换:
| 从实参 |
到形参 |
| Type |
Type & |
| Type & |
Type |
| Type [] |
* Type |
| Type (argument-list) |
Type (*) (argument-list) |
| Type |
const Type |
| Type |
volatile Type |
| Type * |
cosnt Type |
| Type * |
volatile Type * |
三、类模板
类模板是对成员数据类型不同的类的抽象,它说明了类的定义规则,一个类模板可以生成多种具体的类。与函数模板的定义形式类似, 类模板也是使用template关键字和尖括号“<>”中的模板形参进行说明,类的定义形式与普通类相同。
1 2 3 4 5
| template<class T1, class T2, ..., class Tn> class 类模板名 { };
|
例:
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
| template<class T1> class Stack { public: Stack(int capacity = 4) :_a(new T1[capacity]) ,_capacity(capacity) ,_size(0) {} void Push(T1 data) { _a[_size] = data; _size++; } ~Stack() { delete[]_a; _a = nullptr; _capacity = _size = 0; } private: T1* _a; int _capacity; int _size; }; int main() { Stack<int> s1; Stack<double> s2; return 0; }
|