一文速通C++17 std::variant

std::variant

使用场景

在C++17标准出现之前,有一些神奇的术语,像"discriminated union",“type-safe union"和"sum type”。后来它们都意味着相同的类型:“variant”

std::variant 使用场景

  • 所有可能为单个变量获得几种类型的地方:
    1. 解析命令行
    2. 解析ini文件
    3. 语言解析器
    4. 求解方程的根(有效地表达计算的几种可能结果)
  • 错误处理:
    • 例如,您可以返回variant<Object, ErrorCode>,如果返回值是有效的,则返回Object,否则分配一些错误码(C++23可以使用std::expected)。
    • 实际上 optional也是可以的
  • 状态机
  • 不使用虚表和继承实现的多态(visiting pattern)

std::variant<>是一个可变参数类模板(C++11引入的处理任意数量参数的特性)。引入它的同时,还定义了下面的类型和对象:

  • 类模板 std::variant_size
  • 类模板 std::variant_alternative
  • 值 std::variant_npos
  • 类型 std::monostate
  • 异常类 std::bad_variant_access,派生自 std::exception

对比union

头铁 手动管理union

如果你头铁,真的想手动管理union:

#include <variant>
#include <iostream>
#include <string>
#include <vector>union gUnion
{std::string str;std::vector<int> vec;~gUnion() { } // what to delete here?
};int main()
{// 初始化 Union 为一个其中一个复杂类型// - 此时,将 Union 视作其它类型(例如调用 Union.vec)是未定义行为gUnion unionObj = {"Hello, world"};std::cout << "unionObj.str = " << unionObj.str << '\n';// - 不更改激活类型的话,倒是可以连续使用unionObj.str = "OK, world";std::cout << "unionObj.str = " << unionObj.str << '\n';// 现在,把 Union 更改为其它类型// - 调用原类型对应成员的析构函数destructorunionObj.str.~basic_string<char>();// - 调用新类型的constructornew (&unionObj.vec) std::vector<int>;// 现在,新的类型视为被激活// now, s.vec is the active member of the unionunionObj.vec = {1, 2, 3, 4};std::cout << " vector 大小 = " << unionObj.vec.size() << '\n';for (auto mInt : unionObj.vec) {std::cout << mInt << ' ';}// - 使用结束后,记得调用此时类型的析构函数unionObj.vec.~vector<int>();return 0;
}
union 缺点

union 处理一些底层的逻辑,并且只使用基本类型

缺陷

  1. 无法获知当前使用的是哪个类型
  2. 它不会主动调用underlying types的析构函数

缺陷体现在使用上:需要程序员进行大量的维护存储。在切换到新的存储对象之前,程序员必须:

  • 知道(/记住,因为没办法查询)当前使用的是那种类型

  • 手动调用其析构函数/构造函数。

因此不推荐将 union 用于类型双关,union存储类型主要还是用于基本类型,不会看到“高级”类型,例如vector、string或者其他的容器等。

variant 优点

variant 解决了union的两个缺点:

  1. 能清楚的知道当前激活的类型——可以保证类型安全,不允许获取未激活类型的值
  2. 对复杂类型的生命周期的支持:
  • 如果切换类型,会调用对应的析构函数。
  • variant会自动调用非平凡类型的析构函数和构造函数。

体现在使用上:可以使用复杂类型,但不能保存引用、数组和void类型。

variant 内存大小/性能

内存大小:variant所占的内存大小等于所有可能的底层类型中最大的再加上一个记录当前选项的固定内存开销

  • 遵守对齐规则

  • 多出来的内存:用于保证类型安全

    • 类型安全–不允许获取未激活类型的值
    • 例如,需要知道当前variant中激活的类型
  • 多分配的内存在栈上。variant不会要求额外的堆内存分配。

    会创建在自己独立的内存空间内存储有当前选项的值的新对象。

运行时性能:拷贝 std::variant<>的开销要比拷贝当前选项的开销稍微大一点,因为 variant必须找出要拷贝哪个值。

variant对象是值语义,即拷贝被实现为深拷贝。

派生类

variant构建方式

