Why Not Specialize Function Templates?

原文地址:http://www.gotw.ca/publications/mill17.htm
原作者:Herb Sutter

  为什么函数模版的全特化是不参与函数重载的呢?而为什么函数模版没有偏特化概念呢?其实是C++语法规定的,但是在平时的工作过程中,出现过因为函数版本不能偏特化困扰我们的工作吗?答案是没有,也许很多人忽略了这个问题,主要是因为可以通过函数重载来规避这个问题(或者可以认为这不是一个问题)。另,在《C++ Templates》一书的附录B部分对重载解析做过简单介绍,可以参考阅读。

函数模版重载解析的优先级(假设有普通函数,模版函数,模版函数特化等等复杂情况):

1,普通函数,如果类型匹配,优先选中,重载解析结束.

(2) 如果没有普通函数匹配,那么所有的基础函数模版进入候选,编译器开始平等挑选,类型最匹配的,则被选中,注意,此时才会进入第(3)步继续筛选;

(3) 如果第(2)步里面选中了一个模版基础函数,则查找这个模版基础函数是否有全特化版本,如果有且类型匹配,则选中全特化版本,重载解析结束,否则使用(2)里面选中的模版函数,重载解析依然结束。

(4) 如果第(2)步里面没有选中任何函数基础模版,那么匹配失败,编译器会报错,程序员需要检查下代码。


  函数模版的全特化版本不参与函数重载解析,并且优先级低于函数基础模版参与匹配的原因是:C++标准委员会认为如果因为程序员随意写了一个函数模版的全特化版本,而使得原先的重载函数模板匹配结果发生改变(也就是改变了约定的重载解析规则)是不能接受的。

函数模版的全特化到底是哪个函数基础模版的特化,需要参考可见原则,也就是说当特化版本声明时,它只可能特化的是当前编译单元已经定义的函数基础模版。

鉴于上面两个原因,为何还要进行函数模版全特化把自己搞晕呢?!因为函数的全特化的版本和定义一个普通函数基本上一样,把模版声明去掉即可,而且普通函数的重载优先级最高,也就不会踩一些坑了。

重要区别:重载和特化

重要的是要确保我们有以下的条款,因此这里我们快速回顾一下。

在C++里面,有类模板和函数模板。这两种模板并不是完全以相同的方式工作的,并且在重载上面有明显的区别。老版本的C++里面类并不支持重载(译者注:现在也不支持),因此类模版也不支持重载。而一直以来,C++函数都是支持重载的,因此函数模版也就支持重载了。看起来只是一件很自然的事情,例子1进行了一个简单的总结:

// 例1:类模版、函数模版以及重载

//

// 类模版

template<class T> class X {/.../ }; // (a)

// 函数模版以及函数模版重载

template<class T> void f( T); // (b)

template<class T> void f( int, T,double ); // (c)

未特化的模版通常也叫做底层基础模版。

此外,基础模版一般都是可以特化的。但是特化对于类版本和函数模版来说有很大的不同,并且这个差异跟我们下面的讨论有比较重要的关系。类模版可以偏特化和全特化,而函数模版只能进行全特化,但是由于函数模版可以重载,我们通过重载可以获得和偏特化几乎相同的效果。下面的代码说明了这些不同点:

// 续例1:模版特化

//

// (a)模版针对指针类型的偏特化

template<class T> classX<T> { /...*/ };

// (a)模版针对int类型的全特化

template<> class X<int> {/.../ };

// 重载(b)和(c)的独立基础模版

// 需要注意的是,它不是(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:重载解决方案

//

bool b;

int i;

double d;

f( b); // 调用(b)模版 模版参数T = bool

f( i, 42, d ); // 调用(c)模版 T = int

f( &i); // 调用(d)模版 T = int

f( i); // 调用(e)模版

f( d); // 调用(f)函数

目前为止,我都是选择的一些最简单的情况,下面我们即将涉足该问题的一些误区或者说陷阱,去看看一些复杂的情况。

为什么不要特化:Dimov/Abrahams的例子

思考下面的代码:

// 例子2:显示特化

//

template<class T> // (a) 一个基础模版

void f( T );

template<class T> // (b) 另一个基础模版,(a)的重载版本

void f( T* ); // (函数模版没有偏特化;可以用重载替代)

template<> // (c) (b)模版的全特化

void f<>(int*);

// ...

int *p;

f( p); // 调用(c)

例子2里面最后一行选择调用的模版可能正是你所期望的,不过,问题是,它为什么是你所期望的结果呢?如果你期望的原因是错误,接下来发生事情可能令你更加惊讶。也许,有人可能会说,我给int指针类型写了一个专门的特化版本啊,因此这次调用理所当然就应该是选这个特化版本啊,然而,这正是错误的原因。

再考虑下下面的代码,这个例子是由Peter Dimov和Dave Abrahams提出的:

// 例子3:Dimov 和Abrahams提供的例子

//

template<class T> // (a) 和之前一样的基础模版

void f( T );

template<> // (c) 针对(a)的完全特化

void f<>(int*);

template<class T> // (b) 另一个基础模版,对(a)的重载

void f( T* );

