Overloading与 Specialization的重要不同
你有过使用了函数具体化模板却没有得到预期结果的经历吗?
本文部分译自[Why Not Specialize Function Templates?]。具有一定英语阅读能力的读者强烈和强烈建议跳过此博客直接阅读原文。(http://www.gotw.ca/publications/mill17.htm#1)
众所周知,C++中存在着两类模板——函数模板与类模板。在重载与具体化方面它们存在着巨大的不同。
重载
值得引起注意的是,重载是只有函数才有的特性,而对于类模板来说它是没有意义的。这是显然的,因为重载是指函数名相同而函数签名不同。
// Example 1: Class vs. function template, and overloading
//
// A class template
template<class T> class X { /*...*/ }; // (a)
// A function template with two overloads
template<class T> void f( T ); // (b)
template<class T> void f( int, T, double ); // (c)
像a,b,c这样没有具体化(specialized)的模板被称为基模板1
(base template).
具体化
谈到具体化,函数模板与类模板的差异就显得更大了.这里有一句相当重要的概念,我强烈建议读者们把这句话彻底理解了再往下阅读:
类模板可以被完全具体化
2
(fully specialized)或者部分具体化(partially specialized)3
;
函数模板只能被完全具体化,但它可以通过函数重载达到与部分具体化相似的效果.
不妨通过下面这段代码更好地理解上面的话:
// Example 1, continued: Specializing templates
//
// A class template
template<class T> class X { /*...*/ }; // (a)
// A function template with two overloads
template<class T> void f( T ); // (b)
template<class T> void f( int, T, double ); // (c)
// A partial specialization of (a) for pointer types
template<class T> class X<T*> { /*...*/ }; //a1
// A full specialization of (a) for int
template<> class X<int> { /*...*/ }; //a2
// A separate base template that overloads (b) and (c)
// -- NOT a partial specialization of (b), because
// there's no such thing as a partial specialization
// of a function template!
template<class T> void f( T* ); // (d)
// A full specialization of (b) for int
template<> void f<int>( int ); // (e)
// A plain old function that happens to overload with
// (b), (c), and (d) -- but not (e), which we'll
// discuss in a moment
void f( double ); // (f)
再强调一下,a1是基模板a的部分具体化,a2是基模板a的完全具体化.
而与此不同的是,d其实是对基模板b,c的重载,某种程度上可以看作是并列的关系.因为d本质上也是一个基模板而并非基模板的具体化.
那么读者可能会问,那e呢?这正是我们稍后要讨论的重点.事实上,e并不是基模板,当然也不会被重载.
函数模板具体化不会被重载!
但在此之前,先让我们了解一下在不同的情况下使用的究竟是哪个函数模板.其实这一规则可以简单的总结为一个两级系统:
非模板函数处于第一优先级.在函数参数具有相同的匹配度的情况下,非模板函数优先于任何模板函数.
在没有第一优先级函数的情况下,也就是没有匹配的非模板函数的情况下,第二优先级的函数将得到考虑.第二优先级的函数是指基模板的模板函数.至于哪一个基模板函数将被选择,将根据我们熟悉的最佳匹配原则.这可以分为三种情况:
if (确实存在一个唯一的最佳匹配基模板函数)
{
if(该基模板恰好存在具体化而且该具体化适用于本次参数调用)使用该具体化;
else 直接使用该基模板进行实例化;
}
else if(同时存在多个最佳匹配基模板函数)
{
编译器报出"ambiguous"矛盾;
return GG;
}
else
{
没有基模板函数与参数相匹配;
这是一个"bad call",这就需要"fix code";
}
具体例子–不建议使用函数模板具体化的原因所在
先看下面一段代码
// Example 2: Explicit specialization
//
template<class T> // (a) a base template
void f( T );
template<class T> // (b) a second base template, overloads (a)
void f( T* ); // (function templates can't be partially
// specialized; they overload instead)
template<> // (c) explicit specialization of (b)
void f<>(int*);
// ...
int *p;
f( p ); // calls (c)
我们可能很自然的认为f(p)
调用的是c是很合理的一件事情,毕竟这里好像就是它”最佳匹配”,然而仓鼠发现事情并没有那么单纯.再看下面的一段代码
// Example 3: The Dimov/Abrahams Example
//
template<class T> // (a) same old base template as before
void f( T );
template<> // (c) explicit specialization, this time of (a)
void f<>(int*);
template<class T> // (b) a second base template, overloads (a)
void f( T* );
// ...
int *p;
f( p ); // calls (b)! overload resolution ignores
// specializations and operates on the base
// function templates only
这一次,f(p)
调用的实际上却是b.
为什么呢?我们可以返回头去看函数模板的使用规则.
由于函数模板没有部分具体化只有重载,所以a和b是一对并不相同的基模板.又由于c的特殊构造使得它既可以作为a的完全具体化,也可以作为b的完全具体化,所以编译器将按它声明的位置来决定c究竟是属于哪一个基模板的具体化.
很显然,在例2中它充当了b的具体化,而在例3中充当了a的具体化.
因此按照前面所说的函数模板的选用规则
在例2和例3中,首先没有非模板函数,因此编译器转入下一个步骤:寻找最佳匹配的基模板函数. 由于显然都是b基模板更像是一个”最佳匹配”,所以这两个例子中编译器都选择了b基模板. 自然地,例2中具有c这一具体化模板与参数更加匹配,所以就调用了b基模板的具体化c. 而反观例3,这一次b基模板可没有具体化的模板了,因此很自然地直接使用了c来进行实例化.
总结
可能有些读者为函数模板没有部分具体化而只能使用重载来达到类似的效果感到莫名其妙.事实上根本原因来自标准委员会所作出的规定.而为什么具体化也不参与函数重载,而必须依附于基模板而存在,这也是标准委员会所作出的规定.(事实上C++标准委员会也讨论过并且可能还在继续讨论要不要允许函数进行部分具体化)
综上所述,为了避免上面所举例子可能带来的负面作用,我们给出两点建议:
Moral_1:
优先选择重载非模板函数;
Moral_2:
确实想要使用函数具体化的话,那么像下面这样把它封装到类或结构体(静态成员)中,这样就能进行部分具体化和完全具体化了,并且不影响类外的函数重载:
// Example 4: Illustrating Moral #2
//
template<class T>
struct FImpl;
template<class T>
void f( T t ) { FImpl<T>::f( t ); } // users, don't touch this!
template<class T>
struct FImpl
{
static void f( T t ); // users, go ahead and specialize this
};