避免报错 monostate

variant没有空的状态。每一个构造好的variant对象,至少调用了一次构造函数。

  • variant默认构造函数会调用第一个选项类型的默认构造函数 进行初始化

  • 若第一个类型没有 默认构造函数 ,初始化失败 导致编译期错误。

// Iris_param类没有默认构造函数,报错
std::variant<Iris_param, std::string> iris_variant;
// pass
std::variant<std::monostate, Iris_param, std::string> iris_variant;

推荐做法:将std::monostate作为variant的第一个类型来使用。

  • std::monostate是 C++标准库提供了一个特殊的辅助类,以防止variant中的第一个类型没有默认构造函数
  • variant 处于这个选项时表示此 variant没有其他任何类型的值。
  • 比较 std::monostate类型的对象,结果总是相等
指定类型
通过自动推导

如果仅通过编译器帮助 variant 进行匹配(可能存在歧义)

// 自动推导类型
std::variant<std::monostate, int, std::string> iris_variant {"some string"};
指定类型
通过in_place_index指定

指定具体的类型来初始化variant

// 避免被无认为是int
std::variant<long, float, std::string> longFloatString { std::in_place_index<1>, 7 }; 
// 复杂的构造函数传参
std::variant<std::vector<int>, std::string> vecStr { std::in_place_index<0>, { 0, 1, 2, 3 }};

为了避免存在类型匹配的歧义 ambiguity

  • 使用std::in_place_index来显式指定要匹配的类型。

  • 同时,std::in_place_index也允许创建复杂的类型并将参数传递给该类型对应的构造函数。(类似 std::optional使用的 std::in_place_t)

通过in_place_type指定

std::in_place_type指明类型

std::variant<std::vector<int>, std::string> vecStr { std::in_place_type<std::vector<int>>, { 0, 1, 2, 3 }};
variant自我包含

骚操作:

struct VariantObject;using VariantList = std::vector<VariantObject>;
using VariantDict = std::unordered_map<std::string, VariantObject>;struct VariantObject {std::variant<std::monostate, int, VariantList> inner1;std::variant<std::monostate, int, VariantDict> inner2;std::variant<std::nullptr_t, int, VariantDict, VariantList> inner3;
}

操作

当前类型
操作符效果
index()返回当前选项的索引
holds_alternative<T>()返回是否持有类型 T的值
获取 index()

运行时,获取当前使用的类型的索引(第一项的索引是 0)

std::variant<int, float, std::string> intFloatString { "Hello" };// 2
std::cout << intFloatString.index() << "\n";
  • std::variant.index()
  • 通过返回的 int index,得知当前什么类型处于激活状态。
检查 std::holds_alternative

运行时,检查当前使用的类型

if (std::holds_alternative<std::monostate>(iris))std::cout << "the variant holds a std::monostate\n";
else if (std::holds_alternative<float>(intFloatString))std::cout << "the variant holds a float\n";
else if (std::holds_alternative<std::string>(intFloatString))std::cout << "the variant holds a string\n";  // 在C++20中 可以在常量表达式中使用std::variant。
constexpr auto dv = foo(1);
static_assert(std::holds_alternative<double>(dv));
访问值
操作符效果
=赋新值
get<T>()返回类型为 T的选项的值
get<Idx>()返回索引为 Idx的选项的值
get_if<T>() 返回指向类型为 T的选项的指针或 nullptr
get_if<Idx>()返回指向索引为 Idx的选项的指针或 nullpr
无法直接访问
std::variant<int, float, std::string> intFloatString { "Hello" };
// 即使已经明确知道当前激活类型是什么,也无法直接访问
std::string s = intFloatString; 
// error: conversion from 
// 'std::variant<int, float, std::string>' 
// to non-scalar type 'std::string' requested

访问varient内部的值,需要使用如下方法:

激活状态(可以传递类型或索引)

类型安全–不允许获取未激活类型的值

std::getstd::get_if
类型 值std::get<float>(intFloatString)类型index 值引用std::get_if<1>(&intFloatString)
激活状态返回引用返回指针
非激活状态异常std::bad_variant_access返回**nullptr**
std::get

