SFINAE and enable_if

摘要:
例如,以下示例:intnegate{return-i;}template<typenameT>typenameT::value_Typenegate{return-T;}COUNTACALLTONEGATE。ITWILLPICKUP第一次过载并返回-42。然而,在寻找奖励时,必须考虑所有候选人。当编译器考虑模板化否定时,它将替换调用模板的productedargument类型,并返回声明:当调用close时,编译器将最终选择第一个函数,然后返回结果为-42。当编译器进行重载解析时,发现gate模板版本的模板参数派生为int,然后会有这样的声明:int::value_typenegate;当然,该代码是有效的,因为在成员名称和值类型中。您可以问,在这种情况下,编译器是否出现故障和附带的错误消息?好吧,没有。如果是这样,用C++编写通用代码会非常困难。事实上,C++标准对这种情况有一个特殊的条款,请详细解释编译器应该具备的条件。这样的代码当然是非法的。int类型没有类型的名称value_ Member。SFINA根据C++11标准的最新标准,相关章节为4.8.2;它指出,替换失败,例如一个hownabove、occurrence、typeproduction或一个部分类型失败。就是这样。没有错误。编译器隐式忽略这个候选项,并查看其他候选项。C++11标准14.8.2中指出,当替换失败发生在上述场景中时,不会报告编译错误,编译器只需从候选函数集中删除模板,然后考虑其他候选函数。

There's an interesting issue one has to consider when mixing function overloading with templates in C++. The problem with templates is that they are usually overly inclusive, and when mixed with overloading, the result may be surprising:

         因为模板的包容性太强,因此当函数模板和重载掺和在一起时,结果有时会是出人意料的:

void foo(unsigned i) {
  std::cout << "unsigned " << i << "
";
}

template <typename T>
void foo(const T& t) {
  std::cout << "template " << t << "
";
}

 What do you think a call to foo(42) would print? The answer is "template 42", and the reason for this is that integer literals are signed by default (they only become unsigned with the U suffix). When the compiler examines the overload candidates to choose from for this call, it sees that the first function needs a conversion, while the second one matches perfectly, so that is the one it picks [1].

         调用f(42),结果是打印"template 42",这是因为整数常量42默认类型是int,当编译器进行重载决议时,第一个函数需要一个转换,而第二个函数(模板)可以精确匹配,所以它选择了第二个函数(模板)。

When the compiler looks at overload candidates that are templates, it has to actually perform substitution of explicitly specified or deduced types into the template arguments. This doesn't always result in sensical code, as the following example demonstrates; while artificial, it's representative of a lot of generic code written in modern C++:

         当编译器的重载决议发生在模板身上时,它要么进行显式的模板形参替换,或者是对模板形参进行类型推导。有时候得到的模板具现化是无意义的。比如下面的例子:

int negate(int i) {
  return -i;
}

template <typename T>
typename T::value_type negate(const T& t) {
  return -T(t);
}

 Consider a call to negate(42). It will pick up the first overload and return -42. However, while looking for the best overload, all candidates have to be considered. When the compiler considers the templated negate, it substitutes the deduced argument type of the call (int in this case) into the template, and comes up with the declaration:

         当调用negate(42)时,编译器最终会选择第一个函数,然后返回结果是-42。当编译器进行重载决议时,发现模板版本的negate的模板形参推导为int,然后就会有这样的声明:

int::value_type negate(const int& t);

 This code is invalid, of course, since int has no member named value_type. So one could ask - should the compiler fail and emit an error message in this case? Well, no. If it did, writing generic code in C++ would be very difficult. In fact, the C++ standard has a special clause for such cases, explaining exactly how a compiler should behave.

         这样的代码当然是非法的,int类型没有名为value_type的成员。然而编译器此时不应该直接报出一个编译错误,如果真这样的话,那编写C++代码将会变得非常困难。实际上,在C++标准中有专门对此种场景的条款,用于解释发生这种情况是编译器应该怎么做。这就是SFINAE。

SFINAE

In the latest draft of the C++11 standard, the relevant section is 14.8.2; it states that when a substitution failure, such as the one shown above, occurs, type deduction for this particular type fails. That's it. There's no error involved. The compiler simply ignores this candidate and looks at the others.

         在C++11标准中的14.8.2中指出,当像上面那种场景一样发生替代失败(substitution failure)时,不会报编译错误,编译器仅仅是将模板从候选函数集中删除,然后考虑其他的候选函数。

