本篇讲解模板特化
------------------------------------------------------------------------------------------------------------
第12章 特化和重载
------------------------------------------------------------------------------------------------------------
前面几篇博客讲解了C++模板如何使一个泛型定义扩展成一写相关的类家族或者函数家族。但该机制并非适合所有情况,C++通过更多的特化机制具备了许多用特定方式透明替换泛型定义的特性,也即下面介绍的模板特化和函数模板的重载。
12.1 当泛型代码不再使用的时候
书中提供了一个例子说明泛型代码有时使用起来不再方便,类似的例子比较容易找,详见书籍。
12.2 重载函数模板
上面12.1中描述的例子说明两个同名的函数模板可以同时存在,还可以对它们进行实例化,使它们具有相同的参数类型。下面提供另一个例子:
// details/funcoverload.hpp template <typename T> int f(T) { return 1; } template <typename T> int f(T*) { return 2; }
如果我们用int*来替换第1个模板的T,用int来替换第2个模板的T,那么将会获得两个具有相同参数类型(和返回类型)的同名函数。也就是说,不仅是同名模板可以同时存在,同名各自的实例化体也可以同时存在,即使这些实例化体具有相同的参数类型和返回类型。可以如下调用:
// details/funcoverload.cpp #include <iostream> #include "funcoverload.hpp" int main() { std::cout << f<int*>((int*)0) << std::endl; std::cout << f<int>((int*)0) << std::endl; } 程序输入如下: 1 2
为了说明这一点,让我们相信地分析调用f<int*>((int*)0)。语法f<int*>说明我们希望用int*来替换模板f的第1个模板参数,而且这种替换并不依赖于模板实参演绎。在这个例子中,有两个f模板,因此所生成的重载集包含了两个函数:f<int*>(int*)(生成自第1个模板)和f<int*>(int**)(生成自第2个模板)。然而,调用实参(int*)0的类型是int*,因此它将会和第1个模板生成的函数更好地匹配,最后也就调用这个函数。 类似的分析也可以用于第2个调用。
12.2.1 签名
只要具有不同的签名,两个函数就可以在同一个程序中同时存在。我们对函数签名的定义如下:
1. 非受限函数的名称(或者产生自函数模板的这类名称)。
2. 函数名称所属的类作用域或者名字空间作用域:如果函数名称是具有内部链接的,还包括该名称所在的翻译单元。
3. 函数的const、volatile或者const volatile限定符(前提是它是一个具有这类限定符的成员函数)。
4. 函数参数的类型(如果这个函数是产生自函数模板的,那么指的是模板参数被替换之前的类型)。
5. 如果这个函数是产生自函数模板,那么包括它的返回类型。
6. 如果这个函数是产生自函数模板,那么包括模板参数和模板实参。
这就意味着:从原则上讲,下面的模板和它们的实例化可以在同个程序中同时存在:
template<typename T1, typename T2> void f1(T1, T2); template<typename T1, typename T2> void f1(T2, T1); template<typename T> long f2(T); template<typename T> char f2(T);
然而,如果上面这些模板是在同一个作用域中进行声明的话,我们可能不能使用某些模板,因为实例化过程可能会导致重载二义性。如:
#include <iostream> template<typename T1, typename T2> void f1(T1, T2) { std::cout << "f1(T1, T2) "; } template<typename T1, typename T2> void f1(T2, T1) { std::cout << "f1(T2, T1) "; } // 到这里为止一切都是正确的 int main() { f1<char, char>('a', 'b'); // 错误:二义性 }
在上面的代码中,虽然函数f1<T1 = char, T2 = char>(T1, T2)可以和函数f1<T1 = char, T2 = char>(T2, T1)同时存在,但是重新解析规则将不知道应该选择哪一个函数。因此,只有在这两个模板出现于不同的翻译单元时,它们的两个实例化体才可以在同个程序中同时存在(而且,链接器也不应该抱怨说存在重复定义,因为这两个实例化体的签名是不同的)。
12.2.2 重载的函数模板的局部排序
template <typename T> int f(T) { return 1; } template <typename T> int f(T*) { return 2; } int main() { std::cout << f(0) << std::endl; std::cout << f((int*)0) << std::endl; }
f(0)这个调用中,重载解析并没有发挥作用,直接匹配第一个模板;
第2个调用(f((int*)0))中:对于这两个模板,实参演绎都可以获得成功,于是获得两个函数,即f<int*>(int*)和f<int>(int*)。如果根据原来的重载解析观点,这两个函数和实参类型为iint*的调用的匹配程度是一样的,这也就意味着该调用是二义性的。然而,在这中情况下,还应该考虑重载解析的额外规则:选择“产生自更加特殊的模板的函数”。因此,第2个模板被认为是更加特殊的模板,从而产生下面的输出结果:
1
2
12.2.3 正式的排序原则
下面我们将给出一个精确的过程来判断:在参与重载集的所有函数模板中,某个函数模板是否比另一个函数模板更加特殊。然而,我们应该知道这只是不完整的排序原则:就是说,两个模板也可能会被认为具有相同的特殊程度。如果重载解析必须在这两个特殊程度相同的模板中进行选择,那么将不能做出任何决定,也就是说程序包含了一个二义性错误。
假设我们要比较两个同名的函数模板ft1和ft2,对于给定的函数调用,它们看起来都是可行的。在我们下面的讨论中,对于没有被使用的缺省函数实参和省略号参数,我们将不考虑。接下来,通过如下替换模板参数,我们将为这两个模板虚构两份不同的实参类型(如果是转型函数模板,那么还包括返回类型)列表,其中第1份列表针对第1个模板,第2份列表针对第2个模板。“虚构”的实参列表将这样地替换每个模板参数:
1. 用唯一的“虚构”类型替换每个模板类型参数;
2. 用唯一的“虚构”类模板替换每个模板的模板参数;
3. 用唯一的适当类型的“虚构”值替换每个非类型参数。
“更加特殊”的判断:
如果第2个模板针对第1份列表可以进行成功的实参演绎(能够进行精确的匹配),而第1个模板针对第2份列表的实参演绎以失败告终,那么我们就称第1个模板要比第2个模板更加特殊。反之,如果第1个模板针对第2份列表可以进行成功的实参演绎(能够进行精确的匹配),而第2个模板针对第1份列表的实参演绎失败,那么我们就称第2个模板要比第1个模板更加特殊。否则的话(或者是两个都不能成功演绎,或者是两个都能成功演绎),我们就称这两个模板之间不存在特殊的排序关系。
例子参见书籍。
12.2.4 模板和非模板
函数模板也可以和非模板函数同时重载。当其他的所有条件都是一样的时候,实际的函数调用将会优先选择非模板函数。
12.3 显式特化(全局特化)
备注:
1. 类模板和函数模板都可以被全局特化;
2. 类模板能局部特化,不能被重载;
3. 函数模板能被重载,不能被局部特化。
具有对函数模板进行重载的这种能力,再加上可以利用局部排序规则选择最佳匹配的函数模板,我们就能够给泛型实现添加更加特殊的模板,从而可以透明地获得具有更高效率的代码。然而,类模板不能被重载;我们可以选择另一种替换的机制来实现这种透明自定义类模板的能力,那就是显式特化。C++标准的“显式特化”概念指的是一种语言特性,我们通常也称之为全局特化。它为模板提供了一种使模板参数可以被全局替换的实现,而没有剩下模板参数。事实上,类模板和函数模板都是可以被全局特化的,而且类模板的成员(包括成员函数、嵌入类、静态成员变量等,它们的定义可以位于类定义的外部)也可以被全局特化。 在一下节,我们将讨论局部特化。局部特化和全局特化有些类似,但局部特化并没有替换所有的模板参数,就是说某些参数化实现仍然保留在模板的(另一种)实现中。另外,在我们的源代码中,全局特化和局部特化都是显式的,这也是我们在讨论中避免使用显式特化这个概念的原因。实际上,全局特化和局部特化都没有引入一个全新的模板或者模板实例。它们只是对原来的泛型(或者非特化)模板中已经隐式声明的实例提供另一种定义。在概念上,这是一个相对比较重要的现象,也是特化区别于重载模板的关键之处。
12.3.1 全局的类模板特化 如下:
template<typename T> class S { public: void info() { std::cout << "generic (S<T>::info() )"; } }; template<> class S<void> { public: void msg() { std::cout << "fully specialized (S<void>::msg()) "; } };
(1) 我们看到,全局特化的实现不需要与(原来的)泛型实现有任何关联,这就允许我们可以包含不同名称的成员函数(info相对msg)。实际上,全局特化只和类模板的名称有关联。
(2)另外,指定的模板实参列表必须和相应的模板参数列表一一对应。例如,我们不能用一个非类型值来替换一个模板类型参数。然而,如果模板参数具有缺省模板实参,那么用来替换的模板实参就是可选的(即不是必须的)。
template<typename T> class Types { public: typedef int I; }; template<typename T, typename U = typename Types<T>::I> class S; // (1) template<> class S<void> // (2) { public: void f(); }; template<> class S<char, char>; // (3) template<> class S<char, 0>; // 错误:不能用0来替换U int main() { S<int>* pi; // 正确:使用(1),这里不需要定义 S<int> e1; // 错误:使用(1),需要定义,但找不到定义 S<void>* pv; // 正确:使用(2) S<void, int> sv; // 正确:使用(2),这里定义是存在的,因为模板特化的第2个参数的缺省类型为int类型 S<void, char> e2; // 错误:使用(1),需要定义,但找不到定义 S<char, char> e3; // 错误:使用(3),需要定义,但找不到定义 } template<> class S<char, char> // (3)处的定义 { };
如例子中所示,(模板)全局特化的声明并不一定是定义。另外,当一个全局特化声明之后,针对该(特化的)模板实参列表的调用,将不再使用模板的泛型定义,而是使用这个全局特化的定义。因此,如果在调用处需要该特化的定义,而在这之前并没有提供这个定义,那么程序将会出现错误。对于类模板特化而言,“前置声明”类型有时候是很有用的,因为这样就可以构造相互依赖的类型。另外,以这种方式获得的全局特化声明(应该记住它并不是模板声明)和普通的类声明是类似的,唯一的区别在于语法以及该特化的声明必须匹配前面的模板声明。对于特化声明而言,因为它并不是模板声明,所以应该使用(位于类外部)的普通成员定义语法,来定义全局类模板特化的成员(也就是说,不能指定template<>前缀):
template<typename T> class S; template<> class S<char**> { public: void print() const; }; // 下面的定义不能使用template<>前缀 void S<char**>::print() const { std::cout << "pointer to pointer to char "; }
我们知道,可以用全局模板特化来代替对应泛型模板的某个实例化体。然而,全局模板特化和由模板生成的实例化版本是不能共存于同一个程序中的,否则会导致编译期错误。遗憾的是,如果在不同的翻译单元,将很难捕捉到这种错误。如下:
// 翻译单元1 template <typename T> class Danger { public: enum { max = 10 }; }; char buffer[Danger<void>::max]; // 使用了泛型值 extern void clear(char const*); int main() { clear(buffer); } // 翻译单元2 template<typename T> class Danger; template<> class Danger<void> { public: enum { max = 100 }; }; void clear(char const* buf) { // 可能与原先定义的数组大小不匹配 for(intk = 0; k < Danger<void>::max; ++k) { buf[k] = '