// ...

int *p;

f( p); // 调用的是(b)!重载解析规则导致特化版本被忽略了
// 因为有基础模版优先匹配了

  如果你也感到吃惊,恭喜你,你并不是第一个对此感到惊讶的人。很多专家级别的程序员也对此感到惊讶,其实理解这个问题关键并不复杂,那就是函数全特化并不参与重载!

只有基础模版是可以重载的(当然,也包括非模版函数)。再回顾下前面给出的重载解析规则,这次我特意高亮了一些关键词:
……
  如果没有非函数模版匹配,那么函数基础模版作为第二优先级进入候选,但是具体哪个函数基础模版被选择,还需要根据两个点来确定,一点比较肯定就是参数匹配度要是最高的,另一个点就是根据一套比较晦涩的规则来判定哪个模版是“最特殊的”:

  如果很明显有一个“最特殊的”函数基础模版,那么它就被选中了。而如果这个基础模版恰好有一个专门为参数类型特化的版本,那么特化版本优先被选中,否则基础模版被选中。

……等等。重载解析规则只会先从基本模版(或者非模版函数)里面进行挑选。当某个基础模版被选中之后,选择会被锁定,此时,编译器会针对这个基本模版进行搜索,检查是否恰好有一个更合适的全特化模版,如果有的话,那么就会使用这个特化模版。

重要条款
如果你跟我一样,第一次看到这种情况时,可能会问这种问题:“嗯,我已经针对参数是int指针类型写了一个专门的特化版本,而且这里刚好是一个参数是int类型的调用,恰好匹配啊,那为什么我写的特化版本没有被选中呢?”可惜,呃,这个想法是错误的:如果这种情况下(译注:指用int*参数进行调用)你想你的特化版本能被选中,其实你把特化模版它改成一个普通函数就可以做到。
  要理解为什么模版函数的特化(译注:这里指模版函数的全特化,模版函数没有偏特化)不参与函数重载看起来挺怪异,但是一旦解释清楚了也很简单。因为这个原因可能跟你想的恰恰相反:如果因为你针对一个特殊的模版写了一个特化版本,就要改变模版的选择规则,这回惊讶的就是标准委员会了(译注:意思就是标准委员会规定了特化版本不能改变模版选择规则)。在这样的逻辑或者标准下,如果我们已经有办法让函数调用选择我们想选择的版本(译注:譬如上面提到的用普通函数)(我们做的仅仅是让他成为一个普通函数,而不是一个特化),这样也许我们能够更加深刻的理解为什么特化版本不会影响模版的选择过程。
  条款1:如果你正在定制一个函数基础模版,并且希望有重载版本(在一些特殊情况下有专门的精确匹配版本),建议你使用普通的函数重载,而不要使用特化。并且,如果你提供了重载的函数模版,也尽量不要特化它。 但是,你不仅仅是在使用函数模版,而是在写一个函数模版(译者注:可以认为你是公共库的作者),那么怎样才能让函数模版的使用者(包括自己),不遇到前面出现的那个问题呢?实际上,你还可以这样做(参考条款#2):
  条款2:如果你正在写一个函数基础模版,并且希望它成为一个唯一的函数模版,绝不对它进行特化和重载,那么你可以把这个函数模版实现为包含一个相同签名的静态函数的简单类模版。每个人都可以对它进行特化和偏特化,但是不会对重载解析规则产生任何影响。

总结

  重载函数模版是一种不错的方式,重载解析过程会平等的对待所有的基础模版,而且整个解析过程和普通函数重载是类似的,令人感觉很自然,符合期望,所有可见的重载函数模版都纳入重载解析规则,然后编译器从里面选出一个最合适的。
  特化函数模版就是一个不太直观的事情,首先,你还不能偏特化函数模版——原因是C++语言标准规定了!其次,函数模版特化后的版本还不参与重载。也就是说,任何函数模版的特化版本都不影响重载解析过程,这种行为可能违背了大多数人的直觉。然而,如果你写了一个非模版函数用来完全替代某函数模版的特化版本(译注:函数签名一样,其实把函数模版的特化版本的模版声明去掉就变成了普通函数),这个普通函数反而会被优先选择,因为根据C++标准,普通函数总是应该认为比模版函数的匹配度要好。

  如果你正在写一个函数模版,希望它成为一个不会被特化或者重载的唯一的函数模版,并且你在一个类模版中实现了这个函数模版,这可能是一个众所周知的绕过函数模版局限性和陷阱(译注:指函数模版没有偏特化和前面遇到重载解析的问题)的好办法。使用这种方法,程序员可以在类模版的基础上不受限制的使用偏特化和全特化(显示特化),并且不会影响到原始的函数模版的任何期望的动作。这样就避免了函数模版的两个限制,不能偏特化,以及某些情况下因为函数特化版本不参与重载带来的令人惊讶的的效应。至此,问题全部解决。如果你正在使用某人写的老式的函数模版(没有使用上面例子4里面使用类模版包装的方法),并且想写一个特殊情况下的重载版本,不要特化它,只要写一个相同函数签名的普通函数就可以了。

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

推荐阅读更多精彩内容