系统性掌握C++17容器四件套:std::optional, std::any, std::variant, std::tuple

昨天在写《深入探讨C++的高级反射机制(2):写个能用的反射库》的时候,正好遇到动态反射需要的类型擦除技术。所谓的类型擦除,就是在两个模块之间的接口层没有任何类型信息,实现两个模块之间安全的通信。可以理解为:
在这里插入图片描述
为了实现这个功能,于是用到了std::any这个工具。考虑到许多开发者分不清std::variant和std::any之间的区别,于是萌发了写一篇文章系统性介绍一下他们的想法。

传统上,C++17标准为C++的类型系统和容器库带来了重要的补充和改进,std::optional, std::any, 和 std::variant这三种类型被统称为“C++17容器三剑客”,本文将在C++11引入在C++17获得的增强的std::tuple也纳入介绍,系统性地揭示C++体系中这类容器的完整面貌。

1. std::optional —— 语义明确的可选值

std::optional是一个模板类型,它提供了一种表示“可能没有值”的方式。在C++17之前,程序员通常会使用指针、特殊值或者布尔标记来表达这种“可选”语义,这些方法都有其局限性和缺陷。std::optional的出现,让这种表达方式变得更加安全和直观。

1.1 std::optional的基本概念
std::optional可以看作是一个可能包含类型T的值的容器。它提供了一种检查是否存储了值的安全方式,并且可以用简洁的API访问该值或者处理值不存在的情况。

1.2 使用场景

  • 函数可能无法返回有效值时
  • 配置项可能未设置时
  • 缓存结果可能不存在时

1.3 基本用法

#include <optional>
#include <iostream>

std::optional<int> maybeGetInt(bool flag) {
    if (flag) {
        return 123; // 返回有效的int
    }
    return {}; // 返回一个空的optional
}

int main() {
    auto val = maybeGetInt(true);
    if (val) { // 检查是否含有值
        std::cout << "Value: " << *val << std::endl; // 解引用访问值
    }

    auto noVal = maybeGetInt(false);
    std::cout << "No value: " << noVal.value_or(-1) << std::endl; // 使用value_or提供默认值

    return 0;
}

2. std::any —— 类型安全的void*

对于需要存储任意类型的值,C++17提供了std::any。这个容器可以存储任何类型的单个值,并且能够在运行时安全地访问存储的值。这对于编写泛型代码或者需要类型擦除的场景非常有用。非常类似C语义中的void*,不过差别是void*本身不能直接存储对象,而std::any本身提供了存储对象的能力。当然,也可以用std::any存储指针类型。

2.1 std::any的基本概念
std::any可以存储任意类型的值,只要该类型是可复制的。使用std::any_cast可以试图取回原始类型的值,如果类型不匹配,会抛出std::bad_any_cast异常。

2.2 使用场景

  • 动态类型的API设计
  • 类型安全的容器
  • 简化类型擦除实现

2.3 基本用法

#include <any>
#include <iostream>

int main() {
    std::any a = 10;
    std::cout << std::any_cast<int>(a) << std::endl; // 正确类型转换

    a = std::string("Hello, std::any!");
    std::cout << std::any_cast<std::string>(a) << std::endl; // 正确类型转换

    try {
        std::cout << std::any_cast<float>(a) << std::endl; // 错误类型转换,将抛出异常
    } catch (const std::bad_any_cast& e) {
        std::cout << e.what() << std::endl;
    }
	// 你可以使用不抛异常的指针版本的转换:
	auto casted_a = std::any_cast<float*>(a); // 错误类型转换,但不会抛异常,返回空指针
    return 0;
}

2.4 实现原理
std::any 的本质就是一段内存,内存承载的对象是“CopyConstructible”。在MSVC的STL中,如果是平凡对象,则直接存储,否则如果对象地址小于48字节,那么就会直接存储在std::any成员数组中,如果都不满足,就会通过malloc申请对于内存进行存储。
在这里插入图片描述
std::any在还原类型时,核心逻辑如下:
在这里插入图片描述
这段代码笔者依次注释如下:

template <class _Decayed>
_NODISCARD const _Decayed* _Cast() const noexcept {
    const type_info* const _Info = _TypeInfo();
      
    // 首先获取 std::any 对象当前存储值的类型信息。如果没有存储任何值(_Info 为空),或者存储的值的类型不是 _Decayed 类型,那么函数会返回 nullptr。
    if (!_Info || *_Info != typeid(_Decayed)) {
        return nullptr;
    }
    
    // 判断 _Decayed 类型是否为简单类型(POD 类型,可以直接复制内存)
    if constexpr (_Any_is_trivial<_Decayed>) {
        // 获取指向存储的 _Decayed 类型的平凡(trivial)值的指针
        return reinterpret_cast<const _Decayed*>(&_Storage._TrivialData);
    } 
    // 判断 _Decayed 类型是否足够小,可以存储在 std::any 的小对象优化缓冲区中
    else if constexpr (_Any_is_small<_Decayed>) {
        // 获取指向存储的 _Decayed 类型的小对象(small object)值的指针
        return reinterpret_cast<const _Decayed*>(&_Storage._SmallStorage._Data);
    } 
    // 大对象情况,即 _Decayed 类型的对象无法放入小对象优化缓冲区中,需要动态分配内存
    else {
        // 获取指向存储的 _Decayed 类型的大对象(big object)值的指针
        return static_cast<const _Decayed*>(_Storage._BigStorage._Ptr);
    }
}


3. std::variant —— 安全的联合体

std::variant是一个类型安全的联合体。它可以存储定义在它的模板参数列表中的任意类型的值。与C联合体不同的是,std::variant总是知道它当前存储的是哪种类型的值。

3.1 std::variant的基本概念
std::variant<…>可以被理解为一个可以存储多种类型中的一种的容器。使用std::get或std::get_if可以安全地访问存储的值。如果访问的类型不是当前存储的类型,会抛出std::bad_variant_access异常。

3.2 使用场景

  • 需要在同一位置存储不同类型值的情况
  • 替代传统的union或void*指针
  • 类型安全的状态机实现

3.3 基本用法

#include <variant>
#include <iostream>
#include <string>

int main() {
    std::variant<int, std::string> v = 20;
    std::cout << std::get<int>(v) << std::endl; // 正确类型访问

    v = "Variant can hold a string now!";
    std::cout << std::get<std::string>(v) << std::endl; // 正确类型访问

    try {
        std::cout << std::get<double>(v) << std::endl; // 错误类型访问,将抛出异常
    } catch (const std::bad_variant_access& e) {
        std::cout << e.what() << std::endl;
    }

    return 0;
}

4. std::tuple—— 异构元素的组合器

虽然std::tuple并非C++17的新特性,它自C++11起就已经存在,但它在C++17获得更好的完善和加强,并且与std::variant有着互补的特性,在处理类型异构的数据结构时非常有用。

std::tuple允许我们将任意数量和类型的元素组合成单一对象。与std::variant相比,std::tuple可以存储多个不同类型的值,同时保持每个值的类型信息。这使得std::tuple成为了执行多任务返回值、聚合不同类型数据以及实现类型相关算法的理想选择。

4.1 std::tuple的基本概念
std::tuple<T1, T2, ..., TN>可以看作是一个异构的固定大小容器,它可以包含任意数目(N)的不同类型(T1, T2, …, TN)的元素。std::tuple对于打包数据和从函数返回多个值非常有用。

4.2 使用场景

  • 函数需要返回多个值时
  • 将一组不同类型的数据作为单个单位处理时
  • 用于实现编译时计算和元编程技术

4.3 基本用法

#include <tuple>
#include <string>
#include <iostream>

std::tuple<int, std::string, float> createComplexObject() {
    return std::make_tuple(42, "Test", 3.14f);
}

int main() {
    auto [id, name, value] = createComplexObject(); // 结构化绑定(C++17特性)

    std::cout << "ID: " << id << std::endl;
    std::cout << "Name: " << name << std::endl;
    std::cout << "Value: " << value << std::endl;

    // 也可以使用std::get访问tuple中的元素
    std::tuple<int, double, std::string> t = std::make_tuple(1, 2.0, "tuple");
    std::cout << "First element: " << std::get<0>(t) << std::endl;
    std::cout << "Second element: " << std::get<1>(t) << std::endl;

    return 0;
}

总结:

  1. std::optional:常常用于代替nullptr实现空安全(用来包装非指针类型)或其他可能为空的场景。
  2. std::any:STL中少有的不需要指定容器内容类型的模板类(但是在使用时需要传入容器内容类型以获取内容),常用于类型擦除。相当于C++版本的void*。
  3. std::variant:C++版本的“联合体”。提供了类型安全的联合功能。
  4. std::tuple:std::pair 的增强版,支持任意数量的异构元素存储。

