文章目录
- 非类型模板参数
- 函数模板的特化
- 类模板的特化
- 全特化
- 偏特化
- 部分参数特化
- 参数修饰特化
- 模板分离编译
- 解决方法
非类型模板参数
模板的参数分为两种:
- 类型参数: 则是我们通常使用的方式,就是在模板的参数列表中在
class
后面加上参数的类型名称。 - 非类型参数: 则是用一个常量作为模板的参数,在模板中可以当作常量来使用,通常是需要指明大小或者初始化内容的才会用到。
类型参数我们在上一篇博客中讲的很详细了,不再赘述。而非类型参数比较常见的就是 c++
中的 array
:
array
的底层就是直接使用的数组,而数组创建时必须指明大小,并且大小得是个常量,所以就会用到非类型模板参数。
注意:
- 浮点数、自定义类型、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型参数必须在编译期就能确认结果。
通常情况下非类型模板参数都是使用字符型和整型。
函数模板的特化
当我们使用模板来实现一个函数,肯定是想利用它来解决逻辑相同但数据类型不同的一些问题,来实现代码的复用,但是也存在某些特例,比如针对某一情景或者某一类型,这个模板需要有特殊的处理,这个时候就需要用到模板的特化。
例如:
template<class T>
bool IsEqual(T str1, T str2)
{return str1 == str2;
}int main()
{char str1[] = "hello";char str2[] = "hello";if (IsEqual(str1, str2))cout << "true";elsecout << "false";
}
这里不同的原因是传递过去的是两个 char*
类型,他们两个比较的不是字符串的内容,而是指针的地址,这里 str1、str2
是在栈上开辟一块空间后再将 hello
拷贝过去,而 IsEqual
比较的是两者指向的两块不同内存(也就是两个 hello
的内存)的首地址,不可能相同。
如果要比较 char*
,就得用到 strcmp
来对这个情况进行特殊处理, 也就是模板的特化。
template<>
bool IsEqual<char*>(char* str1, char* str2)
{return strcmp(str1, str2) == 0;
}
// extern int strcmp(const char *s1,const char *s2);
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字
template
后面接一对空的尖括号 <>
- 函数名后面的
<>
中指定需要特化的类型 - 特化的函数其形参 一定要与 模板的形参 类型完全相同。
类模板的特化
类也是同理,如果需要有特殊情景也需要特化处理:
类模板如下:
template<class T1, class T2>
class test
{
public:test(){cout << "test<T1, T2>" << endl;}private:T1 _x;T2 _y;
};
全特化
全特化即是将模板参数列表中所有的参数都确定化。
这里对 test<int,double>
版本特化。
template<>
class test<int, double>
{
public:test(){cout << "test<int, double>" << endl;}private:int _x;double _y;
};int main()
{test<double, double> t1;test<int, double> t2;
}
偏特化
偏特化即是任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化有两种表现方式:一种是部分参数特化,一种是参数修饰特化。
部分参数特化
这里对第二个参数特化,只要第二个参数是 double
就会调用对应特化版本。
template<class T1>
class test<T1, double>
{
public:test(){cout << "test<T1, double>" << endl;}private:T1 _x;double _y;
};int main()
{test<double, double> t1;test<int, double> t2;test<float, double> t3;test<double, int> t4;
}
参数修饰特化
比如用指针或者引用来修饰类型,也可以进行特化。
template<class T1, class T2>
class test<T1*, T2*>
{
public:test(){cout << "test<T1*, T2*>" << endl;}private:T1* _x;T2* _y;
};int main()
{test<double, int> t1;test<int*, double*> t2;test<float*, double*> t3;test<char*, double> t4;
}
模板分离编译
对于一个代码量比较多的项目,通常都会采用声明与定义分离的方法,比如在头文件进行声明,在源文件完成代码的实现,最后通过链接的方法链接成单一的可执行文件。但是 C++
的编译器却不支持模板的分离编译,一旦进行分离编译,就会出现链接错误。
//头文件a.h
template<class T>
bool IsEqual(const T& str1, const T& str2);-------------
//源文件a.cpp
template<class T>
bool IsEqual(const T& str1, const T& str2)
{return str1 == str2;
}
--------------
//test.c
#include<iostream>
#include"a.h"
using namespace std;int main()
{cout << IsEqual(3, 5);cout << IsEqual('a', 'b');
}
这里看上去是没有问题的,但是涉及到了模板的实例化规则。
首先,一个编译单元是指一个 .cpp
文件以及它所 #include
的所有 .h
文件,.h
文件里的代码将会被扩展到包含它的 .cpp
文件里,然后编译器编译该 .cpp
文件为一个 .obj
文件(假定我们的平台是 win32
,Linux
则是 .o
文件),后者拥有 PE(Portable Executable,即windows可执行文件)
文件格式,并且本身包含的就已经是二进制码,但是不一定能够执行,因为并不保证其中一定有 main函数
。当编译器将一个工程里的所有 .cpp
文件以分离的方式编译完毕后,再由连接器(linker)
进行连接成为一个 .exe
(Linux
为 .out
)文件。
那么来看上面的代码,
test.c
和a.cpp
被编译器编译成test.obj
和a.obj
。- 当编译器处理主函数调用
IsEqual
的时,这是第一次使用模板,会进行实例化,而实例化需要模板的定义,但是在test.c
中并没有模板的定义,虽然在test.obj
文件中头文件a.h
也被展开,但可惜的是a.h
中只有模板的声明 。 - 那么编译器只能寄希望于连接器,希望它能够在其他
.obj
里面找到IsEqual
的实例,本题中连接器就是去a.obj
中找实例,但问题在于,虽然a.cpp
中有模板的定义,但并没有使用过这个模板,因此a.obj
中不存在IsEqual
的实例。因此连接器只能返回一个连接错误。
解决方法
这个问题其实没有什么完美的解决方法
- 将声明和定义放到同一个头文件中。(导致头文件代码过于庞大)
- 模板定义的位置显式实例化。(不实用)
这个问题刘未鹏大佬写的非常好,可以学习一下他的博客
为什么C++编译器不能支持对模板的分离式编译