SFINEA in C++
SFINAE(substitution failure is not a error) 主要用于模板函数,它是指,编译器在使用具体类型来替换模板类型参数,对模板进行实例化(展开模板)时,如果发生替换失败,那么并不会直接引发编译错误(Error),而只是简单地把这个模板从重载候选者中去除掉。
还是看看代码吧(一个在SFINAE中常遇到的例子):
代码段1:
template <typename T> bool is_class(int T::*) { return true; } template <typename T> bool is_class(...) { return false; } struct Test { }; int main(void) { std::cout<<is_class<Test>(0)<<endl; std::cout<<is_class<int>(0)<<endl; }
运行的结果是输出:
1
0
这表明,如果传给 is_class 的模板参数是一个类,那么返回 true 的那个版本就会被选中,否则false的那个版本会被选中。就是因为SFINAE在起作用。
为什么要提SFINAE?
仅仅从程序员的角度来看,程序段1中,对相应函数选择的结果是非常符合直观的预期,与普通函数重载是很相似的感觉。
例如,对于下面这两个函数:
int max(int a, int b) {return a>b?a:b} float max(float a, float b) {return a>b?a:b} int main(void) { float x1=3.4f, x2=3.6f; cout<<max(x1, x2); }
对于 float 型的参数,float 版本的重载自然会很被选中。在外观上看,程序段1是一样的。那么为什么程序段1就需要特别的 SFNIAE 呢?
我想,对于普通函数的重载而言,由于这些函数的所有信息都已经完备,在发生调用之前,编译器已经可以完成对这些函数的编译,这些函数也不可能再被增加任何新的信息,可以直接产生执行代码。在函数的调用点上,编译器只需要根据参数信息选择一个合适函数的地址就可以了。
但是,对于模板函数重载,情况就不一样了。我们分析下程序段1中,is_class<int>(0) 这个调用,在第一步的选择中,无论从模板参数的个数、函数参数的个数来看,两个 is_class 的实现都可能匹配,由于 int T::* (类成员指针)的匹配优先级比 … 的要高,所以编译器会先试图使用第一个版本进行展开。但编译展开的结果时发现 int::* 是不合法的,于是编译器就放弃展开这个函数,而取另一个函数进行展开,并得到正确的调用。
所以,在真正发生调用(应该说真正需要被展开)之前,模板函数中的信息是不完备的,编译器无法为这些模板函数生成真正的执行代码,而只是进行一些很基本、简单的检查。所有的模板都不是“真正的代码”,它们是编译器用来生成代码的工具。在需要展开的时候,编译器从合适的候选者中选出优先级最高的一个来进行实例化(展开)。在展开后的代码如果不能正确被编译(像上面例子中 int::* 这种情况),编译器只是简单地放弃这次展开,转而寻找其它的模板。试想,如果编译器在展开失败后,直接产生一个编译错误的话,其它的函数就没有机会了,这是非常不合理的,因为:1.本次展开失败并不意味着被展开的模板代码就有问题,因为用其它类型的话还是有可能展开成功的。2.本次展开失败并不代表用于展开的类型无法找到合适的模板,其它模板可能合用。
所以,我觉得,SFINEA 的意义就是:
编译器在每个调用点上,只为当前需要实例化的类型寻找一个合适的模板进行展开,而不会为某一次实例化而展开所有可能合适的重载模板(函数)。
这是编译器“智能”选择模板的表现。普通函数重载则不一样,无论是否被调用,或是无论调用点需要的是什么类型的重载,编译器会将所有参与了重载的函数一个不落的全部编译。如果对模板也采用同样的方式,那么模板将受到巨大的局限而失去意义。
有了 SFINEA ,当我们在写模板代码的时候,就不需要担心这些模板在使用某些类型进行展开的时候会失败,从而造成程序编译错误,因为我们知道编译器只会在能展开的情况展开它们,展开失败的情况下,这些代码并不会真正进入你的程序中。
好了,在结束本文之前,我们再看看 SFINEA “知名”的一个例子:
程序段2:
template <typename T> class is_class { typedef char one; typedef struct {char a[2];} two; template <typename C> static one test(int C::*); template <typename C> static two test(...); public: enum {value = sizeof(test<T>(0)) == sizeof(one)}; };
这是模板圣经《C++ templates》中的一个例子(原程序可能不完全一样),与程序段 1 不同的是,is_class<T>::value 是一个编译期的 bool 值,而程序段 1 ,ture 或是 false 是在运行期才得到的结果。is_class<T>::value 这样的“装置”(device)经常出现在模板编译中,用于根据类型的某种特性(比如,是不是一个类?)来选择不同的模板。boost 中的提供了很多类似的 device,再配合 boost::enable_if 来完成威力巨大的模板编程。
可以说,SFINEA 几乎是随处可见的,不可或缺的重要“原则”。:)
本文完。
后记
这几日再学习和思考之后,又有了以下的一些收获:
在 http://www.martinecker.com/wiki/index.php?title=SFINAE_Principle 有这么一段:
To summarize, the essence of the SFINAE principle is this: If an invalid argument or return type is formed when a function template is instantiated during overload resolution, the function template instantiation is removed from the overload resolution set and does not result in a compilation error.
总结起来,SFINAE 原则的本质就是:当进行重载决议时,如果函数模板实例化后产生了无效的参数类型或是返回类型,那么这个实例化会从重载选项是去除掉,但不产生编译错误。
这句话讲得很到位。要把握两点:1、SFINAE 是在重载决议时起作用的。2、SFINAE 起作用时是因为产生了无效的参数类型或返回值类型,注意,这个类型可以是返回值的类型!(这点很重要,因为有时候你不能在参数列表上做动作,比如重载运算符的时候,编译器对参数的多少是有限定的~!)
我在前文所述:
编译器在每个调用点上,只为当前需要实例化的类型寻找一个合适的模板进行展开,而不会为某一次实例化而展开所有可能合适的重载模板(函数)。
并不贴切,只是沾到“不产生编译错误”这一小点的边。我想,SFINEA 之所以会产生,是因为模板的“侵占性”太强,匹配的面很广,结果很容易相互冲突,所以它必须存在。但“变态”的 Cpper 们,在知道了编译器有 SFINEA 这样的行为之后,利用它来做了很多原来没想过的事情,就像 OwnWaterLoo 在回复中所说到的:
而SFINEA则是刻意的利用C++语言的这种特性,刻意造成这种局面,
确是 SFINEA 应用之精髓。有例子,会更容易理解,但我这里就不再例举(因为我举不出更好的),上面的链接,以及《超越c++标准库——boost程序库导论》中介绍 enable_if 的那一节(手头上没有纸质书,无法确定章节,这一节对 SFINEA 的讲解也很漂亮),里面的例子都很好。而我自己也正好就碰上了使用 SFINEA 解决情况(点击这里),有兴趣的话可以看看(正文与回复一起)。
在结束之前,再来个“有意思”的小东西:
template <typename T> struct has_memfun_hoge { private : template < typename U, typename ... Types> [] check() -> decltype( reinterpret_cast<U *>(nullptr)->hoge(Types ... args), std::true_type) ; template < typename U > std::false_type check() ; public : static const bool value = std::is_same< check<T>(), std::true_type >::value ; } ;
这是 C++0x 中的 SFINAE ,用来判断一个类是不是有 hoge 这个函数,是不是很简单…… 呃,是的,我也是从网上抄的 :P