C++17之std::void_t
目录
1.std::void_t 的原理
2.std::void_t 的应用
2.1.判断成员存在性
2.1.1.判断嵌套类型定义
2.1.2 判断成员是否存在
2.2 判断表达式是否合法
2.2.1 判断是否支持前置++运算符
2.2.3 判断两个类型是否可做加法运算
3.std::void_t 与 std::enable_if
1.std::void_t 的原理
std::void_t<> 是 C++ 17 标准增加的一个非常实用的功能,其实就是一个模板别名的定义,它的作用是将一系列不同的类型映射成 void 类型。在 C++ 20 的概念(concept)推出之前,它被认为是使用 SFINAE 机制的最方便的方法之一。std::void_t<> 根据对一个表达式(比如用 decltype 推断一个操作数的类型的表达式)的有效性判断,从候选集中移除某些符合条件的模板函数或模板类,从而允许特定的函数重载或类模板的特化版本成立。
标准库中这样定义 std::void_t:
template <class... _Types>
using void_t = void;
std::void_t<> 定义了一个模板别名,它的作用是将模板参数列表中的 _Types 映射成 void 类型。这个别名能工作的前提是 _Types 中的每种类型都必须是合法的类型,若 _Types 中包含不合法的类型,则这个别名的定义非法,从而触发 SFINAE 机制产生相应的效果。std::void_t<> 的灵活性在于 _Types 甚至可以是 decltype() 表达式,比如 std::void_t<decltype(5)> 。这个表达式不会触发模板参数替换失败,因为 decltype(5) 推断得到 int 类型,整个表达式最终的结果相当于 void。但是 std::void_t<decltype(std::string::to_integer)> 就会失败,因为目前标准库的 std::string 没有名为 to_integer 的成员,decltype 推断不出一个合法的类型。这就是 std::void_t<>的工作原理,当 std::void_t<> 替换失败时,就会触发 SFINAE 机制删除相应的模板参数替换得到的错误结果。
2.std::void_t 的应用
2.1.判断成员存在性
2.1.1.判断嵌套类型定义
std::void_t<> 可以用来判断一个类型 T 是否嵌套定义了某个类型。判断的原理就是利用 T::SomeType 类型的存在性,转化成 std::void_t<T::SomeType> 定义的合法性。来看下面的例子,判断某个类型是否内部嵌套定义了子类型:InnerType:
template< class, class = std::void_t<> > //或者 template< class, class = void >
struct has_type_member : std::false_type { };template< class T >
struct has_type_member<T, std::void_t<typename T::InnerType>> : std::true_type { };// 演示代码
struct ObjectA {enum class InnerType { COLOR, SHAPE };
};
struct ObjectB {using InnerType = int;
};
struct ObjectC {
};static_assert(has_type_member<ObjectA>::value); //true, ObjectA 有 enum 类型的 InnerType 定义
static_assert(has_type_member<ObjectB>::value); //true, ObjectB 有 int 类型别名 InnerType
static_assert(has_type_member<ObjectC>::value); //false,ObjectC 没有 InnerType
这里说明一下,因为 `std::void_t<>` 定义中的`_Types`是一个变长参数列表,可以是空,所以当 `_Types` 为空的时候也是一个合法的模板别名,即 `std::void_t<> == void`。上述代码中的 `class = std::void_t<>` 很多人也直接写成 `class = void`,效果是一样的。模板实例化过程中的匹配顺序,`void` 总是优先级最低的,所以编译器总是优先匹配第二个 `has_type_member<>` 定义(`has_type_member<>`的偏特化版本),得到`std::true_type<>`定义的 `value`。只有当 `T::InnerType` 不存在,导致`std::void_t<>` 定义失败的时候,编译器才会继续匹配第一个 `has_type_member<>` 的泛化版本。`has_type_member<>` 的泛化版本虽然因为 `void` 优先级低,总是最后才轮到,但是它能匹配任意情况,得到`std::false_type<>`定义的 `value`。根据 SFINAE 机制,编译器对之前的失败也不报错,于是 `has_type_member<>` 就完美地实现了自己的设计意图。
`std::false_type<>` 和 `std::true_type<>` 是 `std::integral_constant<>` 的一个特化版本,`has_type_member<>`借助这个继承关系得到了一个类型为 bool 的静态变量 value。
2.1.2 判断成员是否存在
借助于上一节的思路,判断一个类型中是否存在某个数据成员的方法非常简单,但是需要注意的是,因为`MemberName` 是数据成员的名字,所以`T::MemberName`就不是一个类型了,不能用做`std::void_t<>`的模板参数。这时候就要用到 `decltype()`了,让编译器推导出数据成员的类型:
template< class, class = void >
struct has_data_member : std::false_type { };template< class T >
struct has_data_member<T, std::void_t<decltype(T::Tom)>> : std::true_type { };
但是对于成员函数的判断,就不能直接使用 `decltype(T::Func)`,因为这只适用于静态成员函数的情况,对于非静态的成员函数来说,T::Func 是一个非法的表达式。假设 a 是 T 类型的一个对象实例,则 `a.T::Func` 或 `a.func` 才是合法的表达式。所以对于成员函数来说,如果还是使用 `decltype(T::Func)`的语法形式,则无论的一项是否有名为 Func 的成员函数,都会因为表达式非法而替换失败,从而匹配成 false_type 的结果。难道还要构造一个 T 类型的对象才行吗?当然不是,C++ 不是还有`std::declval()`嘛。
`std::declval() `的作用是返回任意类型 T 的右值引用类型 T&& ,可以借助这个右值引用调用 T 的成员函数,最终的效果就是在没有构造 T 的任何实例的情况下调用了 T 的成员函数,当然,这一切都是在编译期间完成的,编译器甚至都不需要函数的完整定义。具体的做法就是用`std::declval() `得到对象的右值引用,然后使用这个右值引用调用成员函数,再用`decltype()`推导函数调用返回值的类型:
std::void_t<decltype(std::declval<T>().Func())>
如果 T 中存在名为 `Func`的成员函数,则`std::declval<T>().Func()`就是一个合法的函数调用表达式,`decltype()`就能推导出函数返回值的类型,`std::void_t<>`的模板参数就是一个合法的类型,于是它的别名定义就合法。如果 T 中不存在名为 `Func`的成员函数,则`std::declval<T>().Func()`不是一个合法的表达式,最终`std::void_t<>`的定义就是错误的,这会触发 SFINAE 机制删除错误的替换结果,从而达到选择的目的。
根据上述分析,判断类型是否存在指定成员函数的实现可以这样做:
template<class T, class = std::void_t<>>
struct has_func_member : std::false_type {};template<class T>
struct has_func_member<T, std::void_t<decltype(std::declval<T>().fun())>> : std::true_type {};struct ObjectA {int fun() { return 42; }
};struct ObjectB {
};static_assert(has_func_member<ObjectA>::value);
static_assert(!has_func_member<ObjectB>::value);
2.2 判断表达式是否合法
`std::void_t<>`不仅可用于判断成员的存在性,还可用来判断表达式是否合法,实际上,2.1.2 节介绍成员函数的存在性判断时,就是利用了表达式是否合法的方式处理的。这一节,我们再介绍几个这样的例子。
2.2.1 判断是否支持前置++运算符
是的,实现思路也是尝试调用对象的前置 ++ 运算符,如果调用失败说明对象不支持前置 ++ 运算符:
template< class, class = void >
struct has_pre_increment_member : std::false_type { };template< class T >
struct has_pre_increment_member<T, std::void_t< decltype(++std::declval<T&>()) > > : std::true_type { };
`std::declval<T&>()`得到一个右值引用,对这个右值引用调用前置 ++ 运算符,如果表达式合法,则`std::void_t<decltype(++std::declval<T&>())>`的定义就是合法的,`has_pre_increment_member<>` 的 true_type 特化版本实例化成为最佳匹配。反之,若对前置 ++ 运算符的调用失败,则`has_pre_increment_member<>`特化版本就得到一个错误的替换结果,编译器于是“不动声色地”按照`has_pre_increment_member<>`的泛化版本实例化出 false_type 的版本。
2.2.2 判断是否支持迭代器
C++ 标准库中的容器都支持通过 begin() 和 end() 函数获得对应的迭代器,可以借助对这两个成员函数的存在性判断一个类型是否支持迭代器,当然,这不一定严谨,我们只是用这个例子展示 `std::void_t<>`的更多用法。
template <typename, typename = void>
struct is_iterable : std::false_type {};template <typename T>
struct is_iterable<T, std::void_t< decltype(std::declval<T>().begin()), decltype(std::declval<T>().end()) > > : std::true_type {};
对了,谁说 `std::void_t<>`只能用一个模板参数,这个例子不就用了两个嘛。
2.2.3 判断两个类型是否可做加法运算
实现的思路仍然是使用`std::declval()`得到两个对象的右值引用,然后让它们“加”一下,看看“加”的表达式是否合法。
template<typename U, typename V, typename T = void>
struct is_can_add : std::false_type { };template<typename U, typename V>
struct is_can_add<U, V, std::void_t< decltype(std::declval<U>() + std::declval<V>()) > > : std::true_type { };
3.std::void_t 与 std::enable_if
`std::enable_if<>` 可以用在函数返回值上,也可以用在函数参数或模板参数上,它的原理就是借助 `std::enable_if<>` 的失效条件产生错误的函数签名,然后利用 SFINAE 机制从函数候选集中移除替换失败的函数,从而达到选择特定的函数重载形式或类模板的实例化结果的目的,这是`std::enable_if<>`的使用特点。
`std::void_t<>`则可以用来判断一个类型的正确性或存在性,借助于`decltype` 和`std::declval()`,还可以用来判断一个表达式是否合法。如果存在性和合法性判断失败将导致`std::void_t<>`的定义失败,利用这一点配合 SFINAE 机制也可以实现在编译其的一些类型选择。
与传统的使用 SFINAE 机制的方法相比,`std::enable_if<>`和`std::void_t<>`具有更简洁的语义表达方式和更直观的语法形式,使用的方法也很方便。它们一直被认为是 C++ 20 的概念(concept)推出之前使用 SFINAE 机制的最佳方式。
推荐阅读:
C++反射之检测struct或class是否实现指定函数
C++之std::declval
C++之std::enable_if
相关文章:

