该不该特化函数模板?

本文的标题改为陈述句可能更合适:为什么不该特化函数模板。

重载 v.s. 特化

为了更好的理解,我们先快速地回顾一些基础知识。
在 C++ 中,有 类模板函数模板 之别。两者的工作机理并不完全相同,最显著的区别在于重载:C++ 类没有重载,所以类模板也没有重载;然而函数有重载,所以函数模板重载也是理所当然的。看下边的例子,

// Example 1

// 类模板
template class X { /*...*/ };      // (a)

// 有两个重载版本的函数模板
template void f( T );              // (b)
template void f( int, T, double ); // (c)

这些非特化的模板也被叫做 基模板base template)。
基模板是可以被 特化的specialized)。类的基模板和函数的基模板也有个非常重要的区别:类模板即可以被 偏特化partially specialized)也可以被 全特化fully specialized);函数模板只能被 全特化,之所以如此,是因为函数模板的重载达到了偏特化的效果。看下边的例子,

// Example 1(接着上边的例子)

// 指针类型的偏特化
template class X { /*...*/ };

// int 类型的全特化
template<> class X { /*...*/ };

// (b) 和 (c) 的重载版本,即独立的基模板
// 由于没有函数偏特化,所以 (d) 不是 (b) 的偏特化版本!
template void f( T* );             // (d)

// 对 (b) 的 int 类型的全特化
template<> void f( int );              // (e)

// 普通函数,碰巧重载了 (b), (c) 和 (d),
// 注意:不是 (e) 的重载,下文会讨论这个问题
void f( double );                           // (f)

现在我们来讨论,在不同的场景下函数模板的哪个重载/特化版本会被调用:

  1. 首先考虑普通的非模板函数。如果参数类型和非模板函数匹配,那么优先考虑非模板函数。
  2. 在非模板函数不合适的情况下,再去考虑函数基模板。具体哪个基模板被选择,这要看参数类型与个数,而这又分为下边几种情况:
    1. 如果有个基模板比其他的基模板更适合,那么选择此基模板。如果此基模板又碰巧被全特化了,而且全特化后的参数类型与实参碰巧又很搭配,那么选择此全特化版本。
    2. 如果有多个基模板都很合适,那么编译器是没有办法区分的,这时候只能靠程序员指定要选择哪个版本。
    3. 如果没有基模板匹配,那么编译器报错,需要程序员去修复这个问题。

根据这些规则,看下边的例子,

// 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
void f( T );      // (a),一个基模板

template
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
void f( T );      // (a),基模板

template<>
void f<>(int*);   // (c),对 (a) 进行了全特化

template
void f( T* );     // (b),另一个基模板

int *p; 
f( p );           // calls (b)!
                  // 编译器选择了基模板 (b),而不是特化版本 (c)

如果这让你很吃惊,也别觉得奇怪,这个例子也让很多专家吃惊。要理解其实也很容易,只要记得:特化版本不能重载(Specializations don't overload)。
只有基模板才会重载(当然,非模板函数也会重载)。重新回顾一下上文给出的函数模板调用规则:

...

  1. 在非模板函数不合适的情况下,再去考虑 函数基模板。具体哪个基模板被选择,这要看参数类型与个数,而这又分为下边几种情况:
    1. 如果有个基模板比其他的基模板更适合,那么选择此基模板。如果此基模板又碰巧被全特化了,而且全特化后的参数类型与实参碰巧又很搭配,那么选择此全特化版本。
      ...etc

在编译器解析重载的时候只考虑基模板(当然如果有非模板函数更合适,则选择该非模板函数)。当基模板被选定之后,编译器才会去查看有没有合适的特化版本。

编程规则

你可能和我都有这样的疑问:我特意地写了 int* 的偏特化版本,当参数正巧满足的时候,我的这个偏特化版本就不能被调用吗?答案是我们对这种使用方法有误解:如果你希望当参数满足的时候调用指定的函数,那么你应该写个非模板函数,而不是基模板的特化函数。

特化函数不参与重载的原因很简单:如果你为函数模板写了特化函数,那么你就希望这个特化函数被调用,而如果其他人为另一个函数模板写了个特化函数,其他人也希望这个特化函数被调用,那么结果可能会不如你所愿,所以标准委员会禁止了特化函数参与重载。

编程的时候注意以下两个准则:

  1. 如果你希望定制函数基模板,而且希望这个定制的函数参与重载(也就是说,当参数合适的时候,选择这个定制的函数),那么你应该写个普通的非模板函数而不是特化函数。而当你已经对基模板写了个重载的基模板,那么也应该避免对这两个基模板的任何一个提供特化函数。
  2. 如果你正在写函数基模板,那么也应该只写这一个基模板,既不要重载也不要特化;如果希望定制基模板,你可以借助类模板来实现。
// Example 4:对 准则2 进行解释

template 
struct FImpl;

template 
void f( T t ) { FImpl::f( t ); } // 不要修改这里

template 
struct FImpl 
{ 
  static void f( T t ); // 在这里定制
};

总结

编译器在重载解析的时候对所有的函数基模板一视同仁,这和我们常见的普通非模板函数的重载一样:对于所有的模板,编译器选择那个参数最合适的。

然而函数模板的特化却并不直观。一方面,你不能偏特化基模板 -- 仅仅是因为标准委员会不允许;另一方面,函数模板的特化函数不参与重载,这意味着你写的特化函数并不影响编译器选择哪个基模板(这也正是最不直观的地方)。如果你写了个非模板函数,在参数合适的情况下编译器会优先选择这个它。

如果你正在写函数模板,最好不要重载也不要特化;如果你需要对你写的这个基模板进行定制,那么可以借助类模板来实现,通过对类模板进行偏特化/全特化来实现定制的目的,这样子就不会因为函数模板的特化导致意想不到的结果。

如果你正在使用别人写的模板函数(那个人没有使用我们介绍的借助类模板的方法),而你又想定制模板,想让模板在某些情况下按照我们的想法工作,那么写一个签名相同的普通非模板函数。

参考

本文翻译自 Herb Sutter 的一篇文章。译者在不改变原意的基础上进行了适当地修改,以方便理解。
Why Not Specialize Function Templates?

你可能感兴趣的:(该不该特化函数模板?)