【C++ 17 泛型容器对比】C++ 深度解析:std::any 与 std::variant 的细微差别

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【C++ 17 泛型容器对比】C++ 深度解析:std::any 与 std::variant 的细微差别

1. 引言

1.1 C++的类型安全

C++是一种静态类型(Static Typing)的语言。这意味着每个变量和对象在编译时都有一个明确的类型,编译器会对类型进行检查以确保它们的正确使用。好比说,我们不能将一个字符串(std::string)赋值给一个整数(int)变量。

std::string str = "Hello, World!";
int num = str; // 编译错误!

这种静态类型检查非常有用,因为它能够在编译阶段捕获许多可能的错误,从而防止它们在运行时发生。然而,有时我们可能需要在运行时处理各种类型的数据,这时候我们就需要一种能够安全地存储和处理任意类型数据的机制。

在 C++17 中,引入了两个新的类型:std::any 和 std::variant,它们都可以存储和处理各种类型的数据,但是使用方式和适用场景却有所不同。

1.2 对 std::any 和 std::variant 的简单介绍

std::any

std::any(任意类型)是一种可以存储任意类型值的容器,只要这个类型是可复制或可移动的。它的设计目标是为了提供一种安全、易用的方式来在运行时处理各种类型的数据。

std::any a = 1;
a = std::string("Hello, World!");
a = std::vector<int>{1, 2, 3, 4, 5};

然而,std::any 并不知道它存储的数据的具体类型,所以在访问 std::any 中的数据时,我们需要显式地将其转换回正确的类型。

std::variant

std::variant(变体类型)可以看作是一个类型安全的联合体(union)。它可以存储其类型列表中的任何类型的值,并在任何时候都知道当前存储的是哪种类型的值。

std::variant<int, std::string> v = "Hello, World!";
v = 42;

std::variant 在访问其存储的数据时提供了更强的类型安全性,因为它知道当前存储的是哪种类型的值。如果我们试图以错误的类型访问 std::variant 中的值,它将抛出一个 std::bad_variant_access 异常。

这两种类型都有自己的优点和缺点,适用于不同的场景。接下来的章节,我们将深入探讨它们的使用方式,优点,缺点,以及如何根据具体的应用场景选择使用哪一个。

2. std::any 的全面探索

2.1 std::any 的定义和用途

std::any是一个动态类型变量,可以存储任何类型的值。它是由C++17引入的一个新特性。std::any的设计目标是提供一种类型安全且易于使用的方式来在运行时处理各种类型的数据。

std::any a = 42;
std::cout << std::any_cast<int>(a) << '\n'; // 输出 42

这个例子中,我们首先创建了一个std::any类型的变量a,并将一个int值42赋给它。然后,我们使用了std::any_cast函数来将std::any对象的内容转换回int。注意到,我们必须显式地指定要转换的类型,因为std::any在运行时并不知道它存储的数据的具体类型。

2.2 如何使用 std::any

std::any的使用非常直观和简洁。我们可以将任何类型的值赋给std::any对象,只要这个类型是可复制或可移动的。当我们需要访问std::any对象中的值时,我们可以使用std::any_cast函数来进行类型转换。

std::any a = 1;
a = std::string("Hello, World!");
a = std::vector<int>{1, 2, 3, 4, 5};
// 访问 std::any 对象中的值
try {
    std::cout << std::any_cast<std::string>(a) << '\n';
} catch (const std::bad_any_cast& e) {
    std::cout << e.what() << '\n';  // 输出:bad any_cast
}

在这个例子中,我们尝试将std::any对象的值转换为std::string。因为std::any对象实际上存储的是一个std::vector,所以std::any_cast将抛出一个std::bad_any_cast异常。

2.3 std::any 的优点和缺点

std::any的主要优点是它可以存储任何类型的值,这使得我们可以在运行时处理各种类型的数据。此外,std::any提供了一种类型安全的方式来进行这种处理,因为任何错误的类型转换都会在运行时抛出异常。