C++17之std::void_t
目录 1.std::void_t 的原理 2.std::void_t 的应用 2.1.判断成员存在性 2.1.1.判断嵌套类型定义 2.1.2 判断成员是否存在 2.2 判断表达式是否合法 2.2.1 判断是否支持前置运算符 2.2.3 判断两个类型是否可做加法运算 3.std::void_t 与 std::enable_if 1.std::void_t 的…...
零基础入门篇①⑥ Python可变序列类型--字典
Python从入门到精通系列专栏面向零基础以及需要进阶的读者倾心打造,9.9元订阅即可享受付费专栏权益,一个专栏带你吃透Python,专栏分为零基础入门篇、模块篇、网络爬虫篇、Web开发篇、办公自动化篇、数据分析篇…学习不断,持续更新,火热订阅中🔥专栏限时一个月(5.8~6.8)重…...
C语言面试题1-10
C语言中的内存管理及相关问题探讨 在C语言编程中,内存管理是一个至关重要的概念,掌握内存的分布及其操作不仅能够提高代码效率,还能避免常见的内存泄漏等问题。本文将详细介绍C语言中内存的分布、堆区和栈区的区别、标识符的命名规则、定义和…...

Qt Designer工具如何修改MainWindow窗口的标题
Qt Designer工具如何修改MainWindow窗口的标题 在MainWindow的属性编辑器中选择“windowTitle”后面一栏修改成期望的窗口标题名称即可。 按住“ctrlR”即可查看可视化界面的窗口标题...

