【转】C++中的SFINAE

C++中的SFINAE 2011-05-11 15:45:20

分类: C/C++

这几天神游到一段is_base_of的代码迷惑了很久, 在查资料的过程当中, 发现C++中一种称之为SFINAE的技巧, 全称为"匹配失败并不是一种错误(Substitution Failure Is Not An Error)". 这是一种专门利用编译器匹配失败来达到某种目的的技巧.

在说明之前先说说模板匹配的原则: 非模板函数具有最高优先权, 如果不存在匹配的非模板函数的话, 那么最匹配的和最特化的具有最高的优先权.

C++中,函数模板与同名的非模板函数重载时,应遵循下列调用原则:

  • 寻找一个参数完全匹配的函数,若找到就调用它。若参数完全匹配的函数多于一个,则这个调用是一个错误的调用。
  • 寻找一个函数模板,若找到就将其实例化生成一个匹配的模板函数并调用它。
  • 若上面两条都失败,则使用函数重载的方法,通过类型转换产生参数匹配,若找到就调用它。
  • 若上面三条都失败,还没有找都匹配的函数,则这个调用是一个错误的调用。

至于函数的选择原则, 可以看看C++ Primer中的说明:

  • 创建候选函数列表,其中包含与被调用函数名字相同的函数和模板函数。
  • 使用候选函数列表创建可行的函数列表。这些都是参数数目正确的函数,并且有一个隐式的转换序列(参数类型转化),其中包括实参类型与相应的形参类型完全匹配情况。
  • 确定是否有最佳的可行函数,有则调用它,没有则报错。

可行函数的最佳性,主要是判断使用函数的参数与可行性函数的参数的转换规则进行判断,从最佳到最差的顺序如下所示:

  • 完全匹配,但常规函数优先于显示定义模板函数,而显示定义模板函数优先于模板函数。
  • 提升转换,即从小精度数据转换为高精度数据类型,如char/short 转换为int , int转化为long,float转换为double。
  • 标准转换,如int转化为char,long转化为double等
  • 用户自定义转换。


下面先看看带有默认值的模板函数特化和非特化的问题

 1 template<typename T, bool C = true>
 2 struct if_ {
 3         static const int value = 1;
 4  };
 5
 6 template<typename T>
 7 struct if_<T, true> {
 8         static const int value = 2;
 9  };
10
11  int main() {
12         printf("value: %d\n", if_<int>::value);
13 }

上面的输入结果是: value: 2. 编译器在进行匹配的时候, 就如Prime上说的, 编译器会先创建候选函数列表, 在创建候选列表的过程中C已经被赋予默认是true, 然后在进行匹配, 最高优先级是函数, 显然这里没有. 然后是最匹配和最特化的模板函数, 所以, 就匹配到第二个函数了, 因此value等于2.

实际上, 上面的例子相当于

 1 template<typename T, bool C = true>
 2 struct if_ {};
 3
 4 template<typename T>
 5 struct if_<T, false> {
 6        static const int value = 1;
 7  };
 8
 9 template<typename T>
10 struct if_<T, true> {
11        static const int value = 2;
12 };
13
14 int main() {
15        printf("value: %d\n", if_<int>::value);
16 }

因此, 编译器总是先找到函数和模板, 然后根据默认值, 类型转换进行匹配, 然后再根据优先级选择最可行的.

-----------------------------------------华丽的分割线--------------------------------------------------

说到这里应该大致明白重载函数, 函数模板, 特化和偏特化的一些匹配问题. (其实还有一个问题没有说清楚, 后面会提到). 现在来看看SFINAE的一个应用.

要实现一个通用的序列化函数叫做toString, 它可以实现把任何类型序列化成字符串.

1 template<typename T> std::string toString(const T &x);

但是我们希望能检测到T是否有自己的toString方法, 如果有就直接调用, 如果没有就调用通用的方法, 例如仅仅返回类的名称 typeid(T).name().

现在有两个类

1 class A {
2  public:
3         std::string toString() const {
4                 return std::string("toString from class A");
5         }
6  };
7
8  class B {
9  };

这时代码里面有A::toString 就没有问题, 但是编译器找不到B::toString, 利用这个错误来跳过模板的匹配, 从而使得别的模板得以匹配.

 1 template<typename T>
 2 struct HasToStringFunction {
 3         template<typename U, std::string (U::*)() const >
 4         struct matcher;
 5
 6         template<typename U>
 7         static char helper(matcher<U, &U::toString> *);
 8
 9         template<typename U>
10         static int helper(...);
11
12         enum { value = sizeof(helper<T>(NULL)) == 1 };
13  };

