目录

第12章 特化与重载

12.2 重载函数模板

12.2.1 签名

12.2.2 重载的函数模板的局部排序

12.2.3 正式的排序原则

12.2.4 模板和非模板

12.3 显式特化

12.3.1 全局的类模板特化

12.3.2 全局的函数模板特化

12.3.3 全局成员特化

12.4 局部的类模板特化


第12章 特化与重载

目前为止,我们已经知道了:C++模板如何使一个泛型定义扩展成一些相关的类家族或者函数家族。虽然这是一个功能很强大的机制,但该机制并非适合于所有的情况;在一些情况下,这种泛型操作就不是特定模板参数替换的最佳选择。

 

12.2 重载函数模板

两个同名的函数模板可以同时存在,还可以对它们进行实例化,使它们具有相同的参数类型。下面是另一个简单的例子:

template<typename T>
int f(T)
{
    return 1;
}
template<typename T>
int f(T*)
{
    return 2;
}

如果我们用int*来替换第1个模板的T,用int来替换第2个模板的T,那么将会获得两个具有相同参数类型(和返回类型)的同名函数。也就是说,不仅是同名模板可以同时存在,它们各自的实例化体也可以同时存在,即使这些实例化体具有相同的参数类型和返回类型。

 

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);

然而,如果上面这些模板是在同一个作用域中进行声明的话,我们可能不能使用某些模板,因为实例化过程可能会导致重载二义性。例如:

template<typename T1, typename T2>
void f1(T1, T2)
{
    std::cout << "f1(T1, T2)\n";
}

template<typename T1, typename T2>
void f1(T2, T1)
{
    std::cout << "f1(T2, T1)\n";
}
// 到这里为止一切都是正确的

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)):实参的类型是int,如果用int替换T,就能和第1个模板的参数匹配。然而,第2个模板的参数类型总是一个指针;因此,经过演绎之后,只有产生自第1个模板的实例才是该调用的候选函数。在这个调用中,重载解析并没有发挥作用。

第2个调用((f ( (int*) 0) )就显得比较有趣:对于这两个模板,实参演绎都可以获得成功,于是就获得两个函数,即f<int*>(int*)和f<int>(int*)。如果根据原来的重载解析观点,这两个函数和实参类型为 int*的调用的匹配程度是一样的,这也就意味着该调用是二义性的(见附录B)。然而,在这种情况下,还应该考虑重载解析的额外规则:选择“产生自更特殊的模板的函数”。因此(我们将在后面的小节看到),第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个模板更加特殊。否则的话(或者是两个都不能成功演绎,或者是两个都能成功演绎),我们就称这两个模板之间不存在特殊的排序关系。

让我们把这个过程应用于前面的例子,来更加清楚地阐明上面的问题。根据这两个模板和前面所描述的模板参数替换方法,我们虚构了两个实参类型列表:(A1)和(A2*)(A1和A2是不同的虚构类型)。显然,第1个模板可以成功地演绎第2份实参列表,只要用A2* 替换T就可以。然而,第2个模板却不能成功地演绎第1份列表,因为第2个模板的T*是不能和非指针类型A1进行匹配的。因此,我们就可以(正式地)得出结论:第2个模板比第1个模板更加特殊。

 

12.2.4 模板和非模板

函数模板也可以和非模板函数同时重载。当其它的所有条件都是一样的时候,实际的函数调用将会优先选择非模板函数。

 

12.3 显式特化

具有对函数模板进行重载的这种能力,再加上可以利用局部排序规则选择最佳匹配的函数模板,我们就能够给泛型实现添加更加特殊的模板,从而可以透明地获得具有更高效率的代码。然而,类模板是不能被重载的;但我们可以选择另一种替换的机制来实现这种透明自定义类模板的能力,那就是显式特化。

 

12.3.1 全局的类模板特化

引入全局特化需要用到下面3个标记序列:template、< 和 > 。另外,紧跟在类名称声明后面的就是要进行特化的模板实参。下面的例子说明了这一点:

template<typename T>
class S 
{
public:
    void info() 
         {
            std::cout << "generic (S<T>::info())\n";
         }
};

template<>
class S<void> 
{
public:
    void msg() 
         {
            std::cout << "fully specialized (S<void>::msg())\n";
         }
}

我们看到,全局特化的实现并不需要与(原来的)泛型实现有任何关联,这就允许我们可以包含不同名称的成员函数(info相对msg)。实际上,全局特化只和类模板的名称有关联。

另外,指定的模板实参列表必须和相应的模板参数列表一一对应。例如,我们不能用一个非类型值来替换一个模板类型参数。然而,如果模板参数具有缺省模板实参,那么用来替换的模板实参就是可选的(即不是必须的):

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),这里定义是存在的
    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\n";
}

