首先说结论:
- 函数模板支持:overload(重载),和full specialization(全特化),但是暂不支持partial specialization(偏特化)。。
下面逐一介绍相关概念。
Overload && Signature
谈到重载就需要先介绍函数的签名(Signature),它包含如下信息:
- 函数的裸名(不包含所属class或者namespace的作用域修饰,或者是被实例化以得到当前函数的函数模板的名字)。
- 函数所属的class或者namespace的作用域信息。如果该函数有内部链接属性的话,还应包含该函数所属的编译单元的相关信息。
- 如果函数是一个class的成员函数,且被const,volatile,或者const volatile修饰的话,还应包含这些信息。
- 如果函数是一个class的成员函数,且被&或者&&修饰的话,也应包含这些信息。
- 函数参数的类型(如果函数是经由函数模板实例化得到的话,该类型指的是模板参数被实例化之前的类型,比如T或者T1).
- 如果函数是经由函数模板实例化得到,还应包含返回类型(非模板函数不包含此项内容)。
- 如果函数是经由函数模板实例化得到,还应包含模板参数(比如T)以及用来实例化改模板的类型。
如果两个函数有不同的函数签名,那么原则上它们就可以在程序中共存,比如下面这些模板:
template<typename T1, typename T2> //模板1
void f1(T1, T2);
template<typename T1, typename T2> //模板2,与模板1的函数参数类型不同
void f1(T2, T1);
template<typename T> // 模板3
long f2(T);
template<typename T> // 模板4,与模板3的返回值类型不同
char f2(T);
但是可以并存,并不代表可以被正常使用,因为可能会导致重载歧义。比如如果尝试调用f2(42),那么很显然会导致歧义(使用模板3还是模板4?)。
函数模板重载的一个例子:
考虑需要交换两个类型相同的变量的值,可以按如下方式实现一个模板:
template<typename T>
void swap(T& x, T& y)
{
T tmp(x);
x = y;
y = tmp;
}
但是该实现包含了三次copy操作,对于某些类型并不是那么高效。比如对于包含了所存储数据的指针以及长度信息的Array,下面这种实现的效率要高的多:
template<typename T>
void swap(Array<T>& x, Array<T>& y)
{
swap(x.ptr, y.ptr);
swap(x.len, y.len);
}
函数模板的partial specialization(暂不支持)
首先,偏特化并不会引入新的模板,只是对原有模板(主模板)的一次扩充。在模板查找的过程中,最开始的时候只会考虑主模板,在主模板确定后,如果发现还有偏特化的模板,并且它能够与模板参数进行更好的匹配,那么会最终选择该偏特化的版本。
这一点与重载不同,重载会引入一个新的、与被重载模板并列的模板。在选择需要被实例化的模板时,会同时考虑所有的重载模板,并从中选择最匹配的那一个。
在某些重载不能满足需求的情况下,我们可能希望能够使用偏特化,比如对于:
template <typename T, typename R> //模板1
R convert(const T & rhs)
{
...
}
template <typename T> //模板2 试图重载模板1,虽然可以共存,但是却不能被在同一个编译单元中使用,见上节
void convert(const T & rhs)
{
...
}
在已经有了模板1的情况下,如果我们试图重载出一个始终返回void的模板2的话,在使用时是会遇到错误的。此时我们可能会希望能够使用偏特化。
我们所期望的函数模板偏特化的形式可能会像下面这样:
template <typename T, typename R> //模板1
R convert(const T & rhs)
{
...
}
template <typename T> //模板2 模板1的偏特化,以convert后面的“显式模板参数”<T , void> 为偏特化标识
void convert<T, void>(const T & rhs)
{
...
}
但是在某些情况下,偏特化会导致问题,比如对于:
template<typename T>
void add (T& x, int i); // 模板1, 主模板
template<typename T1, typename T2> //模板2, 主模板的一个重载
void add (T1 a, T2 b);
template<typename T> // 模板3,一个偏特化版本。但是它偏特化的是那一个模板呢?
void add <T*> (T*&, int);
我们将很难分辨模板3是对模板1还是模板2进行了偏特化。因此到目前为止,函数模板的偏特化还没有得到C++标准的支持,但是由于其确实能够解决一部分实际问题,因此不排除它在将来会被纳入标准的可能。
函数模板的full specialization
全特化在语言规则上和上面的偏特化类似。小的区别是,当可以通过“参数推断”推断出用来实例化该模板的类型时,可以省略掉“显式模板参数”。比如:
template<typename T> // 模板1
int f(T)
{
return 1;
}
template<typename T> // 模板2
int f(T*)
{
return 1;
}
template<> // 模板3, 对模板1进行了全特化,不需要写成 template<> int f<int>(int)
int f(int)
{
return 1;
}
template<> // 模板4 对模板2进行了全特化,不需要写成 template<> int f<int>(int*)
int f(int*)
{
return 1;
}
另一个比较值得注意的关于全特化的问题是,在很多方面,通过全特化得到的函数和常规函数都是类似的。比如,在一个程序中,只能有一份非inline的函数模板的全特化函数存在。然而,为了保证用户使用的是全特换版本的函数,而不是可能从函数模板实例化出来的其他函数,又需要将全特化函数的声明放在模板声明的后面。因此一个常规的关于全特化的代码组织方式是:
- 一个包含了主模板和偏特化的定义,但是只包含了全特化函数声明的头文件:
#ifndef TEMPLATE_G_HPP
#define TEMPLATE_G_HPP
// template definition should appear in header file:
template<typename T>
int g(T, T x = 42)
{
return x;
}
// specialization declaration inhibits instantiations of the template;
// definition should not appear here to avoid multiple definition errors
template<> int g(int, int y);
#endif // TEMPLATE_G_HPP
- 以及一个包含了全特化函数定义的cpp文件:
#include "template_g.hpp"
template<> int g(int, int y)
{
return y/2;
}
如果非要将全特换函数放置在头文件里面,那么你需要将其声明为inline的:
#ifndef TEMPLATE_G_HPP
#define TEMPLATE_G_HPP
// template definition should appear in header file:
template<typename T>
int g(T, T x = 42)
{
return x;
}
// specialization declaration inhibits instantiations of the template;
// 如果将全特化函数放置在头文件中,那么它必须是inline的
template<> inline int g(int, int y)
{
return y/2;
}
#endif // TEMPLATE_G_HPP
总结
以上内容大部分摘抄自《C++ templates》第二版(https://github.com/Walton1128/CPP-Templates-2nd--),因书中论述相对分散,特摘抄于此。