这里有两个helper方法, 第一个匹配精确度要高于第二个. 因此, 编译器会先尝试用U和U::toString去实例化一个matcher去匹配helper, 对于A这是能通过的, 但是对于B, 由于B::toString不存在, 这个时候编译器实际上就已经发现错误了,但是根据SFINAE原则这个只能算是模板匹配失败,不能算错误,所以编译器会跳过这次对matcher的匹配。但是跳过了以后也就没有别的匹配了,所以整个第一个helper来说对B都是不能匹配成功的,这个时候优先级比较低的第二个helper自然就能匹配上了。

利用这点就可以实现toString方法

 1 template <bool>
 2 struct ToStringWrapper {};
 3
 4 template<>
 5 struct ToStringWrapper<true> {
 6         template<typename T>
 7         static std::string toString(T &x) {
 8                 return x.toString();
 9         }
10  };
11
12 template<>
13 struct ToStringWrapper<false> {
14         template<typename T>
15         static std::string toString(T &x) {
16                 return std::string(typeid(x).name());
17         }
18  };
19
20 template<typename T>
21 std::string toString(const T &x) {
22         return ToStringWrapper<HasToStringFunction<T>::value>::toString(x);
23  }
24
25  int main() {
26         A a;
27         B b;
28
29         std::cout << toString(a) << std::endl;
30         std::cout << toString(b) << std::endl;
31 }

这里有两个小技巧, 一个是sizeof()一个函数, 返回的是函数返回值的大小. 另外一个是U::*表示类成员函数指针. 比如std::string (*)() const 表明这是一个函数指针, 而std::string (U::*)() const表示这是一个类的成员函数.

这样我们可以实现一个判断类型是基本类型还是一个类的类模板is_class

1 template <typename T>
2  class is_class {
3     template <typename U>
4     static char helper(int U::*);
5     template <typename U>
6     static int helper(...);
7  public:
8     static const bool value = sizeof(helper<T>(0)) == 1;
9  };

这里的原因不再重复.

------------------------------------------------华丽的分割线--------------------------------------------
现在终于轮到大名鼎鼎的is_base_of出场

 1 template <typename T1, typename T2>
 2 struct is_same {
 3     static const bool value = false;
 4  };
 5
 6 template <typename T>
 7 struct is_same<T, T> {
 8     static const bool value = true;
 9  };
10
11 template<typename Base, typename Derived, bool = (is_class<Base>::value && is_class<Derived>::value)>
12 class is_base_of {
13     template <typename T>
14     static char helper(Derived, T);
15     static int helper(Base, int);
16     struct Conv {
17         operator Derived();
18         operator Base() const;
19     };
20  public:
21     static const bool value = sizeof(helper(Conv(), 0)) == 1;
22  };
23
24 template <typename Base, typename Derived>
25 class is_base_of<Base, Derived, false> {
26  public:
27     static const bool value = is_same<Base, Derived>::value;
28  };
29
30 template <typename Base>
31  class is_base_of<Base, Base, true> {
32 public:
33     static const bool value = true;
34  };

先来看看is_base_of模板的第三个参数, 如果前两个类型都是类的话, 则为true, 否则为false. 下面一个个情况来分析:

第一种情况是有一个基本类型, 显然, is_base_of第三个参数为false, 按照上面说到的原则匹配到了第二个类模板. 在该情况下只有当两个类型一致时is_base_of的value才为true, 否则为false.

第二种情况是Base和Derived是同一个类型, 则会匹配到第三个模板.

第三种情况
是Base和Derived有继承关系, 此时编译器只能匹配第一个类模板. 在helper(Conv(), 0)中, 显然没有helper的第一个参数无法直接匹配, Conv()默认也无法转换成Base或者是Derived. 因此需要调用自定义的转换函数. 当试图匹配int helper(Base, int)的时候, 编译有两个途径, 一个是: Conv()->Derived()->Base(), 第三步是默认, 另一个是Conv()->const Conv()->Base(), 这实际上是一个SFINAE, 根据原则编译器会继续匹配下一个模板, 匹配成功, 因此value的值为true.

第四种情况是Base和Derived没有继承关系, 当试图匹配int helper(Base, int)的时候能通过Conv()->const Conv()->Base()匹配成功, 因为两者不是继承关系, 因此Derived()->Base()的默认转换不会成功, 因此该情况下此路径不存在. 所以编译器选择的函数是int helper(Base, int), 最后, value的值为false.

----------------------------------- 华丽的分割线 ------------------------------------------------------

结束语: 无语.

