向非C ++程序员解释C ++ SFINAE
什么是C ++中的SFINAE?
你可以用一个对C ++不熟练的程序员来解释吗? 此外,SFINAE对应于像Python这样的语言中的什么概念?
警告:这是一个非常长的解释,但希望它不仅解释了SFINAE的作用,还提供了一些关于何时以及为什么使用它的知识。
好的,为了解释这个,我们可能需要备份和解释一下模板。 众所周知,Python使用通常所说的鸭式打字 – 例如,当您调用一个函数时,只要X提供函数使用的所有操作,就可以将对象X传递给该函数。
在C ++中,普通(非模板)函数要求您指定参数的types。 如果你定义了一个如下的函数:
int plus1(int x) { return x + 1; }
您只能将该函数应用于int
。 事实上,它使用x
的方式, 可以适用于其他types,如long
或float
没有区别 – 它只适用于一个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。
然后在下面的enum
expression式中使用它 – 它看着从选定的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,因为对于某些T
, int
不等于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')
(我们用一个参数调用,因为第二个参数是隐含的):
- 编译器将
T
与char
相提并论,这个char
产生三个T
作为char
- 编译器将声明中的所有
T
replace为char
。 这产生了void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
。 - 第二个参数的types是指向数组
int [sizeof(char)-sizeof(int)]
指针。 这个数组的大小可能是例如。 -3(取决于你的平台)。 - 长度
<= 0
数组无效,因此编译器会丢弃过载。 replace失败不是错误 ,编译器不会拒绝该程序。
最后,如果有多个函数超载,则编译器使用转换序列比较和模板的部分sorting来select一个“最好”的模板。
还有更多这样的“无意义的”结果,它们被列举在标准列表(C ++ 03)中。 在C ++ 0x中,SFINAE的领域扩展到几乎任何types的错误。
我不会写一个SFINAE错误的广泛列表,但一些最stream行的是:
- select一个没有它的types的嵌套types。 例如。
T = int
或T = A
typename T::type
其中A
是没有称为type
的嵌套types的type
。 - 创build一个非正确大小的数组types。 举一个例子,看看这个litb的答案
- 创build一个不是类的types的成员指针。 例如。
int C::*
forC = 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中的名字完成。