首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >向非C++程序员解释C++ SFINAE

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

Stack Overflow用户
提问于 2010-08-05 00:28:33
回答 5查看 6.2K关注 0票数 48

什么是C++中的SFINAE?

你能用一个不精通C++的程序员能理解的语言来解释它吗?另外,在像Python这样的语言中,SFINAE对应于什么概念?

EN

回答 5

Stack Overflow用户

回答已采纳

发布于 2010-08-05 01:56:32

警告:这是一个非常长的解释,但希望它不仅真正解释了SFINAE的作用,而且给出了一些关于何时以及为什么可能使用它的想法。

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

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

代码语言:javascript
复制
int plus1(int x) { return x + 1; }

您只能将该函数应用于int。它使用x的方式也可以应用于其他类型,如longfloat,这并没有什么不同--它只适用于int

为了更接近Python的鸭子类型,您可以改为创建一个模板:

代码语言:javascript
复制
template <class T>
T plus1(T x) { return x + 1; }

现在,我们的plus1更像是在Python语言中--特别是,我们可以同样好地对定义了x + 1的任何类型的对象x调用它。

现在,考虑一下,例如,我们想要将一些对象写到流中。不幸的是,其中一些对象使用stream << object写入流,但其他对象使用object.write(stream);。我们希望能够在不需要用户指定的情况下处理其中的任何一个。现在,模板专门化允许我们编写专门化的模板,所以如果它是一种使用object.write(stream)语法的类型,我们可以这样做:

代码语言:javascript
复制
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确定类型或表达式的大小,但它完全在编译时通过查看所涉及的类型来确定大小,而不计算表达式本身。例如,如果我有如下内容:

代码语言:javascript
复制
int func() { return -1; }

我可以使用sizeof(func())。在本例中,func()返回一个int,因此sizeof(func())等同于sizeof(int)

第二个经常使用的有趣的东西是数组的大小必须是正的,而不是

现在,把这些放在一起,我们可以这样做:

代码语言:javascript
复制
// 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。然后,我们可以使用该值来控制模板专门化,以选择正确的方式来写出特定类型的值。

票数 119
EN

Stack Overflow用户

发布于 2010-08-05 00:35:33

如果你有一些重载的模板函数,当执行模板替换时,一些可能的候选函数可能无法编译,因为被替换的东西可能没有正确的行为。这不被认为是编程错误,失败的模板只是从可用于该特定参数的集合中删除。

我不知道Python是否有类似的特性,也不明白非C++程序员为什么要关心这个特性。但是如果你想了解更多关于模板的知识,最好的书就是C++ Templates: The Complete Guide

票数 10
EN

Stack Overflow用户

发布于 2010-08-05 02:32:14

SFINAE是C++编译器在重载解析过程中用来过滤掉一些模板化函数重载的原则(1)

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

代码语言:javascript
复制
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,因为对于某些Tint不等于complex<T>T*。类似地,f(std::complex<float>(1))将删除第二个变体,f((int*)&x)将删除第三个变体。编译器通过尝试从函数参数中推导出模板参数来实现这一点。如果推导失败(例如在针对intT*中),则会丢弃过载。

我们想要这样做的原因是显而易见的-我们可能想要为不同的类型做一些稍微不同的事情(例如,复数的绝对值由x*conj(x)计算,并产生实数,而不是复数,这与浮点数的计算不同)。

如果你以前做过一些声明式编程,这种机制类似于(Haskell):

代码语言:javascript
复制
f Complex x y = ...
f _           = ...

C++更进一步的方式是,即使推导出的类型是OK的,推导也可能失败,但是反向替换到其他类型会产生一些“无意义”的结果(稍后会有更多介绍)。例如:

代码语言:javascript
复制
template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);

在推导f('c')时(我们使用单个参数调用,因为第二个参数是隐式的):

char

  • the编译器将声明中的所有T替换为数组s时,编译器将Tchar进行匹配,这会产生微不足道的T。这会产生void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0).

  • The类型的第二个参数是指向数组T的指针。该数组的大小可以是例如。(根据platform).

  • Arrays的长度,-3 \f25 <= 0 -3\f6是无效的,因此编译器会丢弃过载。替换失败不是错误,编译器不会拒绝程序。

最后,如果仍然存在多个函数重载,编译器将使用转换序列比较和模板的偏序来选择“最佳”的一个。

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

我不会详细列出SFINAE错误,但其中一些最常见的是:

  • 选择没有嵌套类型的嵌套类型。例如:用于T = intT = Atypename T::type,其中A是没有嵌套类型type.
  • creating的类,是非正大小的数组类型。有关示例,请参见this litb's answer
  • creating一个指向非类类型的成员指针。例如:用于C = int

int C::*

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

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

票数 9
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/3407633

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档