如果你的编译器不支持C++17,那么可以了解Boost 库提供的 Boost.Variant 和 Boost.Any 类型。它们提供了类似的功能,在旧代码或不支持 C++17 的环境中,可以考虑采用作为代替。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity3D Text使用超链接跳转事件

系列文章目录 Unity工具 文章目录 系列文章目录&#x1f449;前言&#x1f449;一、第一种使用TextMeshPro加入超链接&#x1f449;二、继承Text组件,重载OnPopulateMesh方法&#x1f449;三.壁纸分享&#x1f449;总结 &#x1f449;前言 有时候会用到跳转的问题,所以添加一…

Flutter第十五弹 Flutter插件

目标&#xff1a; 1.Flutter插件是什么&#xff1f;有什么作用&#xff1f; 2.怎么创建Flutter插件&#xff1f; 一、什么是插件 在flutter中&#xff0c;一个插件叫做一个package&#xff0c;使用packages的目的就是为了达到模块化&#xff0c;可以创建出可被复用和共享的代…

关于PX4模拟机型的拓展

#多旋翼 #四旋翼&#xff08;默认&#xff09; sudo make px4_sitl gazebo #带光流的四旋翼 sudo make px4_sitl gazebo_iris_opt_flow #3DR Solo&#xff08;四旋翼&#xff09; sudo make px4_sitl gazebo_solo #Typhoon H480&#xff08;六旋翼&#xff09; sudo make px4_s…

超声波清洗机对眼镜有伤害吗?四大顶尖优品公认力作!

超声波清洗机利用超声波在液体中产生的微小气泡爆炸&#xff0c;产生强大的冲击力&#xff0c;能够深入物品的各个角落&#xff0c;有效去除油污、灰尘和细菌。与传统的手工清洗相比&#xff0c;不仅清洁效率高&#xff0c;而且能够保护眼镜不受损伤&#xff0c;特别适合清洗眼…

海纳斯 hinas 的hi3798mv100 华为悦盒 6108v9 安装wifi模块

hi3798mv100安装wifi模块 1.执行脚本 &#xff0c;执行完毕后重启服务器2. 继续执行脚本3.检查网卡驱动安装是否正确4.查看网卡安装状态5.连接wifi结尾 1.执行脚本 &#xff0c;执行完毕后重启服务器 bash <(curl -sSL https://gitee.com/xjxjin/scripts/raw/main/install_…

7km远距离WiFi实时图传模块,无人机海上无线传输方案,飞睿智能WiFi MESH自组网技术

在浩瀚无垠的海洋上&#xff0c;无人机正在开启一场前所未有的技术创新。它们不再只是天空的舞者&#xff0c;更是海洋的守望者&#xff0c;为我们带来前所未有的视野和数据。而这一切的背后&#xff0c;都离不开一项创新性的技术——飞睿智能远距离WiFi实时图传模块与无线Mesh…

链式队列算法库构建

学习贺利坚老师课程,构建链式队列算法库 数据结构之自建算法库——链队&#xff08;链式队列&#xff09;_数据结构函数链队列的算法框架有哪些-CSDN博客文章浏览阅读6.2k次&#xff0c;点赞3次&#xff0c;收藏9次。本文针对数据结构基础系列网络课程(3)&#xff1a;栈和队列…

机器学习--概念理解

知识点 一、机器学习概述 人工智能 机器学习 深度学习 学习的范围&#xff1a;模式识别、数据挖掘、统计学习、计算机视觉、语音识别、自然语言处理 可以解决的问题&#xff1a;给定数据的预测问题 二、机器学习的类型 监督学习 分类 回归 无监督学习 聚类 降维 强化…

iOS项目开发遇到问题杂项坑点记录

ios17 弹窗UIAlertController展示逻辑变化&#xff0c;单个词一行展示不下不换行&#xff08;这前版本会换行&#xff09;&#xff0c;直接截断超出部分。 UINavigationController push立刻pop会异常&#xff0c;使用用setViewCollerllers可以避免这个问题 键盘切换后立刻切页…

【React Native】measureInWindow在安卓上无法正确获取View在屏幕上的布局信息