我们可以使用一个更复杂的例子来进一步理解这个概念:

template<typename T>
class Outside 
{
public:
    template<typename U>
    class Inside {
    };
};

template<>
class Outside<void> 
{
// 下面的嵌套类和前面定义的泛型模板之间并不存在联系
    template<typename U>
    class Inside
    {
    private:
        static int count;
    };
};

//下面的定义不能使用template<>前缀
template<typename U>
int Outside<void>::Inside<U>::count = 1;

可以用全局模板特化来代替对应泛型模板的某个实例化体。然而,全局模板特化和由模板生成的实例化版本是不能够共存于同一个程序中的。如果试图在同一个文件中使用这两者的话,那么通常都会导致一个编译期错误

template <typename T>
class Invalid {
};

Invalid<double> x1;  // 产生一个Invalid<double>实例化体

template<>
class Invalid<double>; // 错误:Invalid<double>已经被实例化了

 

12.3.2 全局的函数模板特化

就语法及其后所蕴涵的原则而言,(显式的)全局函数模板特化和类模板特化大体上是一致的,唯一的区别在于:函数模板特化引入了重载和实参演绎这两个概念。

如果可以借助实参演绎(用实参类型来演绎声明中给出的参数类型)来确定模板的特殊化版本,那么全局特化就可以不声明显式的模板实参。让我们考虑下面的例子:

template<typename T>
int f(T)       // (1)
{
    return 1;
}

template<typename T>
int f(T*)       // (2)
{
    return 2;
}

template<> int f(int)  // OK: (1)的特化
{
    return 3;
}

template<> int f(int*) // OK: (2)的特化。
{
    return 4;
}

全局函数模板特化不能包含缺省的实参值。然而,对于基本(即要被特化的)模板所指定的任何缺省实参,显式特化版本都可以应用这些缺省实参值。例如:

template<typename T>
int f(T, T x = 42)
{
    return x;
}

template<> int f(int, int = 35) // 错误,不能包含缺省实参值
{
    return 0;
}

template<typename T>
int g(T, T x = 42)
{
    return x;
}

template<> int g(int, int y)
{
    return y/2;
}

int main()
{
    std::cout << g(0) << std::endl; // 正确,输出21
}

 

12.3.3 全局成员特化

除了成员模板之外,类模板的成员函数和普通的静态成员变量也可以被全局特化;实现特化的语法会要求给每个外围类模板加上template<>前缀。如果要对一个成员模板进行特化,也必须加上另一个template<>前缀,来说明该声明表示的是一个特化。为了说明这些含义,让我们假设具有下面的声明:

template<typename T>
class Outer 
{             // (1)
public:
    template<typename U>
    class Inner 
    {       // (2)
    private:
        static int count;       // (3)
    };

    static int code;        // (4)

    void print() const           // (5)
    {        
        std::cout << "generic";
    }
};

template<typename T>
int Outer<T>::code = 6;        // (6)

template<typename T> 
template<typename U>
int Outer<T>::Inner<U>::count = 7;  // (7)

template<> class Outer<bool> 
{          // (8)
public:
    template<typename U>
    class Inner 
    {           // (9)
    private:
        static int count;       // (10)
    };

void print() const {        // (11)
}
};