使用 std::get<Type|Index>(std::variant),返回 引用

所需类型状态返回值
激活状态返回对应类型的引用
非激活状态抛出std::bad_variant_access异常
// 激活状态 int 
std::variant<int, float, std::string> intFloatString { std::in_place_index<0>, 2};
try 
{auto f1 = std::get<float>(intFloatString);  // 通过类型访问std::cout << "float! " << f1 << "\n";auto f2 = std::get<1>(intFloatString);  // 通过索引访问std::cout << "float! " << f2 << "\n";
}
catch (std::bad_variant_access&)  // 当索引/类型错误时进行处 理
{std::cout << "our variant doesn't hold float at this moment...\n";
}
std::get_if

函数std::get_if<index>(&ref)

在当前选项为指定类型时返回一个指向当前选项的指针,否则返回 nullptr。

所需类型状态返回值
激活状态返回对应类型的指针
非激活状态返回nullptr(不会引发异常)

注意:std::get返回对variant的引用std::get_if返回对variant的指针

// 激活状态 int 
std::variant<int, float, std::string> intFloatString { std::in_place_index<0>, 2};if (const auto intPtr = std::get_if<0>(&intFloatString)) std::cout << "int!" << *intPtr << "\n";
std::visitor

实践中,访问variant中的值的最重要方法是使用访问者来实现。(见后文)

修改
修改值
操作符效果
=赋新值
get<T>()返回类型为 T的选项的值
get<Idx>()返回索引为 Idx的选项的值
get_if<T>() 返回指向类型为 T的选项的指针或 nullptr
get_if<Idx>()返回指向索引为 Idx的选项的指针或 nullpr
operator=

赋值操作符虽然不能访问varient,但是可以修改varient

// 激活状态是 string
std::variant<int, float, std::string> intFloatString { "Hello" };
// 赋值操作符
// - 类型 修改为 int, string的析构函数被调用
intFloatString = 10; // we're now an int
std::get std::get_if

std::get 获得引用,std::get_if获得指针。

然后通过引用/指针给当前激活的类型赋新值

// 实际上,这让我对 对象生存期 以及 内存分布 产生了困惑。
// 各个类型的引用似乎可以同时存在
std::variant<int, float, std::string> intFloatString;
intFloatString = 1;
auto mRef_int = std::get<int>(intFloatString);
intFloatString = "Hello ";
std::get<std::string>(intFloatString) += std::string(" World");
auto mRef_str = std::get<std::string>(intFloatString);
std::cout << mRef_str << std::endl; // Hello  World
std::cout << mRef_int << std::endl;	// 1if (auto pFloat = std::get_if<float>(&intFloatString); pFloat)*pFloat *= 2.0f;
std::visitor访问器
修改类型/值

operator=将会直接赋予 variant一个新值,只要有和新值类型对应的选项。

emplace()在赋予新值之前会先销毁旧的值。

emplace<index/T> 修改类型/值

成员函数效果
emplace<T>()销毁旧值并赋一个 T类型选项的新值
emplace<Idx>()销毁旧值并赋一个索引为 Idx的选项的新值

std::variant.emplace<>{}

intFloatString.emplace<1>(float(10.22));
auto mRef_float = std::get<float>(intFloatString);
std::cout << " after emplace index1-float " << mRef_float << std::endl;

访问器 std::visitor

stl 函数std::visit。可以在所有的variant参数上调用给定的“访问者”。

std::visit 会 “接受每个variant中所有可能的类型的可调用对象”,可调用对象必须对所有variant中所有可能的类型对进行重载。当这些对象“访问”一个 variant时,它们会调用和当前选项类型最匹配的函数。

template <class Visitor, class... Variants>  
constexpr visit(Visitor&& vis, Variants&&... vars);

一般来说

  • 静态多态:泛型lambda简单粗暴的执行统一的操作。

  • 动态多态:仿函数里面重载多个operator ()

静态多态(泛型lambda)

使用visitor,若采用 泛型 lambda。可以实现静态多态 —— 对variant各个类型执行相同的逻辑