然而,std::any也有一些缺点。首先,因为std::any在运行时并不知道它存储的数据的具体类型,所以我们需要显式地进行类型转换。这可能会使代码变得复杂和难以理解。其次,std::any的性能可能不如其他类型,因为它需要在运行时进行类型检查和类型转换。

特性 std::any
存储类型 任意类型
类型检查 运行时
类型转换 显式转换
异常处理 std::bad_any_cast
性能 可能较慢

下一章中,我们将深入探索std::variant,并比较它与std::any的相似之处和不同之处。

3. std::variant 的全面探索

3.1 std::variant 的定义和用途

std::variant是C++17引入的另一个新类型,它可以被视为一个类型安全的联合体(union)。std::variant可以存储其模板参数列表中的任何类型的值,并在任何时候都知道当前存储的是哪种类型的值。

std::variant<int, std::string> v = "Hello, World!";
std::cout << std::get<std::string>(v) << '\n'; // 输出 "Hello, World!"

在这个例子中,我们首先创建了一个std::variant对象v,并将一个std::string值"Hello, World!"赋给它。然后,我们使用了std::getstd::string函数来访问std::variant对象中的值。注意到,我们同样需要显式地指定要访问的类型,但是如果我们试图以错误的类型访问std::variant中的值,它将抛出一个std::bad_variant_access异常。

3.2 如何使用 std::variant

std::variant的使用方式与std::any类似,但有一些重要的区别。首先,std::variant在创建时必须指定它可以存储的所有可能类型。其次,std::variant在任何时候都知道当前存储的是哪种类型的值。

std::variant<int, std::string> v = 42;
v = "Hello, World!";
// 访问 std::variant 对象中的值
try {
    std::cout << std::get<int>(v) << '\n';
} catch (const std::bad_variant_access& e) {
    std::cout << e.what() << '\n';  // 输出:bad variant access
}

在这个例子中,我们尝试将std::variant对象的值转换为int。因为std::variant对象实际上存储的是一个std::string,所以std::get将抛出一个std::bad_variant_access异常。

3.3 std::variant 的优点和缺点

std::variant的主要优点是它提供了一种类型安全的方式来存储和处理多种类型的数据。因为std::variant在任何时候都知道当前存储的是哪种类型的值,所以它可以在编译时捕获许多可能的错误。

然而,std::variant也有一些缺点。首先,std::variant在创建时必须指定它可以存储的所有可能类型,这可能使得它不如std::any灵活。其次,如果我们试图以错误的类型访问std::variant中的值,它将抛出一个std::bad_variant_access异常,需要我们进行处理。

特性 std::variant
存储类型 指定的类型列表中的任意类型
类型检查 编译时和运行时
类型转换 显式转换
异常处理 std::bad_variant_access
性能 通常比std::any快

4. std::any 与 std::variant 的对比

4.1 数据类型的处理

std::any和std::variant的一个主要差异在于它们处理数据类型的方式。std::any可以存储任何类型的值,而std::variant在创建时必须指定它可以存储的所有可能类型。

对于std::any,我们可以在任何时候将任何类型的值赋给它,而不需要进行任何改动。

std::any a;
a = 42;
a = "Hello, World!";
a = std::vector<int>{1, 2, 3, 4, 5};

对于std::variant,我们在创建时必须指定它可以存储的所有可能类型。如果我们试图将一个不在类型列表中的类型的值赋给std::variant,编译器就会报错。

std::variant<int, std::string> v;
v = 42;  // ok
v = "Hello, World!";  // ok
v = std::vector<int>{1, 2, 3, 4, 5};  // 编译错误!

4.2 安全性的比较

std::any和std::variant都是类型安全的,但是它们提供了不同级别的类型安全性。

