模板简介
模板提供了代码泛化的功能, 同一段代码可应用在不同类型.
模板不是魔法, 不是使用了一种神奇的方法使得代码可以适应不同类型的变量.
实际上, 在编译阶段, 编译器会实例化生成多份不同模板参数的代码, 等价于程序员写了多份不同类型参数的代码, 只不过是编译器替程序员完成了这部分工作罢了.
为什么模板要有特化
模板的代码不符合某些类型的要求, 比如写一个把原迭代器的内容拷贝到目标迭代器的copy函数, 对于类对象需要一个个拷贝,但是对于int, char等指针类型只需要一个memcpy就可以了, 此时就需要一个针对指针类型的特殊版本.
此外, C++标准库中的vector<bool>
也是vector<T>
的特化版本, 有兴趣的可以自己搜一下资料.
模板的特化分为偏特化和全特化, 下面分别介绍
偏特化
偏特化只用于类模板, 因为c++委员会说你不能("you can't partially specialize them -- pretty much just because the language says you can't.") 见Why Not Specialize Function Templates?
也有一些方法可以另类实现函数模板的偏特化, 这篇文章里有提到C++函数模板的偏特化 - 知乎, 以后再详细看看.
STL源码剖析
的第87页中提到了偏特化的几个定义:
- "所谓partial specialization的意思是提供另一份template定义式, 而其本身仍为templatized "
- "针对(任何)template参数更进一步的条件限制所设计出来的特化版本"
根据这两条定义, 我们可以提取出偏特化的两个性质:
- 对已有模板的模板参数进行限制
- 本身还是一个模板
偏特化在类名后加上<...>
进行模板参数指定
典型的偏特化有两种形式, 第一种是对多个模板参数中的某些(非全部)进行类型指定, 如:
template<typename A, typename B>
class C{...};//模板, 接受任意类型
template<typename A>
class C<A, int>{};//偏特化模板, 第一个参数为任意类型, 第二个参数只能为int
第二种偏特化形式, 不是很好理解, 是对指针, const等进行指定
template<typename T>
class C{...};//模板, 接受T为容易类型
template<typename T>
class C<T*>{};//偏特化版本, 只接受T为指针的情况
template<typename T>
class C<const T>{};//偏特化版本, 只接受T为const的情
不过可以看出它还是符合上面说的偏特化的两条性质的.
第二种偏特化的情况易和下面这种函数模板混淆
template<class T>
void f( T* );//函数模板,
没仔细看到话还以为这也是一种偏特化, 实际上这就是一个普通的函数模板(因为函数名后无<>
进行模板参数指定), 并且函数模板也没有偏特化
全特化
全特化在类名后加上<...>
进行模版参数指定
全特化比偏特化理解起来简单, 全特化需要指定所有模板参数, 全特化本身已经不是一个模板了, 如:
template<typename A, typename B>
class C{...};//模板, 接受任意类型
template<>
class C<double, int>{};//全特化, 第一个参数为double, 第二个参数只能为int
函数模板的重载
函数模板之间也可以重载, 如:
template<class T>
void f( T );//接受任意类型
template<class T>
void f( T* );//接受指针类型
函数模板和普通函数的匹配优先级
此部分主要参考 Why Not Specialize Function Templates?
牢记一点: 特化的模板不参与重载!!
对于模板、模板的全特化和模板的偏特化, 以及同名普通函数都存在的情况下,编译器在编译阶段进行匹配时,只匹配普通函数和模板, 匹配顺序如下:
1. 查找普通函数中有没有匹配的,如果有就选它
2. 查找模板中有没有匹配的, 并选则最匹配的版本, 然后进行下面两步
注意, 上面规则没提到特化版本,
如果编译器匹配到了规则2, 然后才进行特化版本的匹配
- 查找全特化版本中有没有匹配的
- 查找偏特化版本中有没有匹配的
特化版本不参与重载的原因是C++委员会认为因为写了一个特化版本而使得原先的重载函数模板匹配结果发生改变是不能接受的.
由于函数模板特化后的匹配行为很容易让人迷惑, 实际中不推荐使用函数模板的特化, 而选择使用普通函数!
下面的这个例子可以加深对特化模板匹配规则的理解,
using std::cout, std::endl;
template <class T>
void f(T x); // a
template <>
void f<>(int *x);//b
template<class T>
void f(T *x)://c
int main()
{
int *p;
f(p); //匹配的是c
return 0;
}
- 首先编译器在匹配f(p)时, 发现有两个同名的模板a, c,
- 然后编译器发现c更加接近f(p), 所以就匹配c, 根本没考虑特化版本b
下面的代码展示了, 存在一个和全特化版本相同的普通函数, 优先匹配普通函数:
#include<algorithm>
#include <iostream>
using std::cout;
/// 函数模板偏特化demo
template <typename A, typename B>
void f(A a, B b) {
std::cout << "Template version." << std::endl;
}
void f(int a, int b) {
std::cout << " Normal version." << std::endl;
}
template <>
void f<int, int>(int a, int b) {
std::cout << "Partial version." << std::endl;
}
int main()
{
// 测试代码
int a = 10;
double b = 12;
f(a, b);// template version
f(a, a);// Normal version
return 0;
}
最后, 再来查看一下普通函数和模板全特化函数的编译出的符号有什么区别, 下图是g++ 8.1.0的结果
可以看到模板函数的符号会另外添加一些额外字符, 可以和普通函数区分, 因此编译不会出现multi definition的错误.
总结:
- 类模板可以全特化, 偏特化
- 函数模板只能全特化, 但不推荐使用函数模板的全特化, 因为特化的模板在重载时的匹配行为很容易让人迷惑, 建议用普通函数代替