深入解析 C++17 中的std::variant与std::visit:从原理到实践

引言

什么是std::variant

在 C++17 之前,如果你想在一个变量中存储多种可能的类型,通常会使用 union 或 void* 指针。然而,这些方法都有明显的缺点。

  • 使用 union 时,类型信息会丢失,使得代码容易出错。
  •  void* 指针则需要手动进行类型转换和内存管理,容易导致内存泄漏或未定义的行为。

std::variant(变体)作为一种更安全、更方便的多类型容器,应运而生。你可以把它看作是一个可以存储多种类型中的任一种的类型安全的容器

如下所示

#include <variant>
#include <iostream>int main() {std::variant<int, double, std::string> v1 = 42;std::variant<int, double, std::string> v2 = 3.14;std::variant<int, double, std::string> v3 = "hello";// 访问存储的值(不安全,需确保类型正确)std::cout << std::get<int>(v1) << std::endl;// 安全地访问存储的值if (auto pval = std::get_if<int>(&v1)) {std::cout << *pval << std::endl;}return 0;
}

与 union 和 void* 的比较

unionvoid*std::variant
类型安全
自动内存管理
运行时类型信息
性能⚖️⚖️⚖️
代码可读性

std::variant 的局限性

尽管 std::variant 非常强大,但它并不是万能的。它的一个主要限制是,虽然它可以存储多种类型,但在任何给定时间点,它只能存储其中一种

类型检查

当你拿到一个 std::variant 对象时,如何知道它当前存储了哪种类型的值?

在 C++ 这样的静态类型(Static Typing)语言中,类型信息在编译时就已经确定。然而,当你使用 std::variant(变体)时,你实际上是在模拟动态类型(Dynamic Typing)的行为。这意味着你需要在运行时去判断它究竟存储了哪种类型的对象。

手动类型检查

C++ 提供了 std::holds_alternative 和 std::get 等函数,用于检查和提取 std::variant 中存储的类型,或者更糟糕的是,使用 std::get_if。这种做法虽然有效,但是很容易出错。

std::variant<int, double, std::string> v = 42;
if (std::holds_alternative<int>(v)) {int value = std::get<int>(v);  // 安全
} else if (std::holds_alternative<double>(v)) {double value = std::get<double>(v);  // 运行时错误!
}

如果你不小心用了错误的类型去访问 std::variant,会抛出一个 std::bad_variant_access 异常。这种情况下,你不得不依赖运行时错误检查,这无疑增加了代码的复杂性。

方法优点缺点
std::holds_alternative简单、直观不能提取值
std::get可以直接提取值类型错误会抛出异常
std::get_if可以检查和提取值,不会抛出异常返回指针,需要额外的空指针检查

什么是std::visit

当你使用 std::variant 时,一个自然而然的问题是如何处理存储在其中的不同类型的值。手动检查和处理多种可能的类型通常很繁琐,而且容易出错。这就是 std::visit 发挥作用的地方。

std::visit 提供了一种机制,让你能够方便、优雅地处理 std::variant 中存储的多种可能的类型。它基于访问者模式(Visitor Pattern),是一种运行时多态的实现。

基本接口

std::visit 的基本接口如下:

template<class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variants&&... vars);
  • Visitor:一个可调用对象,它应该能够接受 Variants 中每种类型的值。它通常是一个重载了 operator() 的结构或类。
  • Variants:一个或多个 std::variant 类型的对象。

std::visit 的工作原理