问题描述&#xff1a; 在React Native中&#xff0c;我们可以使用measureInWindow的方式去获取一个View在屏幕中的位置信息&#xff1a; 下面这个Demo中&#xff0c;我们写了一个页面HomePage和一个列表项组件ListItemA&#xff0c;我们期望每过5s监测一次列表中每一项在屏幕中…

通过搭建 24 点小游戏应用实战,带你了解 AppBuilder 的技术原理

本文将通过一个 24 点小游戏的案例&#xff0c;详细介绍百度智能云千帆 AppBuilder 的基本技术原理和使用方法&#xff0c;帮助读者快速掌握 AI 原生应用的开发流程。 1 三步构建 AI 原生应用方法论 AI 原生应用与传统应用的最大区别是交互形态彻底的拟人化&#xff0c;通过文…

【Linux学习十八】网站管理:防火墙介绍、静态站点、动态站点、域名

1.Apache Apache官网: www.apache.org 软件包名称: httpd 服务端口:80/tcp(http) 443/tcp(https) 配置文件: /etc/httpd/conf/httpd.conf 子配置文件:/etc/httpd/conf.d/*.conf 查看被占用的端口号 netstat -tuln | grep <端口号> 解哪个程序正在使用端口 80&#xff0…

vue封装原生table表格方法

适用场景&#xff1a;有若干个表格&#xff0c;前面几列格式不一致&#xff0c;但是后面几列格式皆为占一个单元格&#xff0c;所以需要封装表格&#xff0c;表格元素自动根据数据结构生成即可&#xff1b;并且用户可新增列数据。 分类&#xff1a; 固定数据部分 就是根据数据…

docker配置redis主从复制

下载redis,复制redis.conf 主节点(6379) 修改redis.conf # bind 127.0.0.1 # 注释掉这里 protected-mode no # 改为no port 6379从节点(6380) 修改redis.conf bind 127.0.0.1 protected-mode no # 改为no port 6380 replicaof 172.17.0.2 6379 # 这里的ip为主节点容器的i…

SpringSecutrity原理

一、基于RBAC实现的权限管理通常需要涉及以下几张表&#xff1a; 1. 用户表&#xff08;user&#xff09;&#xff1a;记录系统中的所有用户&#xff0c;包括用户ID、用户名、密码等信息。 2. 角色表&#xff08;role&#xff09;&#xff1a;记录系统中的所有角色&#xff0…

【项目管理体系】代码评审规范

1完整性检查 2一致性检查 3正确性检查 4可预测性检查 5健壮性检查 6结构性检查 7可追溯性检查 8可理解性检查 9可验证性检查 软件开发全套资料获取&#xff1a;&#xff08;本文末个人名片直接获取&#xff09; 软件产品&#xff0c;特别是行业解决方案软件产品不同于一般的商品…

汽车EDI: BMW EDI项目案例

宝马集团是全世界成功的汽车和摩托车制造商之一&#xff0c;旗下拥有BMW、MINI和Rolls-Royce三大品牌&#xff1b;同时提供汽车金融和高档出行服务。作为一家全球性公司&#xff0c;宝马集团在14个国家拥有31家生产和组装厂&#xff0c;销售网络遍及140多个国家和地区。 本文主…

在Linux Ubuntu系统中使用Pascal语言

Pascal是一种结构化编程语言&#xff0c;而Free Pascal作为其现代编译器&#xff0c;不仅支持跨多种操作系统和处理器架构&#xff0c;还提供了高效的内存使用和函数重载等先进功能。Free Pascal继承了Pascal语言的核心特性&#xff0c;同时进行了扩展和优化&#xff0c;使其成…

【算法】单调队列 - 基础与应用-滑动窗口最大值

题目 给定一个数组 nums&#xff0c;有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回滑动窗口中的最大值。 思路 暴力&#xff1a;遍历一遍的过程中每次从窗口找到最大的数组&#…

Springboot 项目启动时扫描所有枚举并存入缓存(redis)

为什么这么做? 为了springboot 注解属性转换字典方便一点(使用缓存的方式在Springboot 启动时获取字典数据) 在启动时会扫描com.vehicle.manager.core.enumerate包下的所有枚举(包括类中的内部枚举),并取出对应属性以json的方式存入redis 目录结构如下: RedisUtil可以在Red…