时间: 2024-10-22 10:14:24

【转】C++中的SFINAE的相关文章

C++14 SFINAE 解引用迭代器

C++14 SFINAE 解引用迭代器 p { margin-bottom: 0.25cm; line-height: 120% } a:link { } 原问题:编写函数f(r),若r为迭代器,则返回f(*r),否则返回r. p { margin-bottom: 0.25cm; line-height: 120% } a:link { } 摘要: p { margin-bottom: 0.25cm; line-height: 120% } a:link { } 问题: 什么是迭代器? 迭代器是

C++模板元编程 - 3 逻辑结构,递归,一点列表的零碎,一点SFINAE

本来想把scanr,foldr什么的都写了的,一想太麻烦了,就算了,模板元编程差不多也该结束了,离开学还有10天,之前几天部门还要纳新什么的,写不了几天代码了,所以赶紧把这个结束掉,明天继续抄轮子叔的Win32库去. 逻辑结构和递归说白了就是做了一个If,一个For_N,If就和Excel里的If一样,For_N是把一个模板结构迭代N遍,为了所谓的方便,把If做成了宏,写起来还挺有意思的 1 template<typename TTest, typename TTrue, typename TF

SFINAE and enable_if

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: 因为模板的包容性太强,因

C++模板进阶指南:SFINAE

C++模板进阶指南:SFINAE 空明流转(https://zhuanlan.zhihu.com/p/21314708) SFINAE可以说是C++模板进阶的门槛之一,如果选择一个论题来测试对C++模板机制的熟悉程度,那么在我这里,首选就应当是SFINAE机制. 我们不用纠结这个词的发音,它来自于 Substitution failure is not an error 的首字母缩写.这一句之乎者也般难懂的话,由之乎者 -- 啊,不,Substitution,Failure和Error三个词构成

win10周年版eNSP中启动AR提示错误代码40问题

win 10操作系统中安装eNSP 1.2.00.380,一直运行正常,但在2016年11月升级win 周年版之后,启动AR时启动失败,提示错误代码40. 卸载eNSP及VirtualBox之后重装问题依旧.按照论坛和网上各种说法更新virtualbox修改虚拟网卡设置,或者重新注册都无法解决,最终多方查找终于找到解决方案. 环境:win10 周年版,eNSP 1.2.00.380,VirtualBox 4.2.8 eNSP注册后virtualbox管理器中会出现AR_Base,WLAN_AC_

css中的px、em、rem 详解

概念介绍: 1.px (pixel,像素):是一个虚拟长度单位,是计算机系统的数字化图像长度单位,如果px要换算成物理长度,需要指定精度DPI(Dots Per Inch,每英寸像素数),在扫描打印时一般都有DPI可选.Windows系统默认是96dpi,Apple系统默认是72dpi. 2.em(相对长度单位,相对于当前对象内文本的字体尺寸):是一个相对长度单位,最初是指字母M的宽度,故名em.现指的是字符宽度的倍数,用法类似百分比,如:0.8em, 1.2em,2em等.通常1em=16px

angularJs中关于ng-class的三种使用方式说明

在开发中我们通常会遇到一种需求:一个元素在不同的状态需要展现不同的样子. 而在这所谓的样子当然就是改变其css的属性,而实现能动态的改变其属性值,必然只能是更换其class属性 这里有三种方法: 第一种:通过数据的双向绑定(不推荐) 第二种:通过对象数组 第三种:通过key/value 下面简单说下这三种: 第一种:通过数据的双向绑定 实现方式: function changeClass(){   $scope.className = "change2"; } <div clas

Uploadify/uploadifive上传(中文文档)

Uploadify是一款基于JQuery的优秀的文件/图片上传的插件,有基于Flash和HTML5两种版本. Uploadify/uploadifive主要特点有: 1. 多文件上传 2. 个性化设置 3. 上传进度条显示 4. 拖拽上传(HTML5版本) 官网:http://www.uploadify.com 部署 在部署一个Uploadify实例前,请确保满足最低要求: 1.jQuery 1.4.x 或更高版本 2.Flash Player 9.0.24 或更高版本 3.支持PHP, ASP

XShell 连接虚拟机中的服务器 失败 、连接中断(Connection closed by foreign host.)

在使用XShell连接虚拟机中的服务器时,报以下错误并断开连接,之前连接还是挺稳定的,忽然就这样了 Last login: Thu Aug 10 21:28:38 2017 from 192.168.1.102 [[email protected] ~]# Socket error Event: 32 Error: 10053. Connection closing...Socket close. Connection closed by foreign host. Disconnected f