车辆前向碰撞预警系统性能要求和测试规程
前言 本文整理《GB/T 33577-2017 智能运输系统-车辆前向碰撞预警系统性能要求和测试规程》国标文件关键信息,FCW系统性能和测试右给深层次的认识。 术语和定义 车辆前向碰撞预警系统 forward vehicle collision warning system自车 subject vehicle(SV)目标车辆 target ve…...
C#实现winform中渲染图的展示
在WinForms中实现图形的渲染展示,可以使用GDI绘图技术。下面是一个简单的示例,演示如何在WinForms中展示一个圆形图形,并根据用户输入的半径动态改变圆的大小: 请在Visual Studio中创建一个WinForms应用程序,并将以下…...
JTS库的讲解及使用
JTS(Java Topology Suite)是一套用于创建、操作和分析二维几何对象的Java库。JTS提供了丰富的几何操作和分析功能,是GIS(地理信息系统)应用中的重要工具。以下是JTS库的一些主要功能及其详细使用示例: 1. …...

【C++杂货铺】unordered系列容器
目录 🌈 前言🌈 📁 unordered系列关联式容器 📁 底层结构 📂 哈希概念 📂 哈希冲突 📂 哈希函数 📂 哈希冲突解决 📁 模拟实现 📁 总结 🌈 前…...

