该不该特化函数模板?

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

重载 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)

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

  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<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)。
只有基模板才会重载(当然,非模板函数也会重载)。重新回顾一下上文给出的函数模板调用规则:

...

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

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

编程规则

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容