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

时间:2010-08-04 16:28:33

标签: c++ programming-languages c++-faq sfinae

C ++中的SFINAE是什么?

请您用不熟悉C ++的程序员用可理解的词语解释一下吗?另外,像Python这样的语言中的SFINAE对应于什么概念?

5 个答案:

答案 0 :(得分:98)

警告:这是真正长的解释,但希望它不仅可以解释SFINAE的功能,还可以了解您何时以及为何使用它。

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

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

int plus1(int x) { return x + 1; }

您只能 将该功能应用于int。它以可以的方式使用x同样适用于longfloat等其他类型的事实没有任何区别 - 它仅适用于反正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')时(我们用一个参数调用,因为第二个参数是隐含的):

  1. 编译器将Tchar匹配,Tchar
  2. 编译器将声明中的所有T替换为char s。这会产生void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
  3. 第二个参数的类型是指向数组int [sizeof(char)-sizeof(int)]的指针。该阵列的大小可以是例如。 -3(取决于您的平台)。
  4. 长度为<= 0的数组无效,因此编译器会丢弃重载。 替换失败不是错误,编译器不会拒绝该程序。
  5. 最后,如果仍然存在多个函数重载,编译器将使用转换序列比较和模板的部分排序来选择一个“最佳”函数。

    还有更多这样的“荒谬”结果,它们在标准列表中列举(C ++ 03)。在C ++ 0x中,SFINAE的领域几乎扩展到任何类型错误。

    我不会写出大量的SFINAE错误列表,但最受欢迎的一些是:

    • 选择没有它的类型的嵌套类型。例如。 typename T::typeT = int T = A其中A是没有名为type的嵌套类型的类。
    • 创建非正大小的数组类型。有关示例,请参阅this litb's answer
    • 创建指向不是类的类型的成员指针。例如。 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中的名称完成。