当前位置: 首页 > news >正文

C++惯用法: 通过std::decltype来SFINAE掉表达式

目录

1.什么是SFINAE

2.SFINAE(替换失败不是错误)

3.通过std::decltype来SFINAE掉表达式


1.什么是SFINAE

        SFINAE 技术,即匹配失败不是错误,英文Substitution Failure Is Not An Error,其作用是当我们在进行模板特化的时候,会去选择那个正确的模板,避免失败。

        SFINAE一般用于函数重载和编译期间类型检查,标准库中很多type traits模板就是通SFINAE来实现的。

        看个具体的例子:

#include <iostream>
#include <type_traits>
using namespace std;template<typename T>
struct check_has_member_id
{// 仅当T是一个类类型时,“U::*”才是存在的,从而这个泛型函数的实例化才是可行的// 否则,就将触发SFINAEtemplate<typename U>static void check(decltype(&U::id)){}// // 仅当触发SFINAE时,编译器才会“被迫”选择这个版本template<typename U>static int check(...){}enum {value = std::is_void<decltype(check<T>(NULL))>::value};
};struct TEST_STRUCT
{int rid;
};struct TEST_STRUCT2
{int id;
};int main()
{check_has_member_id<TEST_STRUCT> t1;cout << t1.value << endl;check_has_member_id<TEST_STRUCT2> t2;cout << t2.value << endl;check_has_member_id<int> t3;cout << t3.value << endl;return 0;
}
// g++ --std=c++11  xxx.c

        核心的代码是在实例化check_has_member_id对象的时候,通过模板参数T的类型,决定了结构体中对象value的值。而value的值是通过check<T>函数的返回值是否是void决定的。如果T中含有id成员的话,那么就会匹配第一个实例,返回void;如果不包含id的话,会匹配默认的实例,返回int。

        利用这个机制还可以做很多类似的判断,比如判断一个类是否是结构体。

#include <iostream>
#include <type_traits>// 2. 判断变量是否是一个struct 或者 类
// https://www.jianshu.com/p/d09373b83f86
template <typename T>
struct check
{template <typename U>static void check_class(int U::*) {}template <typename U>static int check_class(...) {}enum { value = std::is_void<decltype(check_class<T>(0))>::value };
};class myclass {};int main()
{check<myclass> t;std::cout << t.value << std::endl;check<int> t2;std::cout << t2.value << std::endl;return 0;
}

std::is_void的用法可参考:

C++17之std::void_t-CSDN博客

2.SFINAE(替换失败不是错误)

        在一个函数调用的备选方案中包含函数模板时,编译器首先要决定应该将什么样的模板参数 用于各种模板方案,然后用这些参数替换函数模板的参数列表以及返回类型,最后评估替换 后的函数模板和这个调用的匹配情况(就像常规函数一样)。

        但是这一替换过程可能会遇到问题:替换产生的结果可能没有意义。不过这一类型的替换不会导致错误,C++语言规则要 求忽略掉这一类型的替换结果。

        考虑如下的例子:

// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{return t.size();
}

当传递的参数是裸数组或者字符串常量时,只有那个为裸数组定义的函数模板能够匹配:

int a[10];
std::cout << len(a); // OK: only len() for array matches
std::cout << len("tmp"); //OK: only len() 

        如果只是从函数签名来看的话,对第二个函数模板也可以分别用 int[10]和 char const [4]替换 类型参数 T,但是这种替换在处理返回类型 T::size_type 时会导致错误。因此对于这两个调用, 第二个函数模板会被忽略掉。

        如果传递的是裸指针,以上两个模板都不会被匹配上(但是不会因此而报错)。此时编译 期会抱怨说没有发现合适的 len()函数:

int* p;
std::cout << len(p); // ERROR: no matching len() function found

但是这和传递一个有 size_type 成员但是没有 size()成员函数的情况不一样。比如如果传递的参数是 std::allocator<>:

std::allocator<int> x;
std::cout << len(x); // ERROR: len() function found, but can’t size()

此时编译器会匹配到第二个函数模板。因此不会报错说没有发现合适的 len()函数,而是会 报一个编译期错误说对 std::allocator而言 size()是一个无效调用。此时第二个模板函数不 会被忽略掉。

如果忽略掉那些在替换之后返回值类型为无效的备选项,那么编译器会选择另外一个参数类 型匹配相差的备选项。比如: 

// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{return t.size();}
// 对所有类型的应急选项:
std::size_t len (…)
{return 0;
}

        此处额外提供了一个通用函数 len(),它总会匹配所有的调用,但是其匹配情况也总是所有 重载选项中最差的(通过省略号...匹配)。

        对于指针,只有应急选项能够匹配上,此时编译器不会再报缺少适用 于本次调用的 len()。不过对于 std::allocator的调用,虽然第二个和第三个函数都能匹配 上,但是第二个函数依然是最佳匹配项。因此编译器依然会报错说缺少 size()成员函数。

