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.

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

结束语: 无语.

转载于:https://www.cnblogs.com/gx520/p/4614717.html

【转】C++中的SFINAE相关推荐

  1. 计算机术语多态意思,C ++中的多态性

    据我所知: C ++提供了三种不同类型的多态性. 虚拟功能 函数名称重载 运算符重载 除了上述三种类型的多态性外,还存在其他种类的多态性: 运行 编译时间 ad-hoc多态性 参数多态性 我知道运行时 ...

  2. GNU Make 使用手册(于凤昌中译版)

    GNU Make 使用手册(中译版) 翻译:于凤昌 GNU make Version 3.79 April 2000 Richard M. Stallman and Roland McGrath 1 ...

  3. 面试:第十二章:所有总结

    Java基础 java基本类型哪些,所占字节 byte :1个字节 short :2个字节 char :2个字节 int :4个字节 long :8个字节 float :4个字节 double :8个 ...

  4. linux内核分析(转自某位大哥网上的笔记)

    启动 当PC启动时,Intel系列的CPU首先进入的是实模式,并开始执行位于地址0xFFFF0处的代码,也就是ROM-BIOS起始位置的代码.BIOS先进行一系列的系统自检,然后初始化位于地址0的中断 ...

  5. Visual C++ 中的重大更改

    为什么80%的码农都做不了架构师?>>>    此文章由人工翻译. 将光标移到文章的句子上,以查看原文. 更多信息. Visual C++ 中的重大更改 Visual Studio ...

  6. BT4:库中基本类型——Factory和Blackboard

    大家好,欢迎大家关注我的知乎专栏慢慢悠悠小马车 BehaviorTreeFactory 定义在BehaviorTree.CPP/include/behaviortree_cpp_v3/bt_facto ...

  7. SFINAE and enable_if

    在C ++中将函数重载与模板混合时,必须考虑一个有趣的问题.模板的问题在于它们通常包含过多的内容,并且与重载混合使用时,结果可能令人惊讶: #include <iostream>void ...

  8. SFINAE和enable_if

    SFINAE和enable_if SFINAE SFINAE可以说是C++模板进阶的门槛之一,如果选择一个论题来测试对C++模板机制的熟悉程度,那么在我这里,首选就应当是SFINAE机制. 我们不用纠 ...

  9. C++那些事之SFINAE

    介绍c++的SFINAE概念:类成员的编译时内省 0.导语1.C++自省?2.老式的C++98方式2.1重载决议2.2 SFINAE2.3 sizeof运算符2.4 结合一切2.5 实现我们的想法2. ...

最新文章

  1. Eclipse安装SVN教程
  2. stm32中如何进行printf重定向用于串口调试输出
  3. [BZOJ]2563: 阿狸和桃子的游戏
  4. 【ArcGIS遇上Python】Python实现Modis NDVI批量化月最大合成
  5. 移动端 uniapp 国际化一站式解决方案
  6. 人间值得!支付宝宣布平台上登记器官捐献人数:90后占比超一半
  7. 常见前端面试题及答案(下)
  8. (保姆式教程:从下数据到画图)python如何利用EOF分析SSTA海温异常现象并画图
  9. UML图箭头和实线虚线到底什么意思
  10. Hyper-v 虚拟机固定Ip、连接外网
  11. 【终极】文件夹隐藏方法,彻底隐藏文件夹的方法!显示隐藏的文件也看不到
  12. Java 小白 声明两个字符串:一个是“宋江,卢俊义,林冲,鲁智深,武松“;另一个是“及时雨,玉麒麟,豹子头,花和尚,行者“。以逗号为分隔符分割两个字符串,然后将人物绰号和名字拼接在一起并输出。
  13. 2022年第十三届蓝桥杯大赛软件省赛Java学B组试题
  14. 图片去水印免费软件哪个好?这几款软件值得一看
  15. 计算机专业英语听说,计算机专业英语听说(二).doc
  16. HTML交叉报表制作,交叉报表- 交叉式报表-复杂报表设计教程| 快逸报表工具用户手册...
  17. php中 dsn什么意思,网络dsn是什么意思(图文)
  18. 论文阅读: Channel Augmented Joint Learning for Visible-Infrared Recognition
  19. GitChat·人工智能 | 肿瘤医疗影像 AI 识别技术实践
  20. 在VSCode中配置并调试R语言.r文件

热门文章

  1. redis 配置允许其他主机可以远程连接redis
  2. Linux 列出文件列表命令ls
  3. SQLyog连接虚拟机中mysql8.0详解,2003、1130、2058错误码解决
  4. android os于8.1区别,Android-x86 8.1-rc2发布 运行于x86 PC上的安卓OS
  5. Android传感器-开发指南
  6. 中值滤波讲解-Matlab
  7. 循环队列之舞伴问题(含源码详解)
  8. 决策树准确率低原因_智能质检优化实践:召回率和准确率,哪个更重要?
  9. android 5.0 ios 8,Android 5.0和iOS8.1哪个好?安卓5.0与iOS8.1区别对比
  10. office word中利用宏编程批量调节图片的亮度和对比度