C ++中的SFINAE是什么?
请您用不熟悉C ++的程序员用可理解的词语解释一下吗?另外,像Python这样的语言中的SFINAE对应于什么概念?
答案 0 :(得分:98)
警告:这是真正长的解释,但希望它不仅可以解释SFINAE的功能,还可以了解您何时以及为何使用它。
好的,为了解释这一点,我们可能需要备份并解释模板。众所周知,Python使用通常所说的鸭子类型 - 例如,当您调用函数时,只要X提供函数使用的所有操作,就可以将对象X传递给该函数。
在C ++中,普通(非模板)函数要求您指定参数的类型。如果您定义了如下函数:
int plus1(int x) { return x + 1; }
您只能 将该功能应用于int
。它以可以的方式使用x
同样适用于long
或float
等其他类型的事实没有任何区别 - 它仅适用于反正int
。
为了更接近Python的鸭子类型,您可以改为创建模板:
template <class T>
T plus1(T x) { return x + 1; }
现在我们的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);
}
对于一种类型,这很好,如果我们想要足够严重,我们可以为所有不支持stream << object
的类型添加更多特化 - 但是只要(对于例如)用户添加了一个不支持stream << object
的新类型,事情再次中断。
我们想要的是对支持stream << object;
的任何对象使用第一个特化的方法,但对于其他任何对象使用第二个特殊化(尽管我们有时可能希望为使用x.print(stream);
的对象添加第三个特殊化代替)。
我们可以使用SFINAE来做出这个决定。为此,我们通常依赖于C ++的其他一些奇怪的细节。一种是使用sizeof
运算符。 sizeof
确定类型或表达式的大小,但它完全在编译时通过查看所涉及的 types 来完成,而不评估表达式本身。例如,如果我有类似的东西:
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
重载。第二个采用变量参数列表(...
),这意味着它可以匹配任何类型 - 但它也是编译器在选择重载时所做的最后选择,所以它只 < / em>匹配,如果第一个不。 test
的另一个重载更有趣:它定义了一个带有一个参数的函数:一个指向返回char
的函数的指针数组,其中数组的大小(本质上){ {1}}。如果sizeof(stream << object)
不是有效表达式,stream << object
将产生0,这意味着我们创建了一个大小为零的数组,这是不允许的。这就是SFINAE本身所处的位置。尝试将不支持sizeof
的类型替换为operator<<
会失败,因为它会产生一个零大小的数组。但是,这不是错误 - 它只是意味着从过载集中消除了函数。因此,另一个功能是唯一可以在这种情况下使用的功能。
然后在下面的U
表达式中使用 - 它查看所选的enum
重载的返回值,并检查它是否等于1(如果是,则表示函数选择了返回test
,但是,选择了返回char
的函数。)
如果我们可以使用long
进行编译,那么has_inserter<type>::value
将是l
,如果不能,some_ostream << object;
将是0
。然后我们可以使用该值来控制模板特化,以选择正确的方式来写出特定类型的值。
答案 1 :(得分:10)
如果您有一些重载的模板函数,在执行模板替换时可能无法编译某些可能的候选项,因为被替换的东西可能没有正确的行为。这不被认为是编程错误,简单地从可用于该特定参数的集合中移除失败的模板。
我不知道Python是否有类似的功能,并且不知道为什么非C ++程序员应该关心这个功能。但是,如果您想了解有关模板的更多信息,最好的书籍是C++ Templates: The Complete Guide。
答案 2 :(得分:7)
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
解析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 _ = ...
C ++采取这种方式的方式是,即使推导出的类型是正确的,推论也可能失败,但是反向替换到另一个会产生一些“荒谬的”结果(稍后会详细介绍)。例如:
template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);
推导f('c')
时(我们用一个参数调用,因为第二个参数是隐含的):
T
与char
匹配,T
为char
T
替换为char
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
或T = int
T = A
其中A
是没有名为type
的嵌套类型的类。int C::*
C = int
这种机制与我所知道的其他编程语言中的任何内容都不相似。如果你在Haskell中做类似的事情,你会使用更强大的守卫,但在C ++中是不可能的。
1:或谈论类模板时的部分模板专业化
答案 3 :(得分:5)
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
)和一些并行的,否则相互矛盾的定义。除了一个定义之外的所有定义都会发生一些错误,编译器选择并使用它而不会抱怨其他定义。
哪种错误是可以接受的,这是一个最近才被标准化的重要细节,但你似乎并不是在问这个问题。
答案 4 :(得分:3)
Python中没有任何东西可以远程类似SFINAE。 Python没有模板,当然也没有解析模板特化时出现的基于参数的函数解析。函数查找完全由Python中的名称完成。