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

Explain C++ SFINAE to a non-C++ programmer

提问人:Jim 提问时间:8/5/2010 最后编辑:sbiJim 更新时间:11/3/2023 访问量:8147

问:

C++ 中的 SFINAE 是什么?

你能用不精通 C++ 的程序员可以理解的语言来解释它吗?另外,SFINAE 对应于 Python 等语言中的什么概念?

C 编程语言 C++-FAQ SFINAE

评论

0赞 Jim 8/5/2010
@aaa:是的,我愿意。不过,不熟悉与它们相关的所有规则。
0赞 Jerry Coffin 8/5/2010
虽然它缺少 Python 方向,但 stackoverflow.com/questions/982808/c-sfinae-examples 几乎是重复的。我不认为 Python 中真的有直接的模拟。SFINAE 主要用于模板元编程,这都发生在编译时——但 Python 大多不像 C++ 那样强烈区分编译时和运行时。
0赞 Jim 8/5/2010
@Jerry:我看过那个帖子。什么都不懂。
0赞 Anycorn 8/5/2010
也许会有所帮助吗?boost.org/doc/libs/1_43_0/libs/utility/enable_if.html
0赞 Jim 8/5/2010
@aaa:如果我说“不”,你会说我傻吗?:-|

答:

12赞 anon 8/5/2010 #1

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

我不知道 Python 是否有类似的功能,也不明白为什么非 C++ 程序员应该关心这个功能。但是,如果您想了解有关模板的更多信息,那么关于模板的最佳书籍是 C++ 模板:完整指南

评论

4赞 Jim 8/5/2010
“[我]真的不明白为什么一个非C++程序员应该关心这个功能。 <——因为我现在正在学习C++。
8赞 8/5/2010
@Jim 好吧,SFINAE 应该在你真正需要知道的事情清单上。
7赞 David Rodríguez - dribeas 8/5/2010
+1 需要注意的是,SFINAE 仅在确定替换时可用,即编译器检查签名时。一旦签名匹配并且编译器选择了特定模板,任何后续错误都是错误,编译器将不会测试其他潜在的候选模板。
3赞 Kylotan 8/5/2010
现在知道它很好,但在你对这门语言有了更多的经验之前,你不会完全理解它。我已经使用 C++ 十多年了,这不是我需要熟悉才能有效的方面。
0赞 Edward Strange 8/5/2010
@David - 是的,这是非常重要的一点,如果你遇到它,它会让你永远活着的地狱感到沮丧。
3赞 Marcelo Cantos 8/5/2010 #2

Python 中没有任何内容与 SFINAE 非常相似。Python 没有模板,当然也没有像解析模板专用化时那样基于参数的函数解析。在 Python 中,函数查找完全是按名称完成的。

评论

3赞 jalf 8/5/2010
应该吗?它回答了部分问题。
7赞 Potatoswatter 8/5/2010 #3

Python 根本帮不了你。但你确实说你已经基本熟悉模板了。

最基本的 SFINAE 结构是 的用法。唯一棘手的部分是它没有封装 SFINAE,它只是暴露了它。enable_ifclass enable_if

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

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

评论

0赞 UncleBens 8/5/2010
事实上,代码是错误的:“”和“”。 应该是一个模板,(潜在)失败的部分应该取决于模板参数吗?class "enable_if<false>" has no member "type"cannot overload functions distinguished by return type alonefunction
0赞 Potatoswatter 8/5/2010
@Uncle:哎呀,我确实过于简单化了。SFINAE 在非模板环境中不起作用!
125赞 Jerry Coffin 8/5/2010 #4

警告:这是一个非常长的解释,但希望它不仅能真正解释 SFINAE 的作用,还能说明何时以及为什么使用它。

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

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

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

只能将该函数应用于 .事实上,它的使用方式同样适用于其他类型,比如或者没有区别——它只适用于任何类型。intxlongfloatint

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

template <class T>
T plus1(T x) { return x + 1; }

现在,我们更像是在 Python 中——特别是,我们可以同样很好地调用它来定义任何类型的对象。plus1xx + 1

