该不该特化函数模板?
本文的标题改为陈述句可能更合适:为什么不该特化函数模板。
重载 v.s. 特化
为了更好的理解,我们先快速地回顾一些基础知识。
在 C++ 中,有 类模板 和 函数模板 之别。两者的工作机理并不完全相同,最显著的区别在于重载:C++ 类没有重载,所以类模板也没有重载;然而函数有重载,所以函数模板重载也是理所当然的。看下边的例子,
// Example 1
// 类模板
template<class T> class X { /*...*/ }; // (a)
// 有两个重载版本的函数模板
template<class T> void f( T ); // (b)
template<class T> void f( int, T, double ); // (c)
这些非特化的模板也被叫做 基模板(base template)。
基模板是可以被 特化的(specialized)。类的基模板和函数的基模板也有个非常重要的区别:类模板即可以被 偏特化(partially specialized)也可以被 全特化(fully specialized);函数模板只能被 全特化,之所以如此,是因为函数模板的重载达到了偏特化的效果。看下边的例子,
// Example 1(接着上边的例子)
// 指针类型的偏特化
template<class T> class X<T*> { /*...*/ };
// int 类型的全特化
template<> class X<int> { /*...*/ };
// (b) 和 (c) 的重载版本,即独立的基模板
// 由于没有函数偏特化,所以 (d) 不是 (b) 的偏特化版本!
template<class T> void f( T* ); // (d)
// 对 (b) 的 int 类型的全特化
template<> void f<int>( int ); // (e)
// 普通函数,碰巧重载了 (b), (c) 和 (d),
// 注意:不是 (e) 的重载,下文会讨论这个问题
void f( double ); // (f)
现在我们来讨论,在不同的场景下函数模板的哪个重载/特化版本会被调用:
- 首先考虑普通的非模板函数。如果参数类型和非模板函数匹配,那么优先考虑非模板函数。
- 在非模板函数不合适的情况下,再去考虑函数基模板。具体哪个基模板被选择,这要看参数类型与个数,而这又分为下边几种情况:
- 如果有个基模板比其他的基模板更适合,那么选择此基模板。如果此基模板又碰巧被全特化了,而且全特化后的参数类型与实参碰巧又很搭配,那么选择此全特化版本。
- 如果有多个基模板都很合适,那么编译器是没有办法区分的,这时候只能靠程序员指定要选择哪个版本。
- 如果没有基模板匹配,那么编译器报错,需要程序员去修复这个问题。
根据这些规则,看下边的例子,
// Example 1(接着上边的例子)
bool b;
int i;
double d;
f( b ); // calls (b) with T = bool
f( i, 42, d ); // calls (c) with T = int
f( &i ); // calls (d) with T = int
f( i ); // calls (e)
f( d ); // calls (f)
为什么不要对函数模板特化
有下边的例子,
// Example 2(新的例子)
template<class T>
void f( T ); // (a),一个基模板
template<class T>
void f( T* ); // (b),另一个基模板,对 (a) 进行了重载
template<>
void f<>(int*); // (c),对 (b) 进行了全特化
// ...
int *p;
f( p ); // calls (c)
上例的结果正是你所期望的。那么问题来了,为什么你要期望得到这样的结果呢。如果你的回答是:我写了一个对 int 指针的 (b) 特化版本,当参数是 int* 的时候就应该调用 (c),那么做好准备看看接下来的例子,
// Example 3:The Dimov/Abrahams Example
// 该例子是由 Peter Dimov 和 Dave Abrahams 提出的
template<class T>
void f( T ); // (a),基模板
template<>
void f<>(int*); // (c),对 (a) 进行了全特化
template<class T>
void f( T* ); // (b),另一个基模板
int *p;
f( p ); // calls (b)!
// 编译器选择了基模板 (b),而不是特化版本 (c)
如果这让你很吃惊,也别觉得奇怪,这个例子也让很多专家吃惊。要理解其实也很容易,只要记得:特化版本不能重载(Specializations don't overload)。
只有基模板才会重载(当然,非模板函数也会重载)。重新回顾一下上文给出的函数模板调用规则:
...
- 在非模板函数不合适的情况下,再去考虑 函数基模板。具体哪个基模板被选择,这要看参数类型与个数,而这又分为下边几种情况:
- 如果有个基模板比其他的基模板更适合,那么选择此基模板。如果此基模板又碰巧被全特化了,而且全特化后的参数类型与实参碰巧又很搭配,那么选择此全特化版本。
...etc
- 如果有个基模板比其他的基模板更适合,那么选择此基模板。如果此基模板又碰巧被全特化了,而且全特化后的参数类型与实参碰巧又很搭配,那么选择此全特化版本。
在编译器解析重载的时候只考虑基模板(当然如果有非模板函数更合适,则选择该非模板函数)。当基模板被选定之后,编译器才会去查看有没有合适的特化版本。
编程规则
你可能和我都有这样的疑问:我特意地写了 int* 的偏特化版本,当参数正巧满足的时候,我的这个偏特化版本就不能被调用吗?答案是我们对这种使用方法有误解:如果你希望当参数满足的时候调用指定的函数,那么你应该写个非模板函数,而不是基模板的特化函数。
特化函数不参与重载的原因很简单:如果你为函数模板写了特化函数,那么你就希望这个特化函数被调用,而如果其他人为另一个函数模板写了个特化函数,其他人也希望这个特化函数被调用,那么结果可能会不如你所愿,所以标准委员会禁止了特化函数参与重载。
编程的时候注意以下两个准则:
- 如果你希望定制函数基模板,而且希望这个定制的函数参与重载(也就是说,当参数合适的时候,选择这个定制的函数),那么你应该写个普通的非模板函数而不是特化函数。而当你已经对基模板写了个重载的基模板,那么也应该避免对这两个基模板的任何一个提供特化函数。
- 如果你正在写函数基模板,那么也应该只写这一个基模板,既不要重载也不要特化;如果希望定制基模板,你可以借助类模板来实现。
// Example 4:对 准则2 进行解释
template<class T>
struct FImpl;
template<class T>
void f( T t ) { FImpl<T>::f( t ); } // 不要修改这里
template<class T>
struct FImpl
{
static void f( T t ); // 在这里定制
};
总结
编译器在重载解析的时候对所有的函数基模板一视同仁,这和我们常见的普通非模板函数的重载一样:对于所有的模板,编译器选择那个参数最合适的。
然而函数模板的特化却并不直观。一方面,你不能偏特化基模板 -- 仅仅是因为标准委员会不允许;另一方面,函数模板的特化函数不参与重载,这意味着你写的特化函数并不影响编译器选择哪个基模板(这也正是最不直观的地方)。如果你写了个非模板函数,在参数合适的情况下编译器会优先选择这个它。
如果你正在写函数模板,最好不要重载也不要特化;如果你需要对你写的这个基模板进行定制,那么可以借助类模板来实现,通过对类模板进行偏特化/全特化来实现定制的目的,这样子就不会因为函数模板的特化导致意想不到的结果。
如果你正在使用别人写的模板函数(那个人没有使用我们介绍的借助类模板的方法),而你又想定制模板,想让模板在某些情况下按照我们的想法工作,那么写一个签名相同的普通非模板函数。
参考
本文翻译自 Herb Sutter 的一篇文章。译者在不改变原意的基础上进行了适当地修改,以方便理解。
Why Not Specialize Function Templates?