C++ 的 CTAD 与推断指示(Deduction Guides)

1 类模板参数推导(CTAD)

1.1 曲线救国

​ CTAD 的全称是类模板参数推导(Class Template Argument Deduction),它允许在实例化类模板时,根据构造函数的参数类型自动推导模板参数,从而避免显式指定模板参数。CTAD 是在 C++ 17 引入的,在这之前,只有模板函数支持根据函数参数自动推导模板参数,类模板不支持这样的动作。代码中实例化类模板必须显式指定模板参数,十分不便,以致怨声载道。

​ C++ 11 引入了 auto,用作占位符衍生出了一种“工厂函数”惯用法,就是利用函数模板的推导规则,根据函数参数推导出模板参数,然后用推导出的模板参数实例化类对象。比如这个例子:

template<typename T, typename U>
class Foo {
public:Foo(T begin, U end) : m_begin(std::move(begin)), m_end(std::move(end)) {}
private:T m_begin;U m_end;
};template<typename T, typename U>
auto MakeFoo(T begin, U end) {return Foo<T, U>{begin, end};
}auto f2 = MakeFoo(42, 5.24);

1.2 隐式规则

​ C++ 17 的 CTAD 默认通过类模板的构造函数定义模板参数的推导规则,和函数模板一样,由构造函数的实参类型决定模板的参数类型。比如上一节的 Foo 类,不需要工厂函数,可以直接这样用:

Foo f1{42, 5.24};

但是编译器对类模板参数的推导是有条件的,那就是构造函数的形式参数列表必须能覆盖全部模板参数,并且这些形参都必须参与推导,不能有在非推导语境中的模板参数。简单来说,以下两个类模板就不支持 CTAD 隐式推导:

template<typename T, typename U>
struct Bar {Bar(const T& t) {}
};template<typename T, typename U>
struct Widget {Widget(const T& t, typename std::type_identity_t<U> u) {}
};Bar b1(42); //错误
Widget w(1, 2.3); //错误,不能实例化 Widget<int, double> 类型

Bar 的构造函数形参列表只覆盖了一个模板参数,另一个未知,不能通过构造函数同时推导出T 和 U 的类型。Widget 同样不支持 CTAD,它的构造函数形参覆盖了两个模板参数,但是 U 出现在典型的非推导语境中,它不参与推导,编译器不会根据实参 2.3 去推导 U 为 double,所以不能同时确定 T 和 U 的类型,也就无法实例化 Widget<int, double> 类型。

1.3 演化

​ CTAD 在 C++ 20 改善了一下对聚合类型的支持。对于聚合类型,可以在不提供显式构造函数的情况下,按照聚合类型的初始化顺序实现类型推导。我们假设下面例子中的 Foo 是个聚合类型,为什么假设呢?因为是不是聚合类型还要看它那三个成员的类型,我们这里给出的例子能确保 Foo 实例化后是个聚合类型。

template<typename T, typename U, typename V>
struct Foo {T t;U u;V v;
};Foo f{ 1, 2.3, "Hello" };

大括号中的参数,按照按照聚合类型的初始化顺序,以及传值类型模板形参的推导规则,依次与 t、u 和 v 匹配,推导出 T、U 和 V 的类型为 int、double 和 const char* ,并用 Foo<int, double, const char*> 类型初始化 f。

2 推断指示(Deduction Guides)

2.1 什么是推断指示

​ 尽管 CTAD 可以根据构造函数参数自动推导模板参数,但有些复杂情况下,隐式的规则可能无法满足需求。此时我们可以利用 C++ 17 的显示推断指示(推断指引),通过提供自定义的模板参数推导规则,让编译器知道如何确定类模板的模板参数,从而实现复杂类模板参数的自动推导。

​ 推断指示的语法大概是这个样子的:

//deduction-guide:
explicit(opt) template-name (parameter-declaration-clause) -> simple-template-id ;

explicit 关键字是可选的,用于说明是否是显式推断指示。这个语法的重点是 减号和大于号组成的箭头符号(->),箭头符号左边的 template-name 必须与箭头符号右边的 simple-template-id 具有相同的标识符。此外,如果一个 template-name 有多个推断指引,那么它们的 parameter-declaration-clause 不能相同。以 std::tuple 为例,看看它的推断指引的语法:

template<class... UTypes>
tuple(UTypes...) -> tuple<UTypes...>;

