48

C++ 中的 SFINAE 是什么?

您能否用不懂 C++ 的程序员可以理解的语言解释一下?另外,SFINAE 对应于 Python 这样的语言中的什么概念?

4

5 回答 5

118

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

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

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

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

只能将该功能应用于int. 事实上,它的使用方式x同样适用于其他类型,例如或没有区别——它只适用于无论如何。longfloatint

为了更接近 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确定类型或表达式的大小,但它完全是在编译时通过查看所涉及的类型来完成的,而不评估表达式本身。例如,如果我有类似的东西:

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. 其中第二个采用变量参数列表(the ...),这意味着它可以匹配任何类型——但它也是编译器在选择重载时做出的最后选择,因此只有在第一个不匹配时才会匹配。的另一个重载test更有趣:它定义了一个带有一个参数的函数:一个指向返回函数的指针数组char,其中数组的大小是(本质上)sizeof(stream << object)。如果stream << object不是有效的表达式,sizeof则将产生 0,这意味着我们创建了一个大小为零的数组,这是不允许的。这就是 SFINAE 本身出现的地方。试图替换不支持的operator<<类型U会失败,因为它会产生一个大小为零的数组。但是,这不是错误——它只是意味着从重载集中消除了该函数。因此,其他功能是在这种情况下唯一可以使用的功能。

然后在enum下面的表达式中使用它 - 它查看所选重载的返回值test并检查它是否等于 1(如果是,则表示选择了返回的函数char,否则,选择了返回的函数long) .

结果是,如果我们可以使用has_inserter<type>::value将编译,如果它不会。然后,我们可以使用该值来控制模板专业化,以选择正确的方式来写出特定类型的值。lsome_ostream << object;0

于 2010-08-04T17:56:32.043 回答
11

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

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

于 2010-08-04T16:35:32.747 回答
9

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

resolvef((int)1)将删除版本 2 和 3,因为intis not equal to complex<T>or T*for some 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将声明中的所有 s 替换为chars。这产生void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0).
  3. 第二个参数的类型是指向数组的指针int [sizeof(char)-sizeof(int)]。这个数组的大小可以是例如。-3(取决于您的平台)。
  4. 长度数组<= 0无效,因此编译器会丢弃重载。替换失败不是错误,编译器不会拒绝程序。

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

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

我不会写一个广泛的 SFINAE 错误列表,但一些最受欢迎的是:

  • 选择没有它的类型的嵌套类型。例如。typename T::typeforT = intT = AwhereA是一个没有嵌套类型的类,称为type.
  • 创建一个非正大小的数组类型。例如,请参阅这个 litb 的答案
  • 创建指向不是类的类型的成员指针。例如。int C::*为了C = int

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


1:或在谈论类模板时部分模板特化

于 2010-08-04T18:32:13.603 回答
7

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此处)和许多并行的,否则会相互冲突的定义。除了一个定义,编译器选择并使用它而不抱怨其他的定义中都出现了一些错误。

什么样的错误可以接受是最近才标准化的一个主要细节,但你似乎没有问这个问题。

于 2010-08-04T17:25:48.567 回答
3

Python 中没有任何东西与 SFINAE 很相似。Python 没有模板,当然也没有解析模板特化时发生的基于参数的函数解析。在 Python 中,函数查找纯粹是按名称完成的。

于 2010-08-04T16:35:46.160 回答