对于std::any,在访问其存储的值时,我们需要显式地将其转换回正确的类型。如果我们试图将其转换为错误的类型,std::any_cast将抛出一个std::bad_any_cast异常。

std::any a = 42;
try {
    std::cout << std::any_cast<std::string>(a) << '\n';  // 抛出 std::bad_any_cast 异常
} catch (const std::bad_any_cast& e) {
    std::cout << e.what() << '\n';
}

对于std::variant,它在任何时候都知道当前存储的是哪种类型的值。如果我们试图以错误的类型访问std::variant中的值,它将抛出一个std::bad_variant_access异常。

std::variant<int, std::string> v = "Hello, World!";
try {
    std::cout << std::get<int>(v) << '\n';  // 抛出 std::bad_variant_access 异常
} catch (const std::bad_variant_access& e) {
    std::cout << e.what() << '\n';
}

4.3 性能的对比

由于std::variant在编译时就知道所有可能的类型,所以它的性能通常比std::any要好。std::any需要在运行时进行类型检查和类型转换,这可能会导致额外的性能开销。

在实际使用中,这种性能差异通常是可以忽略不计的,除非你正在进行性能敏感的编程。在大多数情况下,选择std::any还是std::variant应该根据你的具体需求和使用场景,而不是性能。

5. std::any 与 std::variant 的适用场景

5.1 std::any 的典型使用场景

std::any由于能存储任何类型的值,所以它非常适合用于需要处理各种不同类型数据且无法在编译时确定这些类型的场景。例如,如果你正在编写一个JSON解析器,你可能需要处理的数据类型包括:布尔值,数字,字符串,数组,对象,等等。在这种情况下,你可以使用std::any来存储这些不同类型的数据。

std::map<std::string, std::any> json_object;
json_object["name"] = std::string("John Doe");
json_object["age"] = 30;
json_object["is_married"] = false;
json_object["children"] = std::vector<std::any>{std::string("Alice"), std::string("Bob")};

然而,使用std::any时,需要注意我们必须明确知道每个std::any对象中存储的具体类型,以便在需要访问这些数据时能正确地将其转换回原始类型。

5.2 std::variant 的典型使用场景

std::variant则适用于在编译时就可以知道所有可能类型的场景。例如,如果你正在编写一个编译器,你可能需要处理的数据类型包括:整数,浮点数,字符串,标识符,关键字,等等。在这种情况下,你可以使用std::variant来存储这些不同类型的数据。

using Token = std::variant<int, double, std::string, char>;
std::vector<Token> tokens;
tokens.push_back(42);
tokens.push_back(3.14159);
tokens.push_back(std::string("hello"));
tokens.push_back('+');

使用std::variant的优点是它在任何时候都知道当前存储的是哪种类型的值,这使得我们可以在编译时捕获许多可能的错误。

6. 实战演示:std::any 与 std::variant 的应用

6.1 std::any 的代码示例

让我们来看一个使用std::any的示例。在这个示例中,我们将编写一个函数,该函数接受一个std::any参数,并在控制台上打印其内容。

void PrintAny(const std::any& a) {
    if (a.type() == typeid(int)) {
        std::cout << "Integer: " << std::any_cast<int>(a) << '\n';
    } else if (a.type() == typeid(std::string)) {
        std::cout << "String: " << std::any_cast<std::string>(a) << '\n';
    } else {
        std::cout << "Unknown type\n";
    }
}
// 使用示例
std::any a = 42;
PrintAny(a);  // 输出:Integer: 42
a = std::string("Hello, World!");
PrintAny(a);  // 输出:String: Hello, World!

在这个示例中,我们首先检查std::any对象的类型,然后根据其类型使用std::any_cast将其内容转换为对应的类型。

6.2 std::variant 的代码示例

让我们来看一个使用std::variant的示例。在这个示例中,我们将编写一个函数,该函数接受一个std::variant参数,并在控制台上打印其内容。

