SFINEA in C++

青春壹個敷衍的年華 提交于 2020-04-06 02:44:01

SFINEA in C++

作者:唐风
出处:
http://www.cnblogs.com/liyiwen
本文版权归作者和博客园共有,欢迎转载,但请保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!