箭头符号的左边是 std::tuple 的构造函数(之一),其中 UTypes… 就是传递给构造函数的参数包(就是 parameter-declaration-clause)。箭头符号的右边是 std::tuple 的模板参数(simple-template-id),这个语法告诉编译器,可以根据构造函数的参数推断对应的类模板实例化使用的模板参数。

2.2 推断指示的典型用法

​ ContainerT 类有一个符合 CTAD 的构造函数,c1 就是通过这个构造函数提供的隐式规则,推导出 c1 的类型是 ContainerT。但是当我们希望传递一个大括号列表的时候,我们希望 T 是一个 vector 容器类型,此时构造函数提供的默认规则就无能为力了。c2 的定义会导致编译错误,因为模板形参推导不支持大括号列表(auto 的推导支持将大括号列表推导为具体的 std::initializer_list 类型,但这是个写死的规则,算不上推导)。

template <typename T>
class ContainerT {
public:ContainerT(T value) : val(value) {}T val;
};ContainerT c1(5); //ContainerT<int>
ContainerT c2({ 1, 5, 8 }); //错误

​ 为了达成目标,我们需要为 ContainerT 类模板提供一个显式推断指示,通过显式推断指示明确模板参数 T 是 vector 类型。这是我们提供的推断指示:

template <typename U>
ContainerT(std::initializer_list<U>) -> ContainerT<std::vector<U>>;

函数形参不支持自动推导成 std::initializer_list,我们干脆写死就是 std::initializer_list,它与大括号列表是可以匹配的,相当于只需推导 std::initializer_list 的模板参数 U。当确定了 U 之后,我们希望 ContainerT 的模板参数是 std::vector,这就是这条显式推断指示的语法解释。有了这条推断指示,c2 的定义就合法了,并且也得到了我们希望的 ContainerT<std::vector> 类型。

​ 再来看一个稍微复杂一点的例子:

template<typename T>
class Foo {
public:Foo(T value) {m_values.push_back(std::move(value));}template<class Iter>Foo(Iter begin, Iter end) {std::copy(begin, end, std::back_inserter(m_values));}
private:std::vector<T> m_values;
};Foo f1{ 5 }; // Foo<int> std::vector<int> vi{ 1, 3, 5, 7 };
Foo f2{ vi.begin(), vi.end() }; //错误

Foo 有两个构造函数,第一个构造函数配合 CTAD,使得 f1 的定义没有问题,但是 f2 的定义不被编译器支持,因为通过构造函数传递的两个迭代器,编译器无法推断出模板参数 T 的类型。当我们拿到一对迭代器的时候,我们可以通过类型萃取获得迭代器的值类型,可以将这个值类型指代类模板参数 T。

​ 按照这个思路得到推断指示:

template<class Iter>
Foo(Iter begin, Iter end)->Foo<typename std::iterator_traits<Iter>::value_type>;

有了这个显式推断指示,上面例子代码中 f2 的定义就合法了,并且得到的 f2 的类型也是我们希望的 Foo 类型。

2.3推断指示的非典型用法

​ 显示推断指引可以用在一些需要提供类模板特化版本的场合,比如下面这个例子中的 Foo 类模板,当面对指针类型的时候,比如字符串字面量,如果按照默认的构造函数提供的 CTAD,T 被推导为指针,成员 m_t 只保存了字符串的指针,在很多情况下,这都是比较危险的,一不小心就出现野指针访问。传统方法是针对指针类型提供特化版本,就如同这个例子一样。

template<class T>
struct Foo {Foo(T t) { m_t = t; }T m_t;
};//特化版本
template<>
struct Foo<const char*> {Foo(const char* t) { m_t = t; }std::string m_t;
};

​ 提供特化版本也没什么不妥,就是要敲很多键盘。但是如果用显式推断指示,只需一行代码就可以了:

//推断指引
Foo(const char*)->Foo<std::string>;

少敲几次键盘,还不需要提供函数体的代码,通过类型的指示,复用原来的构造函数,有什么利用不用推断指示?

3 总结

​ CTAD 拖了这么长时间实在气愤,好在显式推断指示让类模板参数的自动推导比函数模板的模式匹配强大 N 倍,也就没那么大的气了。显式推断指示在标准库中也是大量引用,比如你可以这样定义一个 array:

std::array arr{1, 2, 3, 4, 5};

因为它有一条这样的推断指示:

template <class... T>
array(T&&... t) -> array<std::common_type_t<T...>, sizeof...(T)>;

参考资料

