🌶️std::variant|C++17

std::variant 是 C++17 中引入的标准库类型,用于表示多个可能类型中的一个。它类似于联合体(union),但更安全且易于使用。std::variant 提供了一种类型安全的方式来处理不同类型的值。

如果你用过 C 语言,你肯定用过 union。在 C 编程语言中,union 是一种复合数据类型,允许在同一内存位置存储不同类型的数据。与结构体一样,union 可以有多个成员,但与结构体不同的是,union 分配的内存只够容纳其最大的成员。这就意味着,union 的所有成员共享同一个内存空间。

std::variant适用于之前使用union的场景。

让我们看看下面的 C 代码:

#include <stdint.h>
#include <stdio.h>

union
{
    struct
    {
        uint8_t low  : 4; // 表示该位域占用4个位,这使得我们能够直接操作和访问字节的高低半部分
        uint8_t high : 4;
    }nibles;

    uint8_t bytes;
}myByte;

int main()
{
    myByte.bytes = 0xAB ;

    printf("High Nible : 0x%X\n",myByte.nibles.high);
    printf("Low Nible : 0x%X\n",myByte.nibles.low);

    return 0;
}

输出结果:

1010,对应十六进制的 A

1011,对应十六进制的 B

如上所述,它分配的内存与联合体中最长的元素一样多。所以是 1 个字节。它将写入其中的变量放在相同的地址。这样,如果名为 bytes 的变量发生变化,那么 union 中的另一个元素,即名为 nibles 的结构也会发生变化。

但是联合体在提供便利的同时也带来了一些麻烦,看下面的代码:

我们将 3.14 赋值给 double 变量。然后我们尝试访问 int 变量。结果如下:

结果有些莫名其妙,事实上我们不应该访问int类型的那个变量,在 C++17 中出现的 std::variant,就是为了解决这个潜在的问题,他为我们提供了类型安全的联合体。

什么是std::variant

std::variant 是 C++17 的一个特性,它提供了一个类型安全的联合体。它是 C++ 标准库的一部分,定义在 头文件中。

在 C++ 中,union 允许你在同一内存位置存储不同类型的数据,但它不提供类型安全。std::variant 是对传统 union 的改进,因为它确保了类型安全,并为处理不同类型的值提供了一种方便的方法。

让我们先测试一下类型安全功能。

输出结果为:

我们创建了一个包含 int 和 double 的 std::variant 变量。我们将值 42 赋给 int 变量。然后,我们尝试访问 double 变量,得到了一个错误。就是这样!这就是类型安全!

我们再来看一个使用 std::variant 的例子。

输出结果是:

在主函数中,定义了一个名为 myVariant 的 std::variant,它可以保存一个 int 或一个 std::string。一个 int 值(本例中为 42)被赋值给 std::variant。然后一个 std::string ("Hello, Variant!")被分配给同一个 std::variant。这表明 std::variant 可以灵活地保存不同类型的值。try-catch 块用于处理访问存储在 std::variant 中的值时可能出现的异常。它使用 std::holds_alternative 检查存储值的类型,然后提取并打印相应的值。如果遇到未知类型,则会打印错误信息。如果出现异常(例如试图访问错误的类型),它会捕获异常并使用 std::cerr 打印错误信息。

std::variant公共成员函数

默认构造函数:

赋值构造函数

使用 index 方法获取当前持有类型的索引

index 方法返回 std::variant 当前持有类型在模板参数列表中的索引。

在这个示例中,index 方法返回了 std::variant 持有值的索引。索引 0 表示 int 类型,索引 1 表示 float 类型,索引 2 表示 std::string 类型。

使用 std::variant_alternative 获取类型

std::variant_alternative 可以用来获取 std::variant 模板参数列表中指定索引处的类型。

在这个示例中,std::variant_alternative 用来获取 std::variant 模板参数列表中指定索引处的类型。std::variant_alternative<0, decltype(var)>::type 获取了 var 的第一个类型 intstd::variant_alternative<1, decltype(var)>::type 获取了第二个类型 float,依此类推。

综合示例

Valueless by Exception

valueless_by_exception() 用于查看variant变量是否赋值了。

Swap

用于交换两个std::variant的数值(注意:需要类型一致)

empalce()

emplace() 使用提供的参数进行就地构造

visit()

std::visit 函数用于访问 std::variant 中存储的值。它通过接受一个访问者(visitor)对象来对 std::variant 中当前存储的值进行操作。访问者对象可以是一个函数对象、lambda 表达式或具有重载 operator() 的结构体。

std::visit 的基本用法包括:

  1. 定义一个 std::variant 对象,包含多个可能的类型。

  2. 创建一个访问者对象,定义对每种可能类型的操作。

  3. 使用 std::visit 传递访问者对象和 std::variant 对象。

详细示例

以下是详细的示例,展示如何使用 std::visit 来访问和操作 std::variant 中存储的值。

示例 1:基本用法

示例 2:使用结构体作为访问者

示例 3:访问多个 std::variant

std::visit 还可以用于同时访问多个 std::variant 对象。此时需要一个接受多个参数的访问者对象。

std::variant的优势

  1. 类型安全

如前所述,当访问错误的类型时,传统的联合体类型可能会导致意想不到的错误。

  1. 可读性更高

明确表达了该变量有多种数据类型。

  1. 可以处理函数有多个返回值类型的情况

  1. 与标准库算法兼容

总结

std::variant<T, U, ...>代表一个多类型的容器,容器中的值是制定类型的一种,是通用的 Sum Type,对应 Rust 的enum。是一种类型安全的union,所以也叫做tagged union。与union相比有两点优势:

  1. 可以存储复杂类型,而 union 只能直接存储基础的 POD 类型,对于如std::vectorstd::string就等复杂类型则需要用户手动管理内存。

  2. 类型安全,variant 存储了内部的类型信息,所以可以进行安全的类型转换,c++17 之前往往通过union+enum来实现相同功能。

通过使用std::variant<T, Err>,用户可以实现类似 Rust 的std::result,即在函数执行成功时返回结果,在失败时返回错误信息,上文的例子则可以改成:

需要注意的是,c++17 只提供了一个库级别的 variant 实现,没有对应的模式匹配(Pattern Matching)机制,而最接近的std::visit又缺少编译器的优化支持,所以在 c++17 中std::variant并不好用,跟 Rust 和函数式语言中出神入化的 Sum Type 还相去甚远,但是已经有许多围绕std::variant的提案被提交给 c++委员会探讨,包括模式匹配,std::expected等等。

reference

Last updated

Was this helpful?