std::visit 的底层原理涉及几个关键概念,包括类型擦除、类型恢复和函数重载解析。这是一个相对复杂的机制,尤其是在涉及模板和变参模板时。以下是 std::visit 的底层工作原理的概述:

  1. 类型擦除std::variant 是一个类型擦除容器,它可以存储一定范围内的不同类型的对象。它内部通常有一个联合体来存储数据和一个标记来表示当前存储的类型。
  2. 访问存储的值:当 std::visit 被调用时,它首先需要确定 std::variant 当前存储的具体类型。这是通过检查内部的类型标记完成的。
  3. 函数模板实例化std::visit 接受一个可调用对象和一个或多个 std::variant 对象。这个可调用对象通常是一个重载的函数对象或 lambda 表达式,其具有多个重载以处理不同的类型。编译器会为这些重载生成函数模板实例。
  4. 类型恢复和函数调用:一旦确定了 std::variant 中的类型,std::visit 通过生成的模板代码来“恢复”此类型,并调用与该类型匹配的函数重载。如果有多个 std::variant 参数,std::visit 将处理所有组合的可能性,并调用适当的重载。
  5. 编译时多态:这一切都在编译时发生。编译器生成适用于所有可能的类型组合的代码。因此,std::visit 实现了一种编译时的多态,而不是运行时多态(如虚函数)。
  6. 效率和优化:由于大部分工作在编译时完成,std::visit 通常比运行时类型检查(如动态类型转换)更高效。编译器可以优化函数调用,尤其是在可预测的分支和内联函数的情况下。

综上所述,std::visit 的核心在于它能够在编译时处理多态性,允许编译器生成处理 std::variant 中所有可能类型的代码。这种方法确保了类型安全,并允许进行高效的代码优化。

简单使用

让我们先来看一个简单的例子,这将帮助你更好地理解 std::variant 和 std::visit 的基本用法。

#include <iostream>
#include <variant>
#include <string>int main() {std::variant<int, double, std::string> myVariant = "Hello, world!";std::visit([](auto&& arg) {std::cout << "The value is: " << arg << std::endl;}, myVariant);return 0;
}

在这个例子中,myVariant 可以存储 intdouble 或 std::string 类型的值。我们使用 std::visit 来访问存储在 myVariant 中的值,并输出它。

这里,std::visit 接受了一个 lambda 表达式作为参数,这个 lambda 表达式可以接受任何类型的参数(由 auto&& 指定),然后输出这个参数。

如何优雅地使用 std::visit

使用泛型 lambda 表达式

std::visit 允许你传入一个可调用对象(callable object),通常是一个 lambda 表达式。现代 C++ 提供了一种特殊的 lambda 表达式,称为泛型 lambda 表达式(generic lambda)。

泛型 lambda 是一个使用 auto 关键字作为参数类型的 lambda 表达式。这意味着 lambda 可以接受任何类型的参数,并在函数体内进行处理。

auto generic_lambda = [](auto x) {// do something with x
};

这种灵活性在处理 std::variant 时尤为有用,因为你可能需要根据多种可能的类型来编写逻辑。

使用 if constexpr 和类型萃取

if constexpr 是 C++17 引入的一种编译时 if 语句,它允许在编译时进行条件判断。这意味着编译器会根据条件来优化生成的代码,这通常会带来更高的性能。

类型萃取:认识你的类型

类型萃取(Type Traits)是 C++11 引入的一组模板,用于在编译时获取类型的属性。例如,std::is_same_v<T1, T2> 可以告诉你 T1 和 T2 是否是同一种类型。

通过结合 if constexpr 和类型萃取,你可以写出高度灵活且类型安全的代码。这也是 std::visit 能发挥最大威力的地方。

 综合应用:泛型 lambda 与类型判断

std::variant<int, double, std::string> v = "hello";std::visit([](auto&& arg) {using T = std::decay_t<decltype(arg)>;if constexpr (std::is_same_v<T, int>) {std::cout << "int: " << arg << std::endl;} else if constexpr (std::is_same_v<T, double>) {std::cout << "double: " << arg << std::endl;} else {static_assert(std::is_same_v<T, std::string>);std::cout << "string: " << arg << std::endl;}
}, v);

这里,我们使用了泛型 lambda 来接受任何类型的 arg,然后用 if constexpr 和类型萃取来确定 arg 的实际类型,并据此执行相应的操作。

std::visit和访问者 模式