[1] Marc Gregoire, Professional C++ (Fifth Edition), John Wiley & Sons, Inc., 2021

[2] https://en.cppreference.com/w/cpp/language/class_template_argument_deduction

[3] Nicolai M. Josuttis, C++20 - The Complete Guide, http://leanpub.com/cpp20’

[4] Jacek Galowicz. C++17 STL Cookbook. Packtpub. 2017

[5] P0702:Language support for Constructor Template Argument Deduction

[6] CWG 2628:Implicit deduction guides should propagate constraints

关注作者的算法专栏
https://blog.csdn.net/orbit/category_10400723.html

关注作者的出版物《算法的乐趣(第二版)》
https://www.ituring.com.cn/book/3180

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/2931.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

sparkSQL练习

1.前期准备 &#xff08;1&#xff09;建议先把这两篇文章都看一下吧&#xff0c;然后把这个项目也搞下来 &#xff08;2&#xff09;看看这个任务 &#xff08;3&#xff09;score.txt student_id,course_code,score 108,3-105,99 105,3-105,88 107,3-105,77 105,3-245,87 1…

使用docker-compose安装ELK(elasticsearch,logstash,kibana)并简单使用

首先服务器上需要安装docker已经docker-compose&#xff0c;如果没有&#xff0c;可以参考我之前写的文章进行安装。 https://blog.csdn.net/a_lllk/article/details/143382884?spm1001.2014.3001.5502 1.下载并启动elk容器 先创建一个网关&#xff0c;让所有的容器共用此网…

vue3+elementPlus之后台管理系统(从0到1)(day1)

vue3官方文档&#xff1a;https://cn.vuejs.org/guide/introduction.html 1、项目创建 确保电脑已安装node 查看命令&#xff1a; node -v进入项目目录&#xff0c;创建项目 npm init vuelatest Need to install the following packages: create-vue3.13.0 Ok to procee…

SparkSQL数据模型综合实践

文章目录 1. 实战概述2. 实战步骤2.1 创建数据集2.2 创建数据模型对象2.2.1 创建常量2.2.2 创建加载数据方法2.2.3 创建过滤年龄方法2.2.4 创建平均薪水方法2.2.5 创建主方法2.2.6 查看完整代码 2.3 运行程序&#xff0c;查看结果 3. 实战小结 1. 实战概述 在本次实战中&#…

软件设计大致步骤

由于近期在做软件架构设计&#xff0c;这里总结下大致的设计流程 软件设计流程 1 首先要先写系统架构图&#xff0c;将该功能在整个系统的位置以及和大致的内部模块划分 2 然后写内部的结构图&#xff0c;讲内部的各个子系统&#xff0c;模块&#xff0c;组件之间的关系和调用…

读取长文本,使用读取底表

文章目录 代码有原始数据内表作为主表连接STXL的示例获取物料分类获取物料分类的文本的宏读取分类 https://blog.csdn.net/DeveloperMrMeng/article/details/118354649 代码 "第三种&#xff1a;读取底表获取文本 DATA: LT_TLINE TYPE STANDARD TABLE OF TLINE. DATA: LS…

阀井可燃气体监测仪,开启地下管网安全新篇章-旭华智能

在城市的脉络中&#xff0c;地下管网犹如隐秘的动脉&#xff0c;支撑着现代生活的运转。而在这庞大网络的关键节点上&#xff0c;阀井扮演着不可或缺的角色。然而&#xff0c;由于其密闭性和复杂性&#xff0c;阀井内部一旦发生可燃气体泄漏&#xff0c;将对公共安全构成严重威…

Golang结合MySQL和DuckDB提高查询性能

要在Golang中组合MySQL和DuckDB以提高查询性能&#xff0c;请考虑使用混合查询执行方法。这种方法利用了MySQL强大的事务管理和DuckDB闪电般的分析处理能力。本文介绍如何充分利用两者的方法。 各取所长 用MySQL处理事务&#xff0c;用DuckDB处理分析 MySQL应该处理常规的INS…

Docker PG流复制搭建实操

目录标题 制作镜像1. 删除旧的容器2. 创建并配置容器3. 初始化数据库并启动 主库配置参数4. 配置主库5. 修改 postgresql.conf 配置 备库配置参数6. 创建并配置备库容器7. 初始化备库 流复制8. 配置&检查主库复制状态9. 检查备库配置 优化建议问题1&#xff1a;FATAL: usin…

增广卡尔曼滤波AKF的要点分析