值得注意的是:传入 lambda的参数不是 variant变量,而是 variant变量中的变量(至于是 值 引用 右值引用 常量引用… 要看具体情况)。 因此无法使用variant 的判断类型的方法 variant.index()std::holds_alternative

对于一个普通的变量,因为类型萃取发生在编译期,所以仅仅是静态多态。

通用的操作符

通过variant中的 所有类型 都支持的运算符、函数,来使用传入的变量。

// a generic lambda:
// 由于variant中的所有类型都支持<<,可以成功打印
auto PrintVisitor = [](const auto& t) {std::cout << t << "\n";
};std::variant<int, float, std::string> intFloatString { "Hello" };std::visit(PrintVisitor, intFloatString);
// 当然,若不加const,visitor可以更改variant当前的值
auto TwiceMoreVisitor = [](auto& t) {t*= 2;
};std::variant<int, float> intFloat { 20.4f };std::visit(TwiceMoreVisitor, intFloat); // 修改为40.8
std::visit(PrintVisitor, intFloat);		// 打印40.8
类型萃取 + 编译期 if

通过类型萃取 + 可以使用编译期 if语句来对不同的选项类型进行不同的处理。可以根据variant中的类型来指向不同的行为。

值得注意的是:类型萃取发生在 编译期,属于静态多态。