在(1)处的泛型模板Outer中,(4)处的code和(5)处print(),这两个普通成员都具有一个外围类模板。因此,需要使用一个template<>前缀说明:后面将用一个模板实参集来对它进行全局特化:

template<>
int Outer<void>::code = 12;

template<>
void Outer<void>::print() const
{
    std::cout << "Outer<void>";
}

这些定义将会用于替代类Outer<void>在(4)处和(5)处的泛型定义;但是,类Outer<void>的其它成员仍然默认地产生自(1)处的模板。另外,在提供了上面的声明之后,就不能再次提供Outer<void>的显式特化。

类似于全局函数模板特化,我们需要一种可以在不指定定义的前提下(为了避免多处定义),可以声明类模板普通成员特化的方法。尽管对于普通类的成员函数和静态成员变量而言,非定义的类外声明在C++中是不允许的;但如果是针对类模板的特化成员,该声明则是合法的。也就是说,前面的定义可以具有如下声明:

template<>
int Outer<void>::code;
template<>
void Outer<void>::print() const;

对于成员模板Outer<T>::Inner,也可以用一个特定的模板实参对它进行特化,而且对于该特化所在的外围 Outer<T>而言,这个特化操作并不会影响 Outer<T>相应实例化体的其它成员。另外,由于存在一个外围模板(也就是Outer<T>),所以我们需要添加一个template<>前缀。最后所获得的代码大致如下:

template<> template<typename X>
class Outer<wchar_t>::Inner 
{
public:
    static long count; // 成员类型发生了改变
};

template<> template<typename X>
long Outer<wchar_t>::Inner<X>::count;

模板Outer<T>::Inner也可以被全局特化,但只能针对Outer<T>的某个给定实例。而且,我们需要添加两个 template<>前缀:因为外围类需要一个 template<>前缀,我们所要全局特化的内围模板(inner template)也需要一个template<>前缀:

template<>
template<>
class Outer<char>::Inner<wchar_t> 
{
public:
    enum { count = 1};
};

// 下面的C++程序是不合法的:
// template<> 不能位于模板实参列表的后面
template<typename X>
template<> class Outer<X>::Inner<void>; // 错误

我们可以将上面这个特化与Outer<bool>的成员模板的特化比较一下。由于Outer<bool>已经在前面全局特化了,所有它的成员模板也就不存在外围模板,因此我们就只需要一个template<>前缀:

template<>
class Outer<bool>::Inner<wchar_t> 
{
public:
    enum { count = 2 };
};

 

12.4 局部的类模板特化

全局模板特化通常都是很有用的, 但有时候我们更希望把类模板特化成一个“针对模板实参”的类家族, 而不是针对“一个具体实参列表”的全局特化。 例如, 假设下面是一个实现链表功能的类模板:

template<typename T>
class List { // (1)
public:
	… 
	void append(T const&);
	inline size_t length() const;
	…
};

对于某个使用这个模板的大项目, 它可能会基于多种类型来实例化该模板的成员。 于是, 对于那些没有进行内联扩展的成员函数(譬如List<T>::append()) , 这就可能会明显增加目标代码的大小。 然而, 如果我们从一个更低层次的实现来看, List<int*>::append()的代码和List<void*>::append()的代码是完全相同的。 也就是说, 我们希望可以让所有的指针List共享同一个实现。 尽管我们不能直接用C++来表达这种实现, 但我们可以指定所有的指针List都实例化自一个不同的模板定义, 从而近似地获得这种实现:

template<typename T>
class List < T* > 
{ // (2)
private:
	List<void*> impl;
	…
public:
	… 
	void append(T* p) 
	{
		impl.append(p);
	} 
	size_t length() const 
	{
		return impl.length();
	} …
};

在这种情况下, 我们把原来的模板(即(1) 处的模板) 称为基本模板, 而后一个定义则被称为局部特化(因为该模板定义所使用的模板实参只是被局部指定) 。 表示一个局部特化的语法包括: 一个模板参数列表声明(template<…>) 和在类模板名称后面显式指定的模板实参列表(在我们的例子中是<T*>) 。

