SFINEA in C++
作者:唐风
原载于: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 几乎是随处可见的,不可或缺的重要“原则”。:)
本文完。