美文网首页
Why Not Specialize Function Temp

Why Not Specialize Function Temp

作者: 池塘游泳的蜗牛 | 来源:发表于2018-01-03 21:06 被阅读0次

原文地址: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里面使用类模版包装的方法),并且想写一个特殊情况下的重载版本,不要特化它,只要写一个相同函数签名的普通函数就可以了。

相关文章

网友评论

      本文标题:Why Not Specialize Function Temp

      本文链接:https://www.haomeiwen.com/subject/xquknxtx.html