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
相关文章:
C++ 的 CTAD 与推断指示(Deduction Guides)
1 类模板参数推导(CTAD) 1.1 曲线救国 CTAD 的全称是类模板参数推导(Class Template Argument Deduction),它允许在实例化类模板时,根据构造函数的参数类型自动推导模板参数,从而避免显式指…...
【Rust自学】13.2. 闭包 Pt.2:闭包的类型推断和标注
13.2.0. 写在正文之前 Rust语言在设计过程中收到了很多语言的启发,而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。 在本章中,我们会讨论 Rust 的一…...
如何将原来使用cmakelist编译的qt工程转换为可使用Visual Studio编译的项目
将原来使用CMakeLists.txt编译的Qt工程转换为可使用Visual Studio编译的项目,可以通过以下步骤实现: 一、准备阶段 安装必要的软件: 确保已安装Visual Studio,并选择了C开发相关的组件。安装CMake,并确保其版本与Qt和…...
微软确认Win10停更不碍Microsoft 365使用!未来是否更新成谜
快科技1月17日消息,微软澄清了关于Windows 10停止支持后Microsoft 365办公套件使用情况的误解。 前两天微软更新支持文档,表示2025年10月14日Windows 10停止支持之后,Microsoft 365应用程序将不再支持Windows 10设备,引发用户担忧…...
Ubuntu、Windows系统网络设置(ping通内外网)
一、 虚拟机VMware和Ubuntu系统的网络配置说明 1、虚拟机的网络适配器的模式有三种: 桥接模式NAT模式主机模式 2、虚拟机VMware的网卡配置(如何进行配置界面(虚拟机->设置)) 注意: 1、以上桥接模式(ubuntu有独立IP)、NAT模式(没有独立IP)都可以联…...
华为OD机试E卷 ---最大值
一、题目描述 给定一组整数(非负),重排顺序后输出一个最大的整数。 二、示例1 用例1 输入 10 9输出 910说明:输出结果可能非常大,所以你需要返回一个 字符串只而不是整数。 三、输入描述 数字组合 四、输出描述 最大的整数 五、解题思路 字符…...
UllnnovationHub,一个开源的WPF控件库
目录 UllnnovationHub1.项目概述2.开发环境3.使用方法4.项目简介1.WPF原生控件1.Button2.GroupBox3.TabControl4.RadioButton5.SwitchButton6.TextBox7.PasswordBox8.CheckBox9.DateTimePicker10.Expander11.Card12.ListBox13.Treeview14.Combox15.Separator16.ListView17.Data…...
Fabric区块链网络搭建:保姆级图文详解
目录 前言1、项目环境部署1.1 基础开发环境1.2 网络部署 2、后台环境2.1、环境配置2.2、运行springboot项目 3、PC端3.1、安装依赖3.2、修改区块链网络连接地址3.3、启动项目 前言 亲爱的家人们,创作很不容易,若对您有帮助的话,请点赞收藏加…...
Kubernetes (K8s) 权限管理指南
1. 引言 Kubernetes (K8s) 作为当今最流行的容器编排平台,其安全性至关重要。本指南旨在全面介绍 K8s 的权限管理机制,帮助具有一定基础的读者深入理解并掌握这一关键领域。 © ivwdcwso (ID: u012172506) 2. Kubernetes 安全模型概述 K8s 的安全模型主要包括三个阶段…...
IM聊天学习资源
文章目录 参考链接使用前端界面简单效果消息窗口平滑滚动至底部vue使用watch监听vuex中的变量变化 websocket握手认证ChatKeyCheckHandlerNettyChatServerNettyChatInitializer 参考链接 zzhua/netty-chat-web - 包括前后端 vue.js实现带表情评论功能前后端实现(仿…...
计算机视觉模型的未来:视觉语言模型
一、视觉语言模型 人工智能已经从识别数据中的简单模式跃升为理解复杂的多模态数据。该领域的发展之一是视觉语言模型 (VLM) 的兴起。这类模型将视觉和文本之间联系起来,改变了我们理解视觉数据并与之交互的方式。随着 VLM 的不断发展,它们正在为计算机视觉设定一个新的水平…...
【JAVA 基础 第(19)课】Hashtable 类用法和注意细节,是Map接口的实现类
Map接口:存放的是具有映射关系的键值对,键映射到值,键必须是唯一的 Hashtable 类,Map接口的实现类,键和值都不能为nullHashtable 是同步的,是线程安全的 public class MapTest {public static void main(String[] arg…...
浅谈 JVM
JVM 内存划分 JVM 内存划分为 四个区域,分别为 程序计数器、元数据区、栈、堆 程序计数器是记录当前指令执行到哪个地址 元数据区存储存储的是当前类加载好的数据,包括常量池和类对象的信息,.java 编译之后产生 .class 文件,运…...
html的iframe页面给帆软BI发送消息
需求:帆软的网页组件嵌套一个HTML页面,HTML页面要给帆软发消息。 解决方法是:fineReportWindow.duchamp.getWidgetByName("txt1").setValue(666); <!DOCTYPE html> <html lang"en"> <head> <…...
spark任务优化参数整理
以下参数中有sql字眼的一般只有spark-sql模块生效,如果你看过spark的源码,你会发现sql模块是在core模块上硬生生干了一层,所以反过来spark-sql可以复用core模块的配置,例外的时候会另行说明,此外由于总结这些参数是在不…...
C++ 模拟真人鼠标轨迹算法 - 防止游戏检测
一.简介 鼠标轨迹算法是一种模拟人类鼠标操作的程序,它能够模拟出自然而真实的鼠标移动路径。 鼠标轨迹算法的底层实现采用C/C语言,原因在于C/C提供了高性能的执行能力和直接访问操作系统底层资源的能力。 鼠标轨迹算法具有以下优势: 模拟…...
生产环境中常用的设计模式
生产环境中常用的设计模式 设计模式目的使用场景示例单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点- 日志记录器- 配置管理器工厂方法模式定义一个创建对象的接口,让子类决定实例化哪个类- 各种工厂类(如视频游戏工厂模式创…...
基于SpringBoot+Vue的药品管理系统【源码+文档+部署讲解】
系统介绍 基于SpringBootVue实现的药品管理系统采用前后端分离的架构方式,系统实现了用户登录、数据中心、药库管理、药房管理、物资管理、挂号管理、系统管理、基础设置等功能模块。 技术选型 开发工具:idea2020.3Webstorm2020.3 运行环境ÿ…...
【CompletableFuture实战】
CompletableFuture实战 前言 前言 过去的一年,匆匆忙忙,换了一次工作,写博客的习惯就落下了,总之,有点懈怠。希望今年能重拾信心,步入正规! CompletableFuture的用法网上资料颇多,…...
Redis 缓存穿透、击穿、雪崩 的区别与解决方案
前言 Redis 是一个高性能的键值数据库,广泛应用于缓存、会话存储、实时数据分析等场景。然而,在高并发的环境下,Redis 缓存可能会遇到 缓存击穿、缓存穿透 和 缓存雪崩 这三大问题。这些问题不仅影响系统的稳定性和性能,还经常出…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...
让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南 在数字化营销时代,邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天,我们将深入解析邮件打开率、网站可用性、页面参与时…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...
RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