一个简单的 std::visit 使用示例。在这个例子中,我将使用 std::variant 来存储不同类型的数据,并展示如何使用 std::visit 以类型安全的方式访问和处理这些数据。

假设我们有一个 std::variant,它可以存储一个 int、一个 double 或一个 std::string 类型的值。我们将编写一个访问者函数对象,这个对象会根据 std::variant 当前存储的类型执行不同的操作。

#include <iostream>
#include <variant>
#include <string>
#include <functional>// 定义 variant 类型
using MyVariant = std::variant<int, double, std::string>;// 访问者函数对象
struct VariantVisitor {void operator()(int i) const {std::cout << "处理 int: " << i << std::endl;}void operator()(double d) const {std::cout << "处理 double: " << d << std::endl;}void operator()(const std::string& s) const {std::cout << "处理 string: " << s << std::endl;}
};int main() {MyVariant v1 = 10;        // v1 存储 intMyVariant v2 = 3.14;      // v2 存储 doubleMyVariant v3 = "hello";   // v3 存储 stringstd::visit(VariantVisitor(), v1); // 输出: 处理 int: 10std::visit(VariantVisitor(), v2); // 输出: 处理 double: 3.14std::visit(VariantVisitor(), v3); // 输出: 处理 string: helloreturn 0;
}

在这个例子中:

  • 我们定义了一个 std::variant 类型 MyVariant,它可以存储 intdouble 或 std::string
  • VariantVisitor 是一个重载了 operator() 的结构体,对每种可能的类型提供了一个处理方法。
  • 在 main 函数中,我们创建了三个 MyVariant 实例,分别存储不同的类型。
  • 使用 std::visit 调用 VariantVisitor 实例,它会自动选择并调用与 variant 当前存储的类型相匹配的重载函数。

这个例子展示了 std::visit 如何提供一种类型安全、灵活的方式来处理存储在 std::variant 中的不同类型的数据。

使用 std::visit 的优缺点

优点

代码简洁

使用 std::visit 可以让你的代码变得更加简洁和组织良好。这正是Bruce Eckel在《Thinking in C++》中所强调的,即“代码的可读性和维护性应当是编程中的首要任务”。

考虑一个没有使用 std::visit 的例子,你可能会这样写:

if (std::holds_alternative<int>(v)) {// 处理 int 类型
} else if (std::holds_alternative<double>(v)) {// 处理 double 类型
} else if (std::holds_alternative<std::string>(v)) {// 处理 std::string 类型
}

而使用 std::visit,这些 if-else 语句可以被优雅地替换为一个泛型 lambda 表达式:

std::visit([](auto&& arg) {// 统一处理逻辑
}, v);

这种简洁性对于代码的组织和可读性有着明显的优势。简单来说,简洁的代码更容易被理解和维护。

 类型安全

std::visit 还具有类型安全(Type Safety)的优点。这意味着编译器将在编译阶段检查类型错误,减少了运行时错误的风险。这与 C++ 的核心原则一致,即“让错误尽早地暴露出来”。

扩展性

std::visit 的另一个优点是扩展性(Extensibility)。如果 std::variant 添加了新的类型,你只需要更新 std::visit 的访问器函数,而无需改动其他代码。

缺点

性能影响

尽管 std::visit 提供了许多优势,但它并非没有代价。其中之一就是潜在的性能影响。由于 std::visit 需要进行运行时类型检查,这可能会引入一定的开销。

然而,现代编译器通常会进行优化,使这种开销最小化。实际上,许多情况下,使用 std::visit 造成的性能损失是可以接受的。

模板代码膨胀

std::visit 是模板函数,这意味着每一种类型组合都可能生成新的实例代码,导致所谓的“模板代码膨胀”(Template Bloat)。

方法代码简洁性类型安全性扩展性性能影响代码膨胀
手动类型检查 (if-else)
std::visit可变

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

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

相关文章

Dijkstra算法对比图神经网络(GNN)