std::variant<std::monostate, int, float, std::string> intFloatStr {std::in_place_index<2>, 20.4f
};auto TypeTraitVisitor = [](auto& arg) {using T = std::decay_t<decltype(arg)>;if (std::is_same_v<T, int>) {std::cout << "int\n";// 使用部分类型特有的 函数/运算符 需要 if constexpr} else if constexpr (std::is_same_v<T, float>) {std::cout << "float\n";arg *= 2;} else {std::cout << "other than int/float type\n";}
};std::visit(TypeTraitVisitor, intFloatStr);
动态多态(仿函数)

lambda 可以实现 执行相同的逻辑/静态多态。但在大多数情况下,我们希望根据激活类型的不同,来执行一些不同的相应操作。并且是运行时的动态多态。

在仿函数里面重载多个operator (),包含每个variant中所有可能的类型。仿函数/仿函数对象都是ok的

符合 “接受每个variant中所有可能的类型的可调用对象” 的定义

  • 允许variant存在 std::monostate,仿函数包含对应的重载operator()即可。

  • 可以包含多余的operator ()重载(该variant没有的类型)。

struct MultiplyVisitor
{float mFactor = 9.9;MultiplyVisitor(float factor) : mFactor(factor) { }MultiplyVisitor() {}void operator()(std::monostate t) const {std::cout << "std::monostate type, mFactor = " << mFactor << std::endl;}void operator()(std::string& ) const {std::cout << "string called , mFactor = " << mFactor  << std::endl;}void operator()(int& i) const {std::cout << "int called , mFactor = " << mFactor  << std::endl;i *= static_cast<int>(mFactor);}void operator()(float& f) const {std::cout << "float called , mFactor = " << mFactor << std::endl;f *= mFactor;}
};
std::variant<std::monostate, int, float, std::string> intFloatStr {std::in_place_index<2>, 20.4f
};// 重载 operator() 的仿函数
std::visit(MultiplyVisitor(), intFloatStr);
// 只要仿函数重载了对应的operator(),monostate也是允许的
intFloatStr = std::monostate();
std::visit(MultiplyVisitor(0.5f), intFloatStr);// 也可以是仿函数对象
intFloatStr = "string sth";
MultiplyVisitor mVisitor = MultiplyVisitor(99.0f);std::visit(mVisitor, intFloatStr);

异常造成的无值

赋新值给variant时发生异常,variant可能会进入特殊的状态:它已经失去了旧的值但还没有获得新的值。

struct S {operator int() { throw "EXCEPTION"; } // 转换为int时会抛出异常
};
std::variant<double, int> var{12.2}; 
// OOPS:当设为int时抛出异常
var.emplace <1>(S{});

如果这种情况发生了,一下函数可以表示 variant当前没有值 的状态:

  • var.valueless_by_exception() 返回 true
  • var.index() 返回 std::variant_npos

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

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

相关文章

【汇编语言】第一个程序(四)—— 谁在幕后启动程序 : 探讨可执行文件的装载与执行

文章目录 前言1. 可执行文件的加载与运行1.1 DOS中的程序加载过程1.2 问题1&#xff1a;谁加载了1.exe&#xff1f;1.3 问题2&#xff1a;程序运行结束后的返回过程1.4 操作系统的外壳1.5 回答问题1和问题21.6 汇编程序执行的完整历程 2. 使用Debug加载与跟踪1.exe2.1 Debug的加…

Unreal Engine 5 C++(C#)开发:使用蓝图库实现插件(一)认识和了解Build.cs

目录 引言 一、创建一个C插件TextureReader插件 二、Build.cs文件 三、ModuleRules 四、TextureReader插件的构造 4.1ReadOnlyTargetRules的作用 4.2TextureReaderd的构造调用 4.3设置当前类的预编译头文件的使用模式 4.4PublicIncludePaths.AddRange与PrivateInclude…

SELS-SSL/TLS

一、了解公钥加密&#xff08;非对称加密&#xff09; 非对称加密中&#xff0c;用于加密数据的密钥与用于解密数据的密钥不同。私钥仅所有者知晓&#xff0c;而公钥则可自由分发。发送方使用接收方的公钥对数据进行加密&#xff0c;数据仅能使用相应的私钥进行解密。 你可以将…

STM32FreeRTOS 使用QSPI驱动nandFlash

STM32FreeRTOS 使用QSPI驱动nandFlash 不清楚为什么STM32同时打开3个以上的音频文件时会出现播放问题&#xff0c;所以更换方案。因为SRAM的内存空间过小&#xff0c;用于存储音频文件不适合&#xff0c;所以使用大小为128MByte的nandFlash。 nandFlash使用华邦的W25N01GVZEI…

vscode的一些使用心得

问题1&#xff1a;/home目录空间有限 连接wsl或者remote的时候&#xff0c;会在另一端下载一个.vscode-server&#xff0c;vscode的插件都会安装进去&#xff0c;导致空间增加很多&#xff0c;可以选择更换这个文件的位置 参考&#xff1a;https://blog.csdn.net/weixin_4389…

1Panel应用商店开源软件累计下载突破200万次!

2024年10月23日&#xff0c;1Panel应用商店内开源软件累计下载突破200万次。 1Panel&#xff08;github.com/1Panel-dev/1Panel&#xff09;是一款现代化、开源的Linux服务器运维管理面板&#xff0c;它致力于通过开源的方式&#xff0c;帮助用户简化建站与运维管理流程。 为…

基于MATLAB多参数结合火焰识别系统

一、课题介绍 本设计为基于MATLAB的火焰烟雾火灾检测系统。传统的采用颜色的方法&#xff0c;误识别大&#xff0c;局限性强。结合火焰是实时动态跳跃的&#xff0c;采用面积增长率&#xff0c;角点和圆形度三个维度相结合的方式判断是否有火焰。该设计测试对象为视频&#xf…

利用摄像机实时接入分析平台LiteAIServer视频智能分析软件进行视频监控:过亮过暗检测算法详解

视频监控作为一种重要的安全和管理工具&#xff0c;广泛应用于各个领域&#xff0c;如安全监控、交通监管、员工监管、公共场所监控等。然而&#xff0c;在实际应用中&#xff0c;视频监控系统经常面临各种挑战&#xff0c;其中之一便是视频画面过亮或过暗的问题。过亮过暗检测…

python画图|坐标轴比例设置方法

【1】引言 在前序学习进程中&#xff0c;我们通过ax.set_box_aspect()函数掌握了坐标轴等比例设置方法。 担当我在回顾以前的学习文章时&#xff0c;发现ax.axis()函数也可以设置坐标轴比例&#xff0c;比如下述文章&#xff0c;文章可通过点击链接直达&#xff1a; python画…

[前端][基础]JavaScript

1&#xff0c;JavaScript简介 JavaScript 是一门跨平台、面向对象的脚本语言&#xff0c;而Java语言也是跨平台的、面向对象的语言&#xff0c;只不过Java是编译语言&#xff0c;是需要编译成字节码文件才能运行的&#xff1b;JavaScript是脚本语言&#xff0c;不需要编译&…

用于文档理解的局部特征

本文介绍了一种名为DocFormerv2的多模态Transformer模型&#xff0c;它专为视觉文档理解&#xff08;VDU&#xff09;而设计。该模型可以处理视觉、语言和空间特征&#xff0c;利用编码器-解码器架构&#xff0c;并通过不对称地使用新颖的无监督任务进行预训练&#xff0c;以促…

Chromium127编译指南 Linux篇 - 额外环境配置(五)

引言 在成功获取 Chromium 源代码后&#xff0c;接下来我们需要配置适当的编译环境&#xff0c;以便顺利完成开发工作。本文将详细介绍如何设置 Python 和相关的开发工具&#xff0c;以确保编译过程无碍进行。这些配置步骤是开发 Chromium 的必要准备&#xff0c;确保环境设置…

HTTP相关返回值异常原因分析,第二部分

今天我们讲讲HTTP相关返回值异常如何解决&#xff08;实例持续更新中&#xff09; 一、4xx状态码 这些状态码表示请求有问题&#xff0c;通常是由于客户端的错误引起的。 1.1 400 Bad Request: 请求格式不正确&#xff0c;服务器无法理解。 状态码400的含义&#xff1a; …

.NET内网实战:通过白名单文件反序列化漏洞绕过UAC

01阅读须知 此文所节选自小报童《.NET 内网实战攻防》专栏&#xff0c;主要内容有.NET在各个内网渗透阶段与Windows系统交互的方式和技巧&#xff0c;对内网和后渗透感兴趣的朋友们可以订阅该电子报刊&#xff0c;解锁更多的报刊内容。 02基本介绍 03原理分析 在渗透测试和红…

Spring Boot 实现文件分片上传和下载

文章目录 一、原理分析1.1 文件分片1.2 断点续传和断点下载1.2 文件分片下载的 HTTP 参数 二、文件上传功能实现2.1 客户端(前端)2.2 服务端 三、文件下载功能实现3.1 客户端(前端)3.2 服务端 四、功能测试4.1 文件上传功能测试4.2 文件下载功能实现 参考资料 完整案例代码&…

【数据结构】-数组

数组 特点&#xff1a; 数组的地址连续&#xff0c;可以通过下标获取数据。 1. 数组扩容 步骤&#xff1a; $1. 创建一个比原来数组更长的新数组 $2. 让原来数组当中的数据依次复制到新数组当中 $3. 让arr指向新数组&#xff0c;原数组空间释放 2. 数组插入 2.1 最后位置…

智慧小区:科技之光点亮幸福家园

智慧社区的未来发展方向与趋势 从智能化管理到便捷化服务&#xff0c;从环保节能到安全监控&#xff0c;智慧社区正以其前瞻性的视野和创新性的技术&#xff0c;引领着未来城市生活的新方向。从智慧社区的基本概念中通过运用现代科技手段&#xff0c;如物联网、云计算、大数据…

0,国产FPGA(紫光同创)-新建PDS工程

国产FPGA正在蓬勃发展&#xff0c;紫光同创FPGA是大家竞赛时经常遇到的一款国产FPGA&#xff0c;本专栏从IP核开始一直到后续图像处理等。 开发板&#xff1a;盘古50K标准板 1&#xff0c;新建PDS工程 点击File&#xff08;1&#xff09;&#xff0c;然后是New Projects&#…

深入解析Sysmon日志:增强网络安全与威胁应对的关键一环

不断演进的网络安全领域中&#xff0c;保持对威胁的及时了解至关重要。Sysmon日志在这方面发挥了至关重要的作用&#xff0c;通过提供有价值的见解&#xff0c;使组织能够加强其安全姿态。Windows在企业环境中是主导的操作系统&#xff0c;因此深入了解Windows事件日志、它们的…