解释一下 C++ SFINAE 到非 C++程序员

发布于 2024-09-12 16:28:00 字数 94 浏览 4 评论 0原文

C++ 中的 SFINAE 是什么?

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

What is SFINAE in C++?

Can you please explain it in words understandable to a programmer who is not versed in C++? Also, what concept in a language like Python does SFINAE correspond to?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

我喜欢麦丽素 2024-09-19 16:28:00

警告:这是一个非常的解释,但希望它不仅真正解释了 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 确定类型或表达式的大小,但它完全在编译时通过查看所涉及的类型来确定,而不评估表达式本身。例如,如果我有类似的内容:

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

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

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

Warning: this is a really long explanation, but hopefully it really explains not only what SFINAE does, but gives some idea of when and why you might use it.

Okay, to explain this we probably need to back up and explain templates a bit. As we all know, Python uses what's commonly referred to as duck typing -- for example, when you invoke a function, you can pass an object X to that function as long as X provides all the operations used by the function.

In C++, a normal (non-template) function requires that you specify the type of a parameter. If you defined a function like:

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

You can only apply that function to an int. The fact that it uses x in a way that could just as well apply to other types like long or float makes no difference -- it only applies to an int anyway.

To get something closer to Python's duck typing, you can create a template instead:

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

Now our plus1 is a lot more like it would be in Python -- in particular, we can invoke it equally well to an object x of any type for which x + 1 is defined.

Now, consider, for example, that we want to write some objects out to a stream. Unfortunately, some of those objects get written to a stream using stream << object, but others use object.write(stream); instead. We want to be able to handle either one without the user having to specify which. Now, template specialization allows us to write the specialized template, so if it was one type that used the object.write(stream) syntax, we could do something like:

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);
}

That's fine for one type, and if we wanted to badly enough we could add more specializations for all the types that don't support stream << object -- but as soon as (for example) the user adds a new type that doesn't support stream << object, things break again.

What we want is a way to use the first specialization for any object that supports stream << object;, but the second for anything else (though we might sometime want to add a third for objects that use x.print(stream); instead).

We can use SFINAE to make that determination. To do that, we typically rely on a couple of other oddball details of C++. One is to use the sizeof operator. sizeof determines the size of a type or an expression, but it does so entirely at compile time by looking at the types involved, without evaluating the expression itself. For example, if I have something like:

int func() { return -1; }

I can use sizeof(func()). In this case, func() returns an int, so sizeof(func()) is equivalent to sizeof(int).

The second interesting item that's frequently used is the fact that the size of an array must be positive, not zero.

Now, putting those together, we can do something like this:

// 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;
};

Here we have two overloads of test. The second of these takes a variable argument list (the ...) which means it can match any type -- but it's also the last choice the compiler will make in selecting an overload, so it'll only match if the first one does not. The other overload of test is a bit more interesting: it defines a function that takes one parameter: an array of pointers to functions that return char, where the size of the array is (in essence) sizeof(stream << object). If stream << object isn't a valid expression, the sizeof will yield 0, which means we've created an array of size zero, which isn't allowed. This is where the SFINAE itself comes into the picture. Attempting to substitute the type that doesn't support operator<< for U would fail, because it would produce a zero-sized array. But, that's not an error -- it just means that function is eliminated from the overload set. Therefore, the other function is the only one that can be used in such a case.

That then gets used in the enum expression below -- it looks at the return value from the selected overload of test and checks whether it's equal to 1 (if it is, it means the function returning char was selected, but otherwise, the function returning long was selected).

The result is that has_inserter<type>::value will be 1 if some_ostream << object; would compile, and 0 if it wouldn't. We can then use that value to control template specialization to pick the right way to write out the value for a particular type.

深府石板幽径 2024-09-19 16:28:00

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

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

If you have some overloaded template functions, some of the possible candidates for use may fail to be compilable when template substitution is performed, because the thing being substituted may not have the correct behaviour. This is not considered to be a programming error, the failed templates are simply removed from the set available for that particular parameter.

I have no idea if Python has a similar feature, and don't really see why a non-C++ programmer should care about this feature. But if you want to learn more about templates, the best book on them is C++ Templates: The Complete Guide.