什么是AI模型? AI模型(人工智能模型)是一类模仿人类智能行为的数学模型或算法。它们通过从大量数据中学习,识别模式、做出预测或决策。常见的AI模型包括机器学习模型(如决策树、神经网络、支持向量机)和深度学习模型(如卷积神经网络CNN、循环神经网络RNN)。简单来说,…

Yarn 安装与使用教程

Yarn 安装与使用教程 Yarn 是一个由 Facebook 开发的 JavaScript 包管理工具&#xff0c;它比传统的 npm 更加高效、可靠&#xff0c;并且在性能上有所提升。Yarn 主要解决了 npm 安装速度慢、并发性差、缓存机制不完善等问题&#xff0c;它提供了更快的安装速度、更稳定的依赖…

Spring Boot 的配置加载顺序

Spring Boot 的配置加载顺序是“后来居上”——优先级高的配置源会覆盖优先级低的配置源中的同名配置 覆盖规则如下&#xff1a; 后加载的配置具有更高的优先级&#xff0c;会覆盖先加载的配置。如果多个配置源中存在同名配置项&#xff0c;最终生效的是具有最高优先级的那个…

Git分支重命名与推送参数解析

这两个参数的解释如下&#xff1a; git branch -M master 中的 -M 参数 -M 是 --move --force 的组合简写&#xff0c;表示强制重命名当前分支为 master。如果当前分支已经存在名为 master 的分支&#xff0c;-M 会强制覆盖它&#xff08;慎用&#xff0c;可能导致数据丢失&…

qt源码编译

问题1&#xff1a; 源码头文件问题&#xff1a; 有部分头文件缺少#include<limits>头文件 home/jetson/qt-everywhere-src-5.15.2/qtbase/include/QtCore/qfloat16.h /home/jetson/qt-everywhere-src-5.15.2/qtbase/src/corelib/text/qbytearraymatcher.h 问题2&…

芯岭技术XL32F003单片机 32位Cortex M0+ MCU简单介绍 性能优异

XL32F003单片机是深圳市芯岭技术有限公司的一款基于 32 位 ARM Cortex-M0 内核的高性能微控制器&#xff0c;提供SOP8/SOP14/SOP16/TSSOP20/SSOP24/QFN20/QFN32多种封装可选&#xff0c;可满足不同设计需求。XL32F003可用于工业控制、手持设备、PC 外设、传感器节点等应用场景&…

计算机图形学实践:结合Qt和OpenGL实现绘制彩色三角形

在Qt项目中结合OpenGL与CMake需要配置正确的依赖关系、链接库以及代码结构设计。以下是具体实现步骤和关键要点&#xff1a; 一、环境准备 安装Qt 确保安装包含OpenGL模块的Qt版本&#xff08;如Qt OpenGL、Qt OpenGLWidgets组件&#xff09;。安装CMake 使用3.10及以上版本&a…

3:QT联合HALCON编程—海康相机SDK二次程序开发

思路&#xff1a; 1.定义带UI界面的主函数类 1.1在主函数中包含其它所有类头文件&#xff0c;进行声明和实例化&#xff1b;使用相机时&#xff0c;是用公共相机的接口在某一个具体函数中去实例化具体的海康相机对象。 1.2设计界面&#xff1a;连接相机&#xff0c;单次采集&a…

基于大模型底座重构司法信息系统

前置篇章&#xff1a;法律智能体所需的基础知识 构建一个高效的法律智能体&#xff0c;特别是在基于RAG&#xff08;Retrieval-Augmented Generation&#xff09;架构的背景下&#xff0c;需要融合多种学科和领域的知识。以下是对法律智能体开发和应用所需核心基础知识的简要介…

类《双人成行》3D动作益智冒险类双人控制游戏开发

服务器端采用了基于开源Kbengine&#xff08;引擎使用C和Python编写&#xff09;的多人在线游戏服务器&#xff0c;客户端采用Unity3D。游戏支持线上的双人联机房间功能。 资源地址&#xff1a;类《双人成行》3D动作益智冒险类双人控制游戏开发教程 | Unity 中文课堂 一、游戏…

