Jim*_*Jim 39 c++ programming-languages c++-faq sfinae
什么是C++中的SFINAE?
能不能用不熟悉C++的程序员理解的话来解释它?另外,像Python这样的语言中的SFINAE对应的概念是什么?
Jer*_*fin 99
警告:这是一个非常长的解释,但希望它不仅能解释SFINAE的功能,还能让您了解何时以及为何使用它.
好的,为了解释这一点,我们可能需要备份和解释模板.众所周知,Python使用通常所说的鸭子类型 - 例如,当您调用函数时,只要X提供函数使用的所有操作,就可以将对象X传递给该函数.
在C++中,普通(非模板)函数要求您指定参数的类型.如果你定义了一个函数:
int plus1(int x) { return x + 1; }
Run Code Online (Sandbox Code Playgroud)
您只能将该功能应用于int
.它使用的事实,x
在一个方式可能只是以及适用于其他类型,如long
或float
没有区别-它仅适用于一个int
反正.
为了更接近Python的鸭子类型,您可以创建一个模板:
template <class T>
T plus1(T x) { return x + 1; }
Run Code Online (Sandbox Code Playgroud)
现在我们plus1
更像是在Python中 - 特别是,我们可以同样很好地调用它定义x
的任何类型的对象x + 1
.
现在,考虑一下,我们想要将一些对象写入流中.不幸的是,其中一些对象使用了写入流stream << object
,但是其他对象则使用object.write(stream);
.我们希望能够处理任何一个而无需用户指定哪一个.现在,模板专门化允许我们编写专用模板,所以如果它是一种使用object.write(stream)
语法的类型,我们可以做类似的事情:
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);
}
Run Code Online (Sandbox Code Playgroud)
这是罚款一类,如果我们想诚心的,我们可以为增加更多的特所有不支持的类型stream << object
-但只要(例如)用户添加了一个新的类型不支持stream << object
,事情再次休息.
我们要的是一个方法,可使用第一个特对任何支持对象stream << object;
,但第二次为别的(尽管我们有时可能会想添加第三个用于使用对象x.print(stream);
来代替).
我们可以使用SFINAE来做出这个决定.为此,我们通常依赖于C++的其他一些奇怪的细节.一种是使用sizeof
操作员.sizeof
确定的类型或表达式的大小,但它确实通过查看所以完全在编译时类型参与,而不计算表达式本身.例如,如果我有类似的东西:
int func() { return -1; }
Run Code Online (Sandbox Code Playgroud)
我可以用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;
};
Run Code Online (Sandbox Code Playgroud)
这里我们有两个重载test
.其中第二个需要一个变量参数列表(...
),这意味着它可以匹配任何类型-但它也是最后的选择,编译器会在选择过载使,所以它会只如果第一个不匹配不会.另一个重载test
更有趣:它定义了一个带有一个参数的函数:一个返回函数的指针数组char
,其中数组的大小(实质上)sizeof(stream << object)
.如果stream << object
不是有效的表达式,sizeof
则将产生0,这意味着我们已经创建了一个大小为零的数组,这是不允许的.这就是SFINAE本身所处的位置.试图替代不支持类型operator<<
为U
会失败,因为它会产生一个零大小的数组.但是,这不是错误 - 它只是意味着从过载集中消除了函数.因此,另一个功能是唯一可以在这种情况下使用的功能.
然后在enum
下面的表达式中使用 - 它查看所选重载的返回值test
并检查它是否等于1(如果是,则表示char
选择了返回的函数,否则,long
选择返回的函数) .
其结果是,has_inserter<type>::value
将是l
,如果我们可以用some_ostream << object;
将编译,0
如果它不会.然后,我们可以使用该值来控制模板特化,以选择正确的方式来写出特定类型的值.
小智 10
如果您有一些重载的模板函数,则在执行模板替换时可能无法编译某些可能的候选项,因为被替换的东西可能没有正确的行为.这不被认为是编程错误,简单地从可用于该特定参数的集合中移除失败的模板.
我不知道Python是否有类似的功能,并且不知道为什么非C++程序员应该关心这个功能.但是,如果您想了解有关模板的更多信息,最好的书就是C++模板:完整指南.
SFINAE是C++编译器用于在重载决策期间过滤掉一些模板化函数重载的原理(1)
当编译器解析特定的函数调用时,它会考虑一组可用的函数和函数模板声明,以找出将使用哪一个.基本上,有两种机制可以做到这一点.一个可以被描述为句法.鉴于声明:
template <class T> void f(T); //1
template <class T> void f(T*); //2
template <class T> void f(std::complex<T>); //3
Run Code Online (Sandbox Code Playgroud)
解析f((int)1)
将删除版本2和3,因为int
不等于complex<T>
或T*
某些T
.同样,f(std::complex<float>(1))
将删除第二个变体并f((int*)&x)
删除第三个变体.编译器通过尝试从函数参数中推导出模板参数来完成此操作.如果推断失败(如T*
反对int
),则丢弃重载.
我们想要这个的原因很明显 - 我们可能想为不同的类型做一些稍微不同的事情(例如,复数的绝对值是由x*conj(x)
并计算得到一个实数,而不是一个复数,这与浮点数的计算不同).
如果你之前做过一些声明性编程,这个机制类似于(Haskell):
f Complex x y = ...
f _ = ...
Run Code Online (Sandbox Code Playgroud)
C++采取这种方式的方式是,即使推导出的类型是正确的,推论也可能失败,但是反向替换到另一个会产生一些"荒谬的"结果(稍后会有更多).例如:
template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);
Run Code Online (Sandbox Code Playgroud)
推导时f('c')
(我们用单个参数调用,因为第二个参数是隐式的):
T
针对char
其产生平凡T
作为char
T
将声明中char
的所有s 替换为s.这产生了void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
.int [sizeof(char)-sizeof(int)]
.该阵列的大小可以是例如.-3(取决于您的平台).<= 0
无效,因此编译器会丢弃重载.替换失败不是错误,编译器不会拒绝该程序.最后,如果仍然存在多个函数重载,编译器将使用转换序列比较和模板的部分排序来选择一个"最佳".
还有更多这样的"荒谬"结果,它们在标准列表中列举(C++ 03).在C++ 0x中,SFINAE的领域几乎扩展到任何类型错误.
我不会写出大量的SFINAE错误列表,但最受欢迎的一些是:
typename T::type
for T = int
或T = A
where A
是没有嵌套类型的类type
.int C::*
对于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 >();
}
Run Code Online (Sandbox Code Playgroud)
在SFINAE中,有一些结构可以设置错误条件(class enable_if
这里)和一些并行的,否则相互矛盾的定义.除了一个定义之外的所有定义都会发生一些错误,编译器选择并使用它而不会抱怨其他定义.
什么样的错误是可以接受的是一个最近才被标准化的重要细节,但你似乎并没有问这个问题.