公布 2024-09-19 16:28:00

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 不等于 complexT* 表示某些 T。同样,f(std::complex(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 的数组无效,因此编译器会丢弃该重载。 替换失败不是错误,编译器不会拒绝该程序。

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

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

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

  • 选择没有该错误的类型的嵌套类型。例如。 typename T::type 表示 T = intT = A,其中 A 是没有嵌套类型的类称为类型
  • 创建非正大小的数组类型。有关示例,请参阅此litb的答案
  • 创建一个指向非类型的成员指针班级。例如。 int C::* for C = int

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


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

SFINAE is a principle a C++ compiler uses to filter out some templated function overloads during overload resolution (1)

When the compiler resolves a particular function call, it considers a set of available function and function template declarations to find out which one will be used. Basically, there are two mechanisms to do it. One can be described as syntactic. Given declarations:

template <class T> void f(T);                 //1
template <class T> void f(T*);                //2
template <class T> void f(std::complex<T>);   //3

resolving f((int)1) will remove versions 2 and three, because int is not equal to complex<T> or T* for some T. Similarly, f(std::complex<float>(1)) would remove the second variant and f((int*)&x) would remove the third. The compiler does this by trying to deduce the template parameters from the function arguments. If deduction fails (as in T* against int), the overload is discarded.

The reason we want this is obvious - we may want to do slightly different things for different types (eg. an absolute value of a complex is computed by x*conj(x) and yields a real number, not a complex number, which is different from the computation for floats).

If you have done some declarative programming before, this mechanism is similar to (Haskell):

f Complex x y = ...
f _           = ...

The way C++ takes this further is that the deduction may fail even when the deduced types are OK, but back substitution into the other yield some "nonsensical" result (more on that later). For example:

template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);

when deducing f('c') (we call with a single argument, because the second argument is implicit):

  1. the compiler matches T against char which yields trivially T as char
  2. the compiler substitutes all the Ts in the declaration as chars. This yields void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0).
  3. The type of the second argument is pointer to array int [sizeof(char)-sizeof(int)]. The size of this array may be eg. -3 (depending on your platform).
  4. Arrays of length <= 0 are invalid, so the compiler discards the overload. Substitution Failure Is Not An Error, the compiler won't reject the program.

In the end, if more than one function overload remains, the compiler uses conversion sequences comparison and partial ordering of templates to select one that is the "best".

There are more such "nonsensical" results that work like this, they are enumerated in a list in the standard (C++03). In C++0x, the realm of SFINAE is extended to almost any type error.

I won't write an extensive list of SFINAE errors, but some of the most popular are:

  • selecting a nested type of a type that doesn't have it. eg. typename T::type for T = int or T = A where A is a class without a nested type called type.
  • creating an array type of nonpositive size. For an example, see this litb's answer
  • creating a member pointer to a type that's not a class. eg. int C::* for C = int

This mechanism is not similar to anything in other programming languages I know of. If you were to do a similar thing in Haskell, you'd use guards which are more powerful, but impossible in C++.


1: or partial template specializations when talking about class templates

你好,陌生人 2024-09-19 16:28:00

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)和许多并行的、否则会发生冲突的定义。除了一个定义之外,所有定义都会出现一些错误,编译器会选择并使用该定义,而不会抱怨其他定义。

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

Python won't help you at all. But you do say you're already basically familiar with templates.

The most fundamental SFINAE construct is usage of enable_if. The only tricky part is that class enable_if does not encapsulate SFINAE, it merely exposes it.

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 >();
}

In SFINAE, there is some structure which sets up an error condition (class enable_if here) and a number of parallel, otherwise conflicting definitions. Some error occurs in all but one definition, which the compiler picks and uses without complaining about the others.

What kinds of errors are acceptable is a major detail which has only recently been standardized, but you don't seem to be asking about that.

一场春暖 2024-09-19 16:28:00

Python 中没有任何东西与 SFINAE 非常相似。 Python 没有模板,当然也没有像解析模板特化时那样的基于参数的函数解析。 Python 中的函数查找纯粹是通过名称来完成的。

There is nothing in Python that remotely resembles SFINAE. Python has no templates, and certainly no parameter-based function resolution as occurs when resolving template specialisations. Function lookup is done purely by name in Python.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文