增广卡尔曼滤波(Augmented Kalman Filter, AKF)是相对特定的卡尔曼滤波模型来说的,在状态量和/或观测量上进行增广,以满足特定的要求。 通常用于:专门用于处理具有状态噪声和观测噪声的不确定性,尤其是在需要同时估计系统状态和额外参数时。它通过将额外的参数或状态变量…

三只松鼠携手爱零食,社区零售新高峰拔地而起

合纵连横&#xff0c;这是当前零售行业发展的一个主旋律。从商超之王胖东来的全国调改&#xff0c;到社区零售正在进行的渠道变革&#xff0c;竞争的激烈和商业模式的升级令人目不暇接。 量贩零食赛道在过去一年就是如此&#xff0c;有杀伐&#xff0c;有并购&#xff0c;刀光…

Java并发编程——线程池(基础,使用,拒绝策略,命名,提交方式,状态)

我是一个计算机专业研0的学生卡蒙Camel&#x1f42b;&#x1f42b;&#x1f42b;&#xff08;刚保研&#xff09; 记录每天学习过程&#xff08;主要学习Java、python、人工智能&#xff09;&#xff0c;总结知识点&#xff08;内容来自&#xff1a;自我总结网上借鉴&#xff0…

Linux第二课:LinuxC高级 学习记录day04

6、shell中的语句 6.3、结构性语句 6.3.1、if if…then…fi 1、结构 1&#xff09;基本结构 if 表达式 then 命令表 fi if [ 表达式 ] // 【】两侧有空格 then 命令表 fi 2&#xff09;分层结构 if 表达式 then 命令表1 else 命令表2 fi 3&#xff09;嵌套结构 if …

tomcat文件目录讲解

目录的用处 bin&#xff1a;tomcat的可执行命令&#xff0c;比如&#xff1a;tomcat的启动停止命令&#xff0c;也包含其他命令以及.bat&#xff08;Windows执行的命令&#xff09;和.sh&#xff08;Linux操作系统执行的命令&#xff09;文件config:关于tomcat的配置&#xff0…

【Rust自学】12.4. 重构 Pt.2:错误处理

12.4.0. 写在正文之前 第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep(Global Regular Expression Print)&#xff0c;是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。 这个项目分为这么几步&#xff1a; 接收命令行参数读取…

梁山派入门指南4——定时器使用详解,包括定时器中断、PWM产生、输入捕获测量频率

梁山派入门指南4——定时器使用详解&#xff0c;包括定时器中断、PWM产生、输入捕获测量频率 1. 定时器概览2.基本定时器2.1 基本定时器介绍2.2 梁山派上的基本定时器开发2.2.1. 了解梁山派上的基本定时器资源&#xff08;实际上我们以及在上面了解过了&#xff09;2.2.2. 配置…

excel仅复制可见单元格,仅复制筛选后内容

背景 我们经常需要将内容分给不同的人&#xff0c;做完后需要合并 遇到情况如下 那是因为直接选择了整列&#xff0c;当然不可以了。 下面提供几种方法&#xff0c;应该都可以 直接选中要复制区域然后复制&#xff0c;不要选中最上面的列alt;选中可见单元格正常复制&#xff…

JVM类加载器(附面试题)

什么是类加载器 类加载器&#xff08;ClassLoader&#xff09; 是 Java 虚拟机&#xff08;JVM&#xff09;中的一个组件&#xff0c;用于在运行时将字节码文件加载到内存中&#xff0c;并将其转换为 JVM 可以执行的二进制数据结构。 字节码文件通常是以.class为扩展名的文件…

FFmpeg硬件解码

使用FFmpeg进行硬件解码时&#xff0c;通常需要结合FFmpeg的API和硬件加速API&#xff08;如CUDA、VAAPI、DXVA2等&#xff09;。以下是一个简单的C代码示例&#xff0c;展示如何使用FFmpeg进行硬件解码。这个示例使用了CUDA作为硬件加速的后端。 1. 安装FFmpeg和CUDA 确保你…

Linux 进程前篇(冯诺依曼体系结构和操作系统)

目录 一.冯诺依曼体系结构 1.概念 2.硬件层面的数据流 3.总结加补充 二.操作系统 (Operating System) 1.概念 2.设计OS的目的 3.定位 4.操作系统的管理 5.计算机体系的层状结构 在我们认识进程之前&#xff0c;我们先了解什么是冯诺依曼体系结构 一.冯诺依曼体系结构…