向非C ++程序员解释C ++ SFINAE

什么是C ++中的SFINAE?

你可以用一个对C ++不熟练的程序员来解释吗? 此外,SFINAE对应于像Python这样的语言中的什么概念?

警告:这是一个非常长的解释,但希望它不仅解释了SFINAE的作用,还提供了一些关于何时以及为什么使用它的知识。

好的,为了解释这个,我们可能需要备份和解释一下模板。 众所周知,Python使用通常所说的鸭式打字 – 例如,当您调用一个函数时,只要X提供函数使用的所有操作,就可以将对象X传递给该函数。

在C ++中,普通(非模板)函数要求您指定参数的types。 如果你定义了一个如下的函数:

int plus1(int x) { return x + 1; } 

只能将该函数应用于int 。 事实上,它使用x的方式, 可以适用于其他types,如longfloat没有区别 – 它只适用于一个int无论如何。

为了更接近Python的duck typing,你可以创build一个模板:

 template <class T> T plus1(T x) { return x + 1; } 

现在我们的plus1更像是在Python中 – 特别是我们可以很好地调用x + 1定义的任何types的对象x

现在考虑一下,例如我们想把一些对象写出来。 不幸的是,这些对象中的一些使用stream << object来写入stream << object ,而另一些则使用object.write(stream); 代替。 我们希望能够处理任何一个,而无需用户指定哪一个。 现在,模板专门化允许我们编写专门的模板,所以如果它是一种使用object.write(stream)语法的types,我们可以执行如下操作:

 template <class T> std::ostream &write_object(T object, std::ostream &os) { return os << object; } template <> std::ostream &write_object(special_object object, std::ostream &os) { return object.write(os); } 

对于一种types来说,这很好,如果我们想要足够糟糕的话,我们可以为所有不支持stream << object的types添加更多的特化,但是一旦(例如)用户添加了一个新的types,支持stream << object ,事情再次破裂。

我们想要的是一个方法来使用第一个专业化的任何对象,支持stream << object; ,但第二个是其他任何东西(尽pipe我们可能有时想为使用x.print(stream);对象添加第三个)。

我们可以用SFINAE做出这个决定。 要做到这一点,我们通常依靠一些其他古怪的C ++细节。 一个是使用sizeof运算符。 sizeof决定了一个types或expression式的大小,但是它通过查看所涉及的types而完全在编译时完成,而不用评估expression式本身。 例如,如果我有这样的东西:

 int func() { return -1; } 

我可以使用sizeof(func()) 。 在这种情况下, func()返回一个int ,所以sizeof(func())等于sizeof(int)

经常使用的第二个有趣的事情是数组的大小必须是正数, 而不是零。

现在,把它们放在一起,我们可以做这样的事情:

 // stolen, more or less intact from: // http://stackoverflow.com/questions/2127693/sfinae-sizeof-detect-if-expression-compiles template<class T> T& ref(); template<class T> T val(); template<class T> struct has_inserter { template<class U> static char test(char(*)[sizeof(ref<std::ostream>() << val<U>())]); template<class U> static long test(...); enum { value = 1 == sizeof test<T>(0) }; typedef boost::integral_constant<bool, value> type; }; 

这里我们有两个test重载。 其中第二个需要一个variables参数列表( ... ),这意味着它可以匹配任何types – 但它也是编译器在select一个重载时所做的最后一个select,所以它只会匹配,如果第一个不是test的另一个重载更有趣一点:它定义了一个函数,该函数接受一个参数:返回char函数的指针数组,其中数组的大小(本质上)是sizeof(stream << object) 。 如果stream << object不是一个有效的expression式,那么sizeof将产生0,这意味着我们已经创build了一个大小为零的数组,这是不允许的。 这是SFINAE自己进入图片的地方。 尝试replaceU不支持operator<<的types将失败,因为它会产生一个零大小的数组。 但是,这不是一个错误 – 它只是意味着函数从超载集中消除。 因此,其他function是唯一可以在这种情况下使用的function。

然后在下面的enumexpression式中使用它 – 它看着从选定的test重载的返回值,并检查它是否等于1(如果是,这意味着函数返回char被选中,否则,函数返回long被选中)。

结果是has_inserter<type>::value将会是l如果我们可以使用some_ostream << object; 将编译,如果不会,则为0 。 然后,我们可以使用该值来控制模板专门化,以select正确的方式来写出特定types的值。