现在,例如,考虑我们想要将一些对象写到流中。不幸的是,其中一些对象使用 写入流,但其他对象则使用 代替。我们希望能够处理其中任何一个,而用户不必指定哪个。现在,模板专用化允许我们编写专用模板,因此,如果它是使用语法的一种类型,我们可以执行以下操作:stream << objectobject.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 << objectstream << object

我们想要的是将第一个专用化用于任何支持 的对象,但将第二个专用化用于其他任何对象(尽管我们有时可能希望为使用代替的对象添加第三个专用化)。stream << object;x.print(stream);

我们可以使用 SFINAE 来做出这一决定。为此,我们通常依赖于 C++ 的其他一些奇怪的细节。一是使用运算符。 确定类型或表达式的大小,但它完全在编译时通过查看所涉及的类型来执行此操作,而不计算表达式本身。例如,如果我有类似的东西:sizeofsizeof

int func() { return -1; }

我可以用.在本例中,返回一个 ,因此等价于 。sizeof(func())func()intsizeof(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;
};

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

然后在下面的表达式中使用它 - 它查看所选重载的返回值,并检查它是否等于 1(如果是,则表示选择了返回的函数,但除此之外,选择了返回的函数)。enumtestcharlong

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

评论

13赞 Thomas 8/5/2010
强制性的“希望我能不止一次地对此投赞成票”的评论很好的解释,我学到的东西太:)
2赞 Carlos 8/5/2010
这是一个详尽的解释,也有不错的例子。
5赞 Mike Dinsdale 8/5/2010
“指向返回的函数的指针数组” - 正如所写的那样,参数实际上不是“指向数组的指针”吗?charchar
10赞 fredoverflow 9/27/2010
“如果表达式无效,则将产生 0” -> 这是错误的。 永远不能应用于无效的表达式,而且它肯定永远不会返回 0,因为即使是空结构也会占用 1 个字节的内存。“零大小数组才是最重要的” -> 也错了。我在原始代码中使用数组的原因是我需要一种方法将任意表达式映射到某种类型。这只是实现这一目标的垫脚石。它将表达式映射到 ,数组类型构造函数将其映射到类型。现在我能拿到诺贝尔奖了吗?;-)stream << objectsizeofsizeofsizeofsize_tsize_t
10赞 fredoverflow 9/27/2010
请注意,在 C++0x 中,由于运算符,将表达式映射到类型是微不足道的。因此,第一个重载可以简化为 。请注意,数组完全不存在。我所需要的只是一个函数,该函数采用指向某个有效类型的指针。它是指向输出流的指针(C++0x 解决方案)还是指向大小无关紧要的字符数组的指针(C++ 98 解决方案)都无关紧要。所以你看,零大小的数组实际上与它完全无关。decltypetesttest(decltype(ref<std::ostream>() << val<U>())*);
9赞 jpalecek 8/5/2010 #5

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

解析将删除版本 2 和版本 3,因为不等于 或 对于某些 .同样,将删除第二个变体并删除第三个变体。编译器通过尝试从函数参数中推断模板参数来实现此目的。如果扣除失败(如 against ),则丢弃重载。f((int)1)intcomplex<T>T*Tf(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. 编译器匹配,其产生微不足道的TcharTchar
  2. 编译器将声明中的所有 S 替换为 S。这会产生 .Tcharvoid f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
  3. 第二个参数的类型是指向数组的指针。该数组的大小可能是 eg。-3(取决于您的平台)。int [sizeof(char)-sizeof(int)]
  4. 长度数组无效,因此编译器会丢弃重载。替换失败不是错误,编译器不会拒绝该程序。<= 0

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

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

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

  • 选择没有嵌套类型的嵌套类型。例如。 for 或 where 是一个没有嵌套类型的类,称为 。typename T::typeT = intT = AAtype
  • 创建非正大小的数组类型。有关示例,请参阅此 litb 的答案
  • 创建指向非类类型的成员指针。 为int C::*C = int

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


1:在谈论类模板时,或部分模板专业化

评论

0赞 musiphil 5/19/2012
sizeof(char)-sizeof(int)是 类型的表达式,所以它永远不能为负数;例如,它可能是 4294967293U。size_t