In the C++ folklore, this rule was dubbed "Substitution Failure Is Not An Error", or SFINAE.

         这个条款被称为"Substitution Failure Is Not An Error",也就是SFINAE。

The standard states:

If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed if written using the substituted arguments. Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure.

         标准中是这样说的:当substitution导致了无效的类型或表达式时,类型推导就失败了。所谓无效的类型或表达式,是指使用了替换后的参数后,产生了不合语法的形式。只有在函数类型以及模板参数类型中的immediate context中产生的非法类型和表达式,才能导致推导失败。

And then goes on to list the possible scenarios that are deemed invalid, such as using a type that is not a class or enumeration type in a qualified name, attempting to create a reference to void, and so on.

然后标准继续列出了可能被视为无效的场景,例如在限定名称中使用不是class或枚举类型的类型,尝试创建对void的引用,等等。

But wait, what does it mean by the last sentence about "immediate context"? Consider this (non-sensical) example:

         所谓的"immediate context",考虑下面的例子:

template <typename T>
void negate(const T& t) {
  typename T::value_type n = -t();
}

 If type deduction matches this overload for some fundamental type, we'll actually get a compile error due to the T::value_type inside the function body. This is outside of the "immediate context of the function type and its template parameter types" mentioned by the standard. The lesson here is that if we want to write a template that only makes sense for some types, we must make it fail deduction for invalid types right in the declaration, to cause substitution failure. If the invalid type sneaks past the overload candidate selection phase, the program won't compile.

         当类型推导匹配到基本类型(int,float等)时,实际上我们会在函数体中因为T::value_type而得到一个编译错误,而非SFINAE。这是因为函数体,已经不属于标准中提到的“函数类型以及模板参数类型的immediate context”范畴了。因此,当我们需要用到SFINAE时,我们必须保证导致推导失败的非法类型仅仅出现在声明中。

enable_if - a compile-time switch for templates

SFINAE has proved so useful that programmers started to explicitly rely on it very early on in the history of C++. One of the most notable tools used for this purpose is enable_if. It can be defined as follows:

SFIEAE被证明是非常有用的,在C++的历史上程序员很早就开始明确地依赖它。用于此目的的最显著的工具之一是enable_if。它可能的定义如下:

template <bool, typename T = void>
struct enable_if
{};

template <typename T>
struct enable_if<true, T> {
  typedef T type;
};

 And now we can do things like [2]:

template <class T,
         typename std::enable_if<std::is_integral<T>::value,
                                 T>::type* = nullptr>
void do_stuff(T& t) {
  std::cout << "do_stuff integral
";
    // an implementation for integral types (int, char, unsigned, etc.)
}

template <class T,
          typename std::enable_if<std::is_class<T>::value,
                                  T>::type* = nullptr>
void do_stuff(T& t) {
    // an implementation for class types
}

 Note SFINAE at work here. When we make the call do_stuff(<int var>), the compiler selects the first overload: since the condition std::is_integral<int> is true, the specialization of struct enable_if for true is used, and its internal type is set to int. The second overload is omitted because without the true specialization (std::is_class<int> is false) the general form of struct enable_if is selected, and it doesn't have a type, so the type of the argument results in a substitution failure.

         当我们使用int类型调用do_stuff时,编译器会选择第一个重载,这是因为std::is_integral<int>结果为true,从而可以用到那个true版本的enable_if,其内部的type被定义为int类型,然后在do_stuff的第二个模板形参中,声明了一个int*,且其默认值为nullptr。第二个版本被忽略掉了,这是因为std::is_class<int>值为false,从而只能使用struct enable_if的普通版本,其内部没有type,从而导致了substitution failure。

enable_if has been part of Boost for many years, and since C++11 it's also in the standard C++ library as std::enable_if. Its usage is somewhat verbose though, so C++14 adds this type alias for convenience:

enable_if在Boost中已经存在很多年了,在C++11中也把他引入到标准库中:std::enable_if。它的写法在C++11中有些复杂,因此C++14稍微改良了一下:

template <bool B, typename T = void>
using enable_if_t = typename enable_if<B, T>::type;

 With this, the examples above can be rewritten a bit more succinctly:

         上面的例子从而可以写成这样:

template <class T,
         typename std::enable_if_t<std::is_integral<T>::value>* = nullptr>
void do_stuff(T& t) {
    // an implementation for integral types (int, char, unsigned, etc.)
}

template <class T,
          typename std::enable_if_t<std::is_class<T>::value>* = nullptr>
void do_stuff(T& t) {
    // an implementation for class types
}

  

Uses of enable_if

enable_if is an extremely useful tool. There are hundreds of references to it in the C++11 standard template library. It's so useful because it's a key part in using type traits, a way to restrict templates to types that have certain properties. Without enable_if, templates are a rather blunt "catch-all" tool. If we define a function with a template argument, this function will be invoked on all possible types. Type traits and enable_if let us create different functions that act on different kinds of types, while still remaining generic [3].

         enable_if非常有用,在C++的STL中很多地方都用到了它。因为它是类型特征(type traits)的关键部分,所谓type traits,就是一种将模板限制为具有特定属性的类型的方法。如果没有enable_if的话,模板将会包容一切,有了type traits和enable_if,使得我们可以再不同的类型上定义不同的函数,而同时保持一般性。

One usage example I like is the two-argument constructor of std::vector:

// Create the vector {8, 8, 8, 8}
std::vector<int> v1(4, 8);

// Create another vector {8, 8, 8, 8}
std::vector<int> v2(std::begin(v1), std::end(v1));

// Create the vector {1, 2, 3, 4}
int arr[] = {1, 2, 3, 4, 5, 6, 7};
std::vector<int> v3(arr, arr + 4);

 There are two forms of the two-argument constructor used here. Ignoring allocators, this is how these constructors could be declared:

         Vector的构造函数中,有两种版本的声明包含两个参数:

template <typename T>
class vector {
    vector(size_type n, const T val);

    template <class InputIterator>
    vector(InputIterator first, InputIterator last);
    ...
}

 Both constructors take two arguments, but the second one has the catch-all property of templates. Even though the template argument InputIterator has a descriptive name, it has no semantic meaning - the compiler wouldn't mind if it was called ARG42 or T. The problem here is that even for v1, the second constructor would be invoked if we didn't do something special. This is because the type of 4 is int rather than size_t. So to invoke the first constructor, the compiler would have to perform a type conversion. The second constructor would fit perfectly though.

         第二个版本是个包容一切的函数模板,当构造v1时,就会调用该版本的构造函数,这是因为4的类型是int,而非size_t,所以要使用第一个版本的话,编译器还需要执行类型转换,而第二个版本却是精确匹配的。

So how does the library implementor avoid this problem and make sure that the second constructor is only called for iterators? By now we know the answer - with enable_if.

         vector的作者如何避免这样的问题,并且保证第二个构造函数只有在传参迭代器的时候才被调用呢?答案就是使用enable_if:

Here is how the second constructor is really defined:

         下面就看一下第二个构造函数是如何定义的:

template <class _InputIterator>
vector(_InputIterator __first,
       typename enable_if<__is_input_iterator<_InputIterator>::value &&
                          !__is_forward_iterator<_InputIterator>::value &&
                          ... more conditions ...
                          _InputIterator>::type __last);

 It uses enable_if to only enable this overload for types that are input iterators, though not forward iterators. For forward iterators, there's a separate overload, because the constructors for these can be implemented more efficiently.

As I mentioned, there are many uses of enable_if in the C++11 standard library. The string::append method has a very similar use to the above, since it has several overloads that take two arguments and a template overload for iterators.

A somewhat different example is std::signbit, which is supposed to be defined for all arithmetic types (integer or floating point). Here's a simplified version of its declaration in the cmath header:

         Std::singbit是一个稍微不同的例子,它是针对所有的算数类型(整型、浮点型等)而定义的:

template <class T>
typename std::enable_if<std::is_arithmetic<T>, bool>::type
signbit(T x) {
    // implementation
}

 Without using enable_if, think about the options the library implementors would have. One would be to overload the function for each of the known arithmetic type. That's very verbose. Another would be to just use an unrestricted template. But then, had we actually passed a wrong type into it, say std::string, we'd most likely get a fairly obscure error at the point of use. With enable_if, we neither have to write boilerplate, nor to produce bad error messages. If we invoke std::signbit as defined above with a bad type we'll get a fairly helpful error saying that a suitable function cannot be found.

         如果不使用enable_if,则要么就是针对每个已知的算数类型定义一个重载版本;要么就是使用一个不受限制的模板,但是这种情况下,当我们传递一个错误的类型时,比如std::string,我们可能直到使用时才能得到一个相当隐晦的错误信息。

A more advanced version of enable_if

Admittedly, std::enable_if is clumsy, and even enable_if_t doesn't help much, though it's a bit less verbose. You still have to mix it into the declaration of a function in a way that often obscures the return type or an argument type. This is why some sources online suggest crafting more advanced versions that "get out of the way". Personally, I think this is the wrong tradeoff to make.

std::enable_if is a rarely used construct. So making it less verbose doesn't buy us much. On the other hand, making it more mysterious is detrimental, because every time we see it we have to think about how it works. The implementation shown here is fairly simple, and I'd keep it this way. Finally I'll note that the C++ standard library uses the verbose, "clumsy" version of std::enable_if without defining more complex versions. I think that's the right decision.

[1]    If we had an overload for int, however, this is the one that would be picked, because in overload resolution non-templates are preferred over templates.

[2]    Update 2018-07-05: Previously I had a version here which, while supported by earlier compilers, wasn't entirely standards-compliant. I've modified it to a slightly more complicated version that works with modern gcc and Clang. The trickiness here is due to do_stuff having the exact same signature in both cases; in this scenario we have to be careful about ensuring the compiler only infers a single version.

[3]    Think of it as a mid-way between overloading and templates. C++ has another tool to implement something similar - runtime polymorphism. Type traits let us do that at compile time, without incurring any runtime cost.

https://eli.thegreenplace.net/2014/sfinae-and-enable_if/

免责声明:文章转载自《SFINAE and enable_if》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇洛谷-统计数字-NOIP2007提高组复赛MariaDB/MySQL备份和恢复(三):xtrabackup用法和原理详述下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

随便看看

postgresql笔记

一旦任何有价值的对象被转移到新所有者,可以使用DROPOWNED命令删除被删除角色所拥有的任何剩余对象。此外,DROPOWNED不会删除整个数据库或表空间。因此,如果角色有任何尚未转移到新所有者的数据库或表空间,则需要手动删除它们。DROPOWNED还将注意到,对于不属于目标角色的对象,删除授予目标角色的任何特权。因为REASSIGNOWNED不会接触这些对...

socket网络编程(二)—— 实现持续发送

exit(exit_FAILURE);}//初始化套接字元素structsockaddr_inserver_addr;intserver_len=大小(server_addr);内存集(&amp;server_len);0){ERR_EXIT(“listenclientfail”);client_len);buffer);}//关闭套接字(m_con...

利用油猴插件实现全网VIP视频免费看

利用油猴插件实现全网VIP视频免费看第一步:首先打开谷歌应用商店搜索tampermonkey安装这个插件第二步:在百度搜索框搜索油猴可以看到以下页面,点击进入。下载谷歌上网助手解压后,将后缀为crx的文件拖入即可。之后注册一个谷歌上网助手账后登录即可进入谷歌应用商店油猴插件...

iphone的mov文件复制到电脑的方法

解决方案:1.对于iPhone-˃设置-˃照片,将底部选项“传输到MAC或PC”从“自动”更改为“保持原始”。...

Ansible-Tower--安装配置及破解

Ansible-Tower是将ansible的指令界面化,简明直观,简单易用。2)Ansibke-tower其实就是一个图形化的任务调度,复杂服务部署,IT自动化的一个管理平台,属于发布配置管理系统,支持Api及界面操作,Django编写。3)Ansible-tower可以通过界面从github拉取最新playbook实施服务部署,提高生产效率。...

koroFileHeader插件快速入门使用教程

插件下载插件可以直接在vscode的扩展中查找koroFileHeader,但是有时候由于网络的问题会查找不到软件。插件配置koroFileHeader支持许多功能,但是不是所有功能都是需要,我们关注往往是如何配置注释内容和注释的一些选项。"fileheader.cursorMode":{//这部分是函数头的配置},"fileheader.customMad...