Spark--基本介绍

Spark是基于内存的快速&#xff0c;通农用&#xff0c;可拓展的大数据分析计算引擎&#xff0c;Hadoop是一个分布式系统基础架构 Spark和Hadoop之间的对比和联系 架构与组件&#xff1a; Hadoop&#xff1a; ■ HDFS&#xff1a;分布式文件系统&#xff0c;负责海量数据存储。…

05-GPIO原理

一、概述 1、GPIO,即通用I/O(输入/输出)端口&#xff0c;是STM32可控制的引脚。STM32芯片的GPIO引脚与外部设备连接起来&#xff0c;可实现与外部通讯、控制外部硬件或者采集外部硬件数据的功能。 2、GPIO的复用:引脚复用是指将单个引脚配置为多个功能的能力。在 STM32 中&…

基于LangChain4J的AI Services实践:用声明式接口重构LLM应用开发

基于LangChain4J的AI Services实践&#xff1a;用声明式接口重构LLM应用开发 前言&#xff1a;当Java开发遇上LLM编程困境 在LLM应用开发领域&#xff0c;Java开发者常面临两大痛点&#xff1a;一是需要手动编排Prompt工程、记忆管理和结果解析等底层组件&#xff0c;二是复杂…

深入解析 Docker 容器进程的 cgroup 和命名空间信息

深入解析 Docker 容器进程的 cgroup 和命名空间信息 在现代 Linux 系统中&#xff0c;控制组&#xff08;cgroup&#xff09;和命名空间&#xff08;namespace&#xff09;是实现容器化技术的核心机制。cgroup 用于管理和限制进程的资源使用&#xff08;如 CPU、内存、I/O&…

【汽车ECU电控数据管理篇】S19文件格式解析篇章

一、S19格式是啥 在电控文件管理的初期阶段&#xff0c;我首次接触到的是 A2L 和 HEX 文件。其中&#xff0c;A2L 文件主要承担着描述性功能&#xff0c;它详细地描述了各种参数和配置等相关信息。而 HEX 文件则是一种刷写文件&#xff0c;其内部明确记录了具体的地址以及对应的…

python编程相关的单词

the: 在编程中&#xff0c;“the” 是一个常见的英语单词&#xff0c;用于指定特定的对象或变量。例如&#xff0c;“the function” 指的是某个特定的函数。 the的拼写是t,h,e.再读一次t,h,e and: 在编程中&#xff0c;“and” 是一个逻辑运算符&#xff0c;用于连接两个条件&…

网络原理 - 4(TCP - 1)

目录 TCP 协议 TCP 协议段格式 可靠传输 几个 TCP 协议中的机制 1. 确认应答 2. 超时重传 完&#xff01; TCP 协议 TCP 全称为 “传输控制协议”&#xff08;Transmission Control Protocol&#xff09;&#xff0c;要对数据的传输进行一个详细的控制。 TCP 协议段格…

python博客爬虫列表

我希望对指定网页的&#xff0c;博客列表&#xff0c;获取url&#xff0c;然后保存成本地文件&#xff0c;用python实现 step1: import requests from bs4 import BeautifulSoup import jsondef get_blog_links(url):headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win6…

软件测试入门学习笔记

今天学习新知识&#xff0c;软件测试。 什么是软件测试&#xff1f; 使用人工和自动手段来运行或测试某个系统的过程&#xff0c;目的在于检验它是否满足规定的需求或弄清实际结果与预期结果之间的差别。 软件测试的目的&#xff1f; 1&#xff09;为了发现程序&#xff0…

uniapp开发2--uniapp中的条件编译总结

以下是对 uni-app 中条件编译的总结&#xff1a; 概念&#xff1a; 条件编译是一种技术&#xff0c;允许你根据不同的平台或环境&#xff0c;编译不同的代码。 在 uni-app 中&#xff0c;这意味着你可以编写一套代码&#xff0c;然后根据要编译到的平台&#xff08;例如微信小…