我们前面的代码还存在一个问题, 因为 List<void*>会递归地包含一个相同类型的List<void*>成员。 为了打破这种无限递归, 我们可以在这个局部特化前面先提供一个全局特化:

template<>
class List<void*> 
{ // (3)
    … 
    void append (void* p);
    inline size_t length() const;
    …
};

这样, 一切才是正确的。 因为当进行匹配的时候, 全局特化会优于局部特化。 于是, 指针List的所有成员函数都被委托给List<void*>的实现(通过容易内联的函数) 。 针对C++模板备受指责的代码膨胀的缺点, 这也是克服该缺点的有效方法之一。

对于局部特化声明的参数列表和实参列表, 存在一些约束。 下面就是一些重要的约束:

1.局部特化的实参必须和基本模板的相应参数在种类上(可以是类型、 非类型或者模板) 是匹配的。

2.局部特化的参数列表不能具有缺省实参; 但局部特化仍然可以使用基本类模板的缺省实参。

3.局部特化的非类型实参只能是非类型值, 或者是普通的非类型模板参数; 而不能是更复杂的依赖型表达式(诸如2*N, 其中N是模板参数) 。

4.局部特化的模板实参列表不能和基本模板的参数列表完全等同(不考虑重新命名) 。

下面的例子详细地说明了这些约束:

template<typename T, int I = 3>
class S; // 基本模板

template<typename T>
class S < int, T > ; // 错误: 参数类型不匹配

template<typename T = int>
class S < T, 10 > ; // 错误: 不能具有缺省实参

template<int I>
class S < int, I * 2 > ; // 错误: 不能有非类型的表达式

template<typename U, int K>
class S < U, K > ; // 错误: 局部特化和基本模板之间没有本质的区别

每个局部特化(和每个全局特化一样) 都会和基本模板发生关联。当使用一个模板的时候, 编译器肯定会对基本模板进行查找, 但接下来会匹配调用实参和相关特化的实参, 然后确定应该选择哪一个模板实现。 如果能够找到多个匹配的特化, 那么将会选择“最特殊”的特化(和重载函数模板所定义的原则一样) ; 如果有未能找到“最特殊”的一个特化, 即存在几个特殊程度一样的特化, 那么程序将会包含一个二义性错误。

最后, 我们应该指出: 类模板局部特化的参数个数是可以和基本模板不一样的; 既可以比基本模板多, 也可以比基本模板少。 让我们再次考虑泛型模板List(在(1) 处声明) 。 我们已经讨论了应该如何优化指针List的实现, 但我们希望可以针对(特定的) 成员指针类型实现这种优化。 下面的代码就是针对指向成员指针的指针(pointer-to-memberpointers) , 来实现这种优化:

template<typename C>
class List < void* C::* >
{ // (4)
public:
	// 针对指向void*的成员指针的特化
	// 除了void*类型之外, 每个指向成员指针的指针类型都会使用这个特化
	typedef void* C::*ElementType;
	… 
	void append(ElementType pm);
	inline size_t length() const;
	…
};

template<typename T, typename C>
class List < T* C::* > { // (5)
private:
	List<void* C::*> impl;
	…
public:
	// 针对任何指向成员指针的指针类型的局部特化
	// 除了指向void*的成员指针类型, 它在前面已经处理了
	// 我们看到这个局部特化具有两个模板参数
	// 然而基本模板却只有一个参数
	typedef T* C::*ElementType;
	… 
	void append(ElementType pm) 
	{
		impl.append((void* C::*)pm);
	} 
	inline size_t length() const 
	{
		return impl.length();
	} 
	…
};

除了模板参数数量不同之外, 我们看到在(4) 处定义的公共实现本身也是一个局部特化(对于简单的指针例子, 这里应该是一个全局特化) , 而所有其它的局部特化((5) 处的声明) 都是把实现委托给这个公共实现。 显然, 在(4) 处的公共实现要比(5) 处的实现更加特殊化, 因此也就不会出现二义性问题。

更多推荐

[C++ Template]深入模板--特化与重载