【问题标题】:Explain C++ SFINAE to a non-C++ programmer向非 C++ 程序员解释 C++ SFINAE
【发布时间】:2011-03-25 08:55:36
【问题描述】:

什么是 C++ 中的 SFINAE?

你能用不懂C++的程序员可以理解的语言解释一下吗?还有,SFINAE对应Python这样的语言中的什么概念?

【问题讨论】:

  • @aaa:是的,我愿意。不熟悉与他们相关的所有规则。
  • 虽然它缺乏 Python 方向,但 stackoverflow.com/questions/982808/c-sfinae-examples 几乎是重复的。我不认为 Python 中真的有直接的类比。 SFINAE 主要用于模板元编程,这一切都发生在编译时——但 Python 大多不像 C++ 那样区分编译时和运行时。
  • @Jerry:我看过那个帖子。什么都不懂。
  • @aaa:如果我说“不”,你会说我笨吗? :-|

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


【解决方案1】:

警告:这是一个非常长的解释,但希望它不仅能真正解释 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 + 1 的任何类型的对象x

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

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 &lt;&lt; object的类型添加更多特化——但尽快(例如)用户添加了一个不支持 stream &lt;&lt; object 的新类型,事情又中断了。

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

然后在下面的enum 表达式中使用它——它查看来自test 的选定重载的返回值并检查它是否等于1(如果是,则意味着函数返回char被选中,否则,返回long的函数被选中)。

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

【讨论】:

  • 强制性的“希望我能多次投票”评论很好的解释,我也学到了一些东西:)
  • 这是一个详尽的解释,也有很好的例子。
  • “指向返回 char 的函数的指针数组” - 所写的参数实际上不是“指向 char 数组的指针”吗?
  • "如果stream &lt;&lt; object 不是一个有效的表达式,sizeof 将产生 0" -> 这是错误的。 sizeof 永远不能应用于无效表达式,而且它肯定永远不会返回 0,因为即使是空结构也会占用 1 个字节的内存。 “零大小的数组很重要”->也是错误的。我在原始代码中使用数组的原因是我需要一种将任意表达式映射到某种类型的方法。 sizeof 只是实现该目标的垫脚石。它将表达式映射到size_t,数组类型构造函数将size_t 映射到类型。现在我得到我的诺贝尔奖了吗? ;-)
  • 请注意,在 C++0x 中,由于decltype 运算符,将表达式映射到类型是微不足道的。第一个test 重载因此可以简化为test(decltype(ref&lt;std::ostream&gt;() &lt;&lt; val&lt;U&gt;())*);。请注意完全没有数组。我所需要的只是一个函数,它接受一个指向某个有效类型的指针。它是指向输出流的指针(C++0x 解决方案)还是指向大小无关紧要的字符数组的指针(C++98 解决方案)都没有关系。所以你看,零大小的数组真的和它一点关系都没有。
【解决方案2】:

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

可接受的错误类型是最近才标准化的一个主要细节,但您似乎没有问这个问题。

【讨论】:

  • 事实上,代码是错误的:“class "enable_if&lt;false&gt;" has no member "type"”和“cannot overload functions distinguished by return type alone”。 function 应该是一个模板,并且(可能)失败的部分应该依赖于模板参数?
  • @Uncle:哎呀,我确实过于简单化了。 SFINAE 不能在非模板环境中工作!
【解决方案3】:

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,因为对于某些Tint 不等于complex&lt;T&gt;T*。同样,f(std::complex&lt;float&gt;(1)) 将删除第二个变体,f((int*)&amp;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. 编译器将声明中的所有Ts 替换为chars。这会产生void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
  3. 第二个参数的类型是指向数组int [sizeof(char)-sizeof(int)]的指针。这个数组的大小可以是例如。 -3(取决于您的平台)。
  4. 长度为&lt;= 0 的数组无效,因此编译器会丢弃重载。 替换失败不是错误,编译器不会拒绝程序。

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

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

我不会写一个详尽的 SFINAE 错误列表,但其中一些最受欢迎的是:

  • 选择没有嵌套类型的类型。例如。 typename T::type 代表T = intT = A,其中A 是一个没有嵌套类型的类,称为type
  • 创建一个非正大小的数组类型。例如,请参阅this litb's answer
  • 创建指向非类类型的成员指针。例如。 int C::*C = int

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


1: 或者说类模板时的部分模板特化

【讨论】:

  • sizeof(char)-sizeof(int)size_t 类型的表达式,所以它永远不会是负数;例如,它可能是 4294967293U。
【解决方案4】:

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

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

【讨论】:

  • “[我] 真的不明白为什么非 C++ 程序员应该关心这个特性。”
  • @Jim 好吧,SFINAE 应该在你真正需要知道的事情清单上。
  • +1 需要注意的是,SFINAE 仅在确定替换时可用,即编译器检查签名时。一旦签名匹配并且编译器选择了一个特定的模板,任何以后的错误都是一个错误,编译器将不会测试其他潜在的模板候选。
  • 现在知道它很好,但是在您对该语言有更多经验之前,您不会完全理解它。我已经使用 C++ 十多年了,这不是我需要熟悉才能有效的方面。
  • @David - 是的,这是非常重要的一点,如果你遇到它,它会让你感到沮丧。
【解决方案5】:

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

【讨论】:

  • 应该吗?它回答了部分问题。
猜你喜欢
  • 2010-10-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多