在之前的文章中,介绍了模板初阶:Cpp_桀桀桀桀桀桀的博客-CSDN博客
在本篇中将会对模板进一步的讲解。本篇中的主要内容为:非类型模板参数、函数模板的特化、类模板的特化(其中包含全特化和偏特化),最后讲解了模板的分离编译问题,以及出现链接错误的原因。
目录如下:
目录
1. 非类型模板参数
1.1 模板的按需实例化
2. 模板的特化
2.1 特化的概念
2.2 函数模板特化
2.3 类模板特化
3. 模板的分离编译
3.1 模板的分离编译及其原理
1. 非类型模板参数
模板参数分为:类类型形参和非类型形参。
类型形参:出现在模板参数列表中,跟在 class 或者 typename 之类的参数类型名称之后。
非类型形参:使用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
如下所示的两份代码:
namespace MyArray1 { // 使用宏定义 #define N 10 // 定义一个模板类型的静态数组 template<class T> class array { public: T& operator[](size_t index) { return _array[index]; } const T& operator[](size_t index)const { return _array[index]; } size_t size()const { return _size; } bool empty()const { return 0 == _size; } private: T _array[N]; size_t _size; }; } namespace MyArray2 { // 使用宏定义 // 定义一个模板类型的静态数组 template<class T, size_t N = 10> class array { public: T& operator[](size_t index) { return _array[index]; } const T& operator[](size_t index)const { return _array[index]; } size_t size()const { return _size; } bool empty()const { return 0 == _size; } private: T _array[N]; size_t _size; }; }
如上所示,当我们想要写一个常量数组类,我们可以使用两种方法,一种是使用宏定义来确定数组的大小,另一种是使用非类型模板参数来充当数组的大小,我们在使用非类型模板参数的时候,也可以像使用函数参数一样,给一个缺省值。
关于使用其他的非类型模板参数,我们的非类型模板参数只能使用整型做模板参数,对于其他非类型的模板参数,只有到 C++20 标准之后才可以使用。如下:
关于类(函数)模板传参与函数传参的时刻:对于类模板传参而言,传参的时候是在编译阶段,因为模板属于一个半成品,在编译阶段需要使用传入的参数来生成确定的类(代码);而对于函数传参而言,函数传参是在运行时传参,将我们需要运行的参数传入函数中进行计算。
1.1 模板的按需实例化
在模板实例化的时候,并不会检测语法错误,如下:
我们在函数中调用 assert 函数和 size 函数都出现了语法错误,但是我们在生成解决方案的时候却可以通过,这是因为对于模板而言,这是一个半成品,在语法编译的的时候,因为并没有调用类函数,所以并不会检测语法。
当我们实例化一个对象的时候,是否会检测出错误呢?如下:
当我们实例化一个对象的时候,调用其中一个函数的时候,也还是不会检测出错误,这是因为按需实例化,不仅仅是类的按需实例化,类函数也是按需实例化,只有当调用的函数出现错误的时候,才会检测出来。
对于模板实例化的步骤为:根据模板实例化 --> 半成品模板 --> 实例化成具体的类/函数 --> 语法编译。
2. 模板的特化
2.1 特化的概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要我们进行特殊的处理,如下:
如上所示,当使用指针进行比较的时候,原本应该输出为0,但是却输出为1,这是因为在 Less 中并没有比较指针指向的内容重载函数,而是直接的比较指针的大小,所以输出的结果显示错误。
这个时候,我们就需要对模板进行特化,即:在原模板的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。
2.2 函数模板特化
函数模板特化的步骤:
1. 必须要现有一个基础的函数模板;
2. 关键字 template 后面接一个空的<>;
3. 函数名后面跟一对尖括号,尖括号内指定需要特化的类型;
4. 函数形参必须要和模板函数的基础参数类型完全相同,如果不同,编译器可能会薄一些奇怪的错误。
如下:
template<class T> bool Less(T x, T y) { return x < y; } // 函数模板特化 template<> bool Less<Date*>(Date* x, Date* y) { return *x < *y; } // 函数重载 bool Less(Date* x, Date* y) { return *x < *y; }
如上所示的函数模板特化形式,就可以解决这样的问题,但是其实我们也可以写一个重载函数来解决这个问题。通常情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现功能,最好直接写一个重载函数,通常不建议写函数模板特化。
2.3 类模板特化
关于类模板的特化其中包含全特化和偏(半)特化。
其中全特化为将模板参数列表中所有的参数都确定化,如下:
template<class T1, class T2> class A { public: A() { cout << "A<T1, T2>" << endl; } private: T1 _a1; T2 _a2; }; // 将参数列表中所有的参数都确定化 template<> class A<int, char> { public: A() { cout << "A<int, char>" << endl; } private: int _a1; char _a2; }; void Test01() { A<int, int> aa1; A<int, char> aa2; }
关于偏特化,也分为两种类型,一种是将模板参数类表中的一部分参数特化,另一种是将参数进一步的限制,如下:
// 将模板参数表中的一部分参数特化 template<class T1> class A<T1, int> { public: A() { cout << "A<T1, int>" << endl; } private: T1 _a1; int _a2; }; // 将模板参数表中的参数进一步的限制 template<class T1,class T2> class A<T1*, T2*> { public: A() { cout << "A<T1*, T2*>" << endl; } private: T1 _pa1; T2 _pa2; };
3. 模板的分离编译
首先关于什么是分离编译,分离编译就是将一个类或者一个函数的声明与定义分别放在不同的文件之中,然后在生成目标文件的过程中,需要将所有目标文件(每一个源文件都会生成一个目标文件)链接起来,形成一个单一的可执行文件的过程叫做分离编译模式。
3.1 模板的分离编译及其原理
关于模板的编译与分离,就是将一个模板的声明放在一个 .h 文件中,然后将一个模板的定义放在另一个 .cpp 文件之中,如下:
如上图所示,当我们将模板的声明与定义分隔开的时候,调用对应函数的时候就会导致报错(若我们不调用对应的函数的时候,就不会报错,这是因为对于模板而言,只是一个半成品,只有按需实例化的时候才会检测出错误),链接错误。
出现这种错误的原因:
当我们声明和定义分离的时候,我们将头文件包含在当前 main 函数所在的文件中,在预处理阶段,会将头文件在 main 函数所在的文件展开,然后在编译阶段,我们将我们的模板进行实例化,根据传入的模板参数进行实例化,有多少种就会实例化多少种代码,然后在链接的阶段,会去找我们调用的函数,但是在展开实例化的函数中,我们只有声明,然后就会去分离定义的文件中寻找,当在其他文件中找到定义的时候,这里的定义并没有在编译阶段跟着实例化,仍然还是带有模板参数的模板函数,所以链接的时候就找不对对应需要的实例化模板函数。
关于以上的解决方法,在分离定义中进行显示实例化,如下:
如上所示,我们可以使用显示实例化来解决这个问题,但是这种问题的解决方法也仅仅只是治标不治本,当我们传入另一个模板实参的时候,还需要显示实例化一次,这并符合模板的特点,所以说,最好将模板的定义和声明放在同一个地方,或者分离定义在同一个文件中。
另外,关于模板这一块的调用形式很奇怪,一不小心就会用错。所以对于模板的使用,建议将模板的声明和定义放入到同一个文件中(因为在同一个文件中时,既有声明也有定义,直接就实例化,编译的时候,有函数定义,就有函数地址,就不需要等到链接的时候在去寻找)。