模板-初阶
引言: 在C,我们已经学过了函数重载,这使得同名函数具有多个功能。但是还有一种更省力的方法:采用模板。 本文主要介绍以下内容 1. 泛型编程 2. 函数模板 3. 类模板 1.泛型编程 在将这一部分之前,通过一个故事引…...

重载运算符C++---学习笔记
一、笔记 1. 重载运算符基础知识 重载运算符进行的运算和普通数的加减运算不同之处在于重载运算符的操作数为一个一个自定义的对象,所以相应的要对普通的运算符如-*%/的调用方法进行重写,重载的本质还是函数调用 2. 重载运算符的语法 重载运算符的语…...

SpringMVC枚举类型字段处理
在日常的项目开发中经常会遇到一些取值范围固定的字段,例如性别、证件类型、会员等级等,此时我们可以利用枚举来最大程度减少字段的乱定义,统一管理枚举的值。 SpringMVC中对于枚举也有默认的处理策略: 对于RequestParam…...

集成算法:Bagging模型、AdaBoost模型和Stacking模型
概述 目的:让机器学习效果更好,单个不行,集成多个 集成算法 Bagging:训练多个分类器取平均 f ( x ) 1 / M ∑ m 1 M f m ( x ) f(x)1/M\sum^M_{m1}{f_m(x)} f(x)1/M∑m1Mfm(x) Boosting:从弱学习器开始加强&am…...
DW怎么Python:探索Dreamweaver与Python的交织世界
DW怎么Python:探索Dreamweaver与Python的交织世界 在数字世界的广袤天地中,Dreamweaver(简称DW)与Python这两大工具各自闪耀着独特的光芒。DW以其强大的网页设计和开发能力著称,而Python则以其简洁、易读和强大的编程…...

算法(十三)回溯算法---N皇后问题
文章目录 算法概念经典例子 - N皇后问题什么是N皇后问题?实现思路 算法概念 回溯算法是类似枚举的深度优先搜索尝试过程,主要是再搜索尝试中寻找问题的解,当发生不满足求解条件时,就会”回溯“返回(也就是递归返回&am…...

论文阅读:Correcting Motion Distortion for LIDAR HD-Map Localization
目录 概要 Motivation 整体架构流程 技术细节 小结 论文地址:http://arxiv.org/pdf/2308.13694.pdf 代码地址:https://github.com/mcdermatt/VICET 概要 激光雷达的畸变矫正是一个非常重要的工作。由于扫描式激光雷达传感器需要有限的时间来创建…...

Git操作笔记
学git已经好多次了。但是还是会忘记很多的东西,一些常用的操作命令和遇到的bug以后在这边记录汇总下 一.github图片展示 图片挂载,我是创建了一个库专门存图片,然后在github的md中用专用命令展示图片,这样你的md就不会全是文字那…...
使用Python进行数据分析的基本步骤
简介: 在当今的数据驱动世界中,数据分析已成为各行各业不可或缺的一部分。Python作为一种强大的编程语言,提供了丰富的库和工具,使得数据分析变得简单易行。本文将带你了解使用Python进行数据分析的基本步骤。 一、数据获取 从外…...

NGINX优化
NGINX优化分为两个方面: 一. nginx应用配置文件的优化: 1.nginx的性能优化: 全局块: 设置工作进程数: work_processes #设置工作进程数 设置工作进程连接数:work_rilmit_nofile #设置每个worker进程最大可…...

【LeetCode刷题】二分查找:山脉数组的峰顶索引、寻找峰值
【LeetCode刷题】Day 13 题目1:852.山脉数组的峰顶索引思路分析:思路1:暴力枚举O(N)思路2:二分查找O(logN) 题目2:162.寻找峰值思路分析:思路1:二分查找O(logN) 题目1:852.山脉数组的…...
《Python学习》-- 实操篇一
一、文件操作 1. 1 读取文本文件 # 文件操作模式 # r (默认) - 只读模式。文件必须存在,否则会抛出FileNotFoundError。在这种模式下,你只能读取文件内容,不能写入或追加。 # w - 写入模式。如果文件存在,内容会被清空ÿ…...

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1
每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...

PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...

android RelativeLayout布局
<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...