如果您有一些重载的模板函数,则在执行模板replace时,某些可能的候选对象可能无法编译,因为被replace的对象可能不具有正确的行为。 这不被认为是一个编程错误,失败的模板只是从该特定参数可用的集合中删除。

我不知道Python是否有类似的function,也不知道为什么非C ++程序员应该关心这个function。 但是如果你想了解更多关于模板的知识,最好的书就是C ++模板:完整指南 。

SFINAE是C ++编译器用于在重载parsing期间过滤出一些模板函数重载的原理(1)

当编译器parsing特定的函数调用时,它会考虑一系列可用的函数和函数模板声明来确定将使用哪一个。 基本上有两种机制可以做到这一点。 一个可以被描述为句法。 给定声明:

 template <class T> void f(T); //1 template <class T> void f(T*); //2 template <class T> void f(std::complex<T>); //3 

parsingf((int)1)将删除版本2和3,因为对于某些Tint不等于complex<T>T* 。 同样的, f(std::complex<float>(1))会移除第二个变体,而f((int*)&x)会移除第三个变体。 编译器通过试图从函数参数中推导出模板参数。 如果扣除失败(如T*中的int ),则超载将被丢弃。

我们想要这个的原因是显而易见的 – 我们可能希望为不同types做些微不同的事情(例如,一个复杂的绝对值由x*conj(x)计算,并产生一个实数,而不是一个复数,即不同于花车的计算)。

如果你之前做过一些声明式编程,这个机制与(Haskell)类似:

 f Complex xy = ... f _ = ... 

C ++更进一步的方式是,即使推导types正确,推导也可能失败,但是将其replace为另一个会产生一些“无意义的”结果(稍后更多)。 例如:

 template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0); 

当推导f('c') (我们用一个参数调用,因为第二个参数是隐含的):

  1. 编译器将Tchar相提并论,这个char产生三个T作为char
  2. 编译器将声明中的所有Treplace为char 。 这产生了void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
  3. 第二个参数的types是指向数组int [sizeof(char)-sizeof(int)]指针。 这个数组的大小可能是例如。 -3(取决于你的平台)。
  4. 长度<= 0数组无效,因此编译器会丢弃过载。 replace失败不是错误 ,编译器不会拒绝该程序。

最后,如果有多个函数超载,则编译器使用转换序列比较和模板的部分sorting来select一个“最好”的模板。

还有更多这样的“无意义的”结果,它们被列举在标准列表(C ++ 03)中。 在C ++ 0x中,SFINAE的领域扩展到几乎任何types的错误。

我不会写一个SFINAE错误的广泛列表,但一些最stream行的是:

  • select一个没有它的types的嵌套types。 例如。 T = intT = A typename T::type其中A是没有称为type的嵌套types的type
  • 创build一个非正确大小的数组types。 举一个例子,看看这个litb的答案
  • 创build一个不是类的types的成员指针。 例如。 int C::* for C = int

这个机制与我所知的其他编程语言中的任何东西都不相似。 如果你在Haskell中做类似的事情,你会使用更强大的守卫,但在C ++中是不可能的。


1:或者在讨论类模板时使用部分模板专门化

Python根本无法帮到你。 但是你说你已经基本熟悉模板了。

最基本的SFINAE结构是使用enable_if 。 唯一棘手的部分是class enable_if封装 SFINAE,它只是暴露它。

 template< bool enable > class enable_if { }; // enable_if contains nothing… template<> class enable_if< true > { // … unless argument is true… public: typedef void type; // … in which case there is a dummy definition }; template< bool b > // if "b" is true, typename enable_if< b >::type function() {} //the dummy exists: success template< bool b > typename enable_if< ! b >::type function() {} // dummy does not exist: failure /* But Substitution Failure Is Not An Error! So, first definition is used and second, although redundant and nonsensical, is quietly ignored. */ int main() { function< true >(); } 

在SFINAE中,有一些结构设置了一个错误条件( class enable_if here)和一些并行的,否则冲突的定义。 除了一个定义之外,一些错误会发生,编译器会select并使用,而不会抱怨其他错误。

什么样的错误是可以接受的,只是最近才被标准化的一个主要细节,但是你似乎并没有问这个问题。

Python中没有任何东西与SFINAE类似。 Python没有模板,当解决模板特化时,肯定没有基于参数的函数parsing。 函数查找完全由Python中的名字完成。