什么是C++中的SFINAE?
你能用一个不精通C++的程序员能理解的语言来解释它吗?另外,在像Python这样的语言中,SFINAE对应于什么概念?
发布于 2010-08-05 01:56:32
警告:这是一个非常长的解释,但希望它不仅真正解释了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 + 1的任何类型的对象x调用它。
现在,考虑一下,例如,我们想要将一些对象写到流中。不幸的是,其中一些对象使用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确定类型或表达式的大小,但它完全在编译时通过查看所涉及的类型来确定大小,而不计算表达式本身。例如,如果我有如下内容:
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重载。第二个参数带有一个变量参数列表( ...),这意味着它可以匹配任何类型--但这也是编译器在选择重载时的最后一个选择,所以只有当第一个参数不匹配时,它才会匹配。test的另一个重载有点有趣:它定义了一个带有一个参数的函数:一个指向返回char的函数的指针数组,其中数组的大小(本质上)是sizeof(stream << object)。如果stream << object不是一个有效的表达式,sizeof将返回0,这意味着我们已经创建了一个大小为0的数组,这是不允许的。这就是SFINAE本身的用武之地。尝试用不支持operator<<的类型替换U将会失败,因为这将产生一个零大小的数组。但是,这并不是一个错误--它只是意味着该函数已从重载集中删除。因此,另一个函数是在这种情况下唯一可以使用的函数。
然后在下面的enum表达式中使用它--它查看所选test重载的返回值并检查返回值是否等于1(如果是,则表示选择了返回char的函数,否则,表示选择了返回long的函数)。
结果是,如果我们可以使用some_ostream << object;编译,那么has_inserter<type>::value将为l,如果不能,则为0。然后,我们可以使用该值来控制模板专门化,以选择正确的方式来写出特定类型的值。
发布于 2010-08-05 00:35:33
如果你有一些重载的模板函数,当执行模板替换时,一些可能的候选函数可能无法编译,因为被替换的东西可能没有正确的行为。这不被认为是编程错误,失败的模板只是从可用于该特定参数的集合中删除。
我不知道Python是否有类似的特性,也不明白非C++程序员为什么要关心这个特性。但是如果你想了解更多关于模板的知识,最好的书就是C++ Templates: The Complete Guide。
发布于 2010-08-05 02:32:14
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,因为对于某些T,int不等于complex<T>或T*。类似地,f(std::complex<float>(1))将删除第二个变体,f((int*)&x)将删除第三个变体。编译器通过尝试从函数参数中推导出模板参数来实现这一点。如果推导失败(例如在针对int的T*中),则会丢弃过载。
我们想要这样做的原因是显而易见的-我们可能想要为不同的类型做一些稍微不同的事情(例如,复数的绝对值由x*conj(x)计算,并产生实数,而不是复数,这与浮点数的计算不同)。
如果你以前做过一些声明式编程,这种机制类似于(Haskell):
f Complex x y = ...
f _ = ...C++更进一步的方式是,即使推导出的类型是OK的,推导也可能失败,但是反向替换到其他类型会产生一些“无意义”的结果(稍后会有更多介绍)。例如:
template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);在推导f('c')时(我们使用单个参数调用,因为第二个参数是隐式的):
当char
T替换为数组s时,编译器将T与char进行匹配,这会产生微不足道的T。这会产生void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0).
T的指针。该数组的大小可以是例如。(根据platform).
<= 0 -3\f6是无效的,因此编译器会丢弃过载。替换失败不是错误,编译器不会拒绝程序。最后,如果仍然存在多个函数重载,编译器将使用转换序列比较和模板的偏序来选择“最佳”的一个。
还有更多这样的“无意义的”结果像这样工作,它们在标准(C++03)中列举在一个列表中。在C++0x中,SFINAE的领域被扩展到几乎任何类型错误。
我不会详细列出SFINAE错误,但其中一些最常见的是:
T = int或T = A的typename T::type,其中A是没有嵌套类型type.C = int的int C::*
这种机制与我所知道的其他编程语言中的任何东西都不相似。如果你要在Haskell中做类似的事情,你会使用更强大的卫士,但在C++中是不可能的。
1:在讨论类模板时使用部分模板专门化
https://stackoverflow.com/questions/3407633
复制相似问题