using MyVariant = std::variant<int, std::string>;
void PrintVariant(const MyVariant& v) {
    std::visit([](auto&& arg) {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, int>) {
            std::cout << "Integer: " << arg << '\n';
        } else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << "String: " << arg << '\n';
        } else {
            static_assert(std::always_false<T>::value, "non-exhaustive visitor!");
        }
    }, v);
}
// 使用示例
MyVariant v = 42;
PrintVariant(v);  // 输出:Integer: 42
v = std::string("Hello, World!");
PrintVariant(v);  // 输出:String: Hello, World!

在这个示例中,我们使用了std::visit函数和一个泛型lambda表达式来访问std::variant对象的内容。这种方式更具类型安全性,因为如果我们忘记处理某种类型,编译器会给出错误信息。

在下一章中,我们将总结std::any和std::variant的主要差异,并探讨如何根据你的具体需求选择使用哪一个。

7. 结论

7.1 如何根据需求选择 std::any 或 std::variant

std::any 和 std::variant 是 C++17 引入的两种新类型,它们都可以用于存储和处理各种类型的数据,但是它们的使用方式和适用场景有所不同。

选择 std::any 还是 std::variant,主要取决于你的具体需求:

  • 如果你需要处理的数据类型在编译时是未知的,或者可能有很多种,那么 std::any 可能是一个好选择。它可以存储任何类型的值,使用起来非常灵活。
  • 如果你在编译时就能知道所有可能的类型,那么 std::variant 可能是一个更好的选择。它可以存储多种指定的类型,并且提供了更强的类型安全性。

总的来说,std::any 和 std::variant 都是非常有用的工具,它们可以帮助我们编写更安全、更清晰、更易于理解的代码。我们应该根据具体的需求和场景,选择最适合的工具。

7.2 对未来 C++ 类型系统的展望

C++的类型系统一直在发展,std::any和std::variant只是其中的一部分。这两种类型提供了一种新的方式来处理多种类型的数据,这对于编写灵活且安全的代码非常有用。

随着C++的发展,我们期待有更多的类型和特性被引入,以帮助我们更好地解决各种编程问题。无论你是新手还是有经验的开发者,都应该持续关注C++的最新发展,以便能充分利用这些强大的工具。

8. 参考资料

  1. ISO C++标准文档:https://www.iso.org/standard/68564.html
  2. C++ Primer, 5th Edition
  3. Effective Modern C++
  4. C++17 - The Complete Guide

以上就是我们对于C++中std::any和std::variant的全面介绍,希望你能从中获得有用的信息,并在你的编程实践中找到它们的应用。

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。

目录
相关文章
|
9天前
|
存储 对象存储 C++
C++ 中 std::array<int, array_size> 与 std::vector<int> 的深入对比
本文深入对比了 C++ 标准库中的 `std::array` 和 `std::vector`,从内存管理、性能、功能特性、使用场景等方面详细分析了两者的差异。`std::array` 适合固定大小的数据和高性能需求,而 `std::vector` 则提供了动态调整大小的灵活性,适用于数据量不确定或需要频繁操作的场景。选择合适的容器可以提高代码的效率和可靠性。
30 0
|
3月前
|
存储 算法 C++
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
文章详细探讨了C++中的泛型编程与STL技术,重点讲解了如何使用模板来创建通用的函数和类,以及模板在提高代码复用性和灵活性方面的作用。
64 2
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
|
2月前
|
存储 编译器 C++
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
50 9
|
2月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
70 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
3月前
|
安全 C++
C++: std::once_flag 和 std::call_once
`std::once_flag` 和 `std::call_once` 是 C++11 引入的同步原语,确保某个函数在多线程环境中仅执行一次。
|
5月前
|
C++ 运维
开发与运维编译问题之在C++中在使用std::mutex后能自动释放锁如何解决
开发与运维编译问题之在C++中在使用std::mutex后能自动释放锁如何解决
77 2
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
79 0
|
1天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
1天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析