3.通过std::decltype来SFINAE掉表达式

        对于有些限制条件,并不总是很容易地就能找到并设计出合适的表达式来 SFINAE 掉函数模 板。

        比如,对于有 size_type 成员但是没有 size()成员函数的参数类型,我们想要保证会忽略掉函 数模板 len()。如果没有在函数声明中以某种方式要求 size()成员函数必须存在,这个函数模 板就会被选择并在实例化过程中导致错误:

template<typename T>
typename T::size_type len (T const& t)
{return t.size();
}
std::allocator<int> x;
std::cout << len(x) << ’\n’; //ERROR: len() selected, 

处理这种情况有一个常见的模式或者习惯用法:

1)通过尾置返回类型语法(函数名前用auto修饰,并在函数名后跟->,再加末尾的返回类型) 来制定返回类型。

2)使用std::decltype和逗号运算符来定义返回类型。

3)将所有必须成立的表达式放置于逗号运算符开头(表达式转换为void类型,以防逗号运算符重载)。

4)在逗号运算符末尾定义一个实际返回类型(类型为返回类型)的对象。

例如:

template<typename T>
auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() )
{return t.size();
}

这里返回类型定义为:

decltype( (void)(t.size()), T::size_type() )

        由于decltype构造的操作数是以逗号分隔的表达式列表,因此,最后一个表达式T::size_type()生成所需返回类型的值(decltype将其转换为返回类型)。(最后一个)逗号之前的表达式是必须成立的。在本例中就是t.size()。将表达式强制转换为void,是为了避免由于用户自定义重载表达式对于类型的逗号运算符而带来的问题。

        请注意,decltype的实参是一个未求值的操作数。这意味着可以在不调用构造函数的情况下创建"虚对象",请参考:

C/C++中decltype关键字用法总结_c++ decltype用法-CSDN博客

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 有关去中心化算路大模型的一些误区:低带宽互连导致训练速度太慢;小容量设备无法生成基础规模的模型;去中心化总是会花费更多;虫群永远不够大
  • 内容协商源码解析与自定义 MessageConverter
  • 100个C++面试题
  • C#实现Winform程序右下角弹窗消息提示
  • Web组成架构
  • Spring Boot项目中JPA操作视图会改变原表吗?
  • Spring源码十九:Bean实例化流程二
  • 离线下载linux mysql和mysql基本库
  • JS实现:统计字符出现频率/计算文字在文本中的出现次数
  • 【大规模训练】混合专家系统
  • 【算法】平衡二叉树
  • 【CUDA|CUDNN】安装
  • CANoe:为什么两个VLAN接口不能设置同一个网络的IP地址呢?
  • Django 常见的操作符
  • 修复 Ubuntu 24.04 Dock 丢失应用程序图标
  • [ JavaScript ] 数据结构与算法 —— 链表
  • 11111111
  • canvas 绘制双线技巧
  • ES6系列(二)变量的解构赋值
  • IDEA 插件开发入门教程
  • Linux Process Manage
  • Mac 鼠须管 Rime 输入法 安装五笔输入法 教程
  • PHP 使用 Swoole - TaskWorker 实现异步操作 Mysql
  • Swift 中的尾递归和蹦床
  • 产品三维模型在线预览
  • 复杂数据处理
  • 构建二叉树进行数值数组的去重及优化
  • 关于Java中分层中遇到的一些问题
  • 前端面试之CSS3新特性
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • ​创新驱动,边缘计算领袖:亚马逊云科技海外服务器服务再进化
  • ​决定德拉瓦州地区版图的关键历史事件
  • ​软考-高级-系统架构设计师教程(清华第2版)【第1章-绪论-思维导图】​
  • ​油烟净化器电源安全,保障健康餐饮生活
  • #1014 : Trie树
  • #systemverilog# 之 event region 和 timeslot 仿真调度(十)高层次视角看仿真调度事件的发生
  • (1综述)从零开始的嵌入式图像图像处理(PI+QT+OpenCV)实战演练
  • (4)通过调用hadoop的java api实现本地文件上传到hadoop文件系统上
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • (LNMP) How To Install Linux, nginx, MySQL, PHP
  • (Note)C++中的继承方式
  • (ros//EnvironmentVariables)ros环境变量
  • (第27天)Oracle 数据泵转换分区表
  • (附源码)ssm学生管理系统 毕业设计 141543
  • (十二)Flink Table API
  • (数据结构)顺序表的定义
  • (一)Java算法:二分查找
  • (转)项目管理杂谈-我所期望的新人
  • (轉貼) UML中文FAQ (OO) (UML)
  • .class文件转换.java_从一个class文件深入理解Java字节码结构
  • .gitignore文件_Git:.gitignore
  • .locked1、locked勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .net core 6 集成 elasticsearch 并 使用分词器
  • .net core开源商城系统源码,支持可视化布局小程序
  • .NET Framework杂记