在 C++17 之前,如果我们希望让一个变量能存储多种类型的数据(例如有时是 int,有时是 std::string),往往需要依赖 unionvoid* 或多态指针,但这些方式都有明显的安全隐患。

C++17 引入的 std::variant 则提供了一种更优雅、更安全的解决方案。
它是一个 类型安全的联合体(type-safe union),允许你定义一个变量,在同一时间只存储多种指定类型之一。


一、基本用法

#include <iostream>
#include <variant>

int main()
{
    std::variant<std::string, int> data;

    data = "Miku";
    std::cout << std::get<std::string>(data) << "\n";

    data = 39;
    std::cout << std::get<int>(data) << "\n";
}

在上面的例子中:

  • data 可以存储 std::stringint

  • std::get<T>(variant) 用于从 variant 中取出指定类型的值

输出:

Miku 
39

如果类型取错,比如当前存的是 int 却用 std::get<std::string>(data)
程序会抛出异常:

throw std::bad_variant_access;

二、判断当前存储的类型

1. 使用 .index()

variant.index() 返回当前存储类型的索引(从 0 开始):

std::variant<std::string, int> data = "Miku";
std::cout << data.index() << std::endl; // 输出 0
data = 39;
std::cout << data.index() << std::endl; // 输出 1

这种方式能快速判断类型,但不够直观,也不具备编译期安全性。


2. 使用 std::get_if

更推荐的方式是使用 std::get_if<T>()
它返回一个指向值的指针,如果类型不匹配,则返回 nullptr

if (auto value = std::get_if<std::string>(&data))
{
    std::cout << "String value: " << *value << std::endl;
}
else if (auto value = std::get_if<int>(&data))
{
    std::cout << "Int value: " << *value << std::endl;
}

这种写法不会抛出异常,而且非常直观、类型安全。


三、std::visit —— 更优雅的多类型访问方式

如果 variant 包含多个可能类型,而你希望统一处理所有类型,
使用 std::visit 会比多层 if 更简洁:

std::variant<std::string, int> data = 39;

std::visit([](auto&& value) {
    std::cout << "Value: " << value << std::endl;
}, data);

输出:

Value: 39

这里使用了泛型 Lambda 表达式(auto&& 参数),
编译器会根据 variant 当前类型自动推导出 value 的类型。


四、默认构造与 std::monostate

如果你定义的 variant 没有默认构造的类型,比如:

std::variant<int, std::string> v;

这时它会自动构造为第一个类型的默认值(即 int(0))。

但有时我们希望“什么都没有”,类似于“空状态”。
这时可以加上 std::monostate

std::variant<std::monostate, int, std::string> data;
std::cout << data.index() << std::endl; // 输出 0(表示空状态)

data = "hello";
std::cout << data.index() << std::endl; // 输出 2

std::monostate 是一个占位类型,表示“当前没有实际值”。


五、std::variantunion 的区别

特性 union std::variant
类型安全 ❌无类型检查,可能访问错误类型 ✔ 自动记录当前类型
存储结构 所有类型共用一块内存 存储最大类型并记录索引
可用类型 限制多(不能有非平凡构造的类型) 任意可复制/可移动类型
错误行为 访问错误类型是 UB(未定义行为) 抛出 std::bad_variant_access
用途 底层优化、驱动、嵌入式 安全地表达“多种可能的类型”

简单来说:

union 更快,但危险。
std::variant 稍慢,但几乎不可能出错。


六、实际应用:函数返回多种结果类型

std::variant 在函数返回值场景中非常常见。
比如你想实现一个函数,它在读取文件时:

  • 成功则返回字符串内容;

  • 失败则返回错误码。

可以这样写:

#include <variant>
#include <string>

enum class ErrorCode
{
    None = 0,
    NotFound = 1,
    NoAccess = 2
};

std::variant<std::string, ErrorCode> ReadFileAsString(const std::string& filePath)
{
    // 模拟文件读取失败
    return ErrorCode::NotFound;
}

调用:

auto result = ReadFileAsString("data.txt");

std::visit([](auto&& value) {
    using T = std::decay_t<decltype(value)>;
    if constexpr (std::is_same_v<T, std::string>)
        std::cout << "File content: " << value << std::endl;
    else
        std::cout << "Error code: " << static_cast<int>(value) << std::endl;
}, result);

输出:

Error code: 1

这种写法不仅优雅,还能在编译期检查所有可能分支是否都被处理。


七、内存布局与性能

std::variant 的大小 ≈ 最大成员类型的大小 + 类型索引(discriminator)
这个索引用来标记当前储存的是哪种类型。

例如:

std::variant<int, double, std::string> v;
std::cout << sizeof(v) << std::endl;

你会发现它略大于 std::string,因为内部需要额外存储当前类型索引和对齐信息。


八、总结

std::variant 是 C++17 引入的一个极其强大的类型工具,
它本质上是 受控的多态,让你能安全、清晰地表达“一个对象可能是几种类型之一”的概念。

核心特性回顾:

操作 功能
std::variant<T1, T2, ...> 定义多类型容器
std::get<T>(v) 获取指定类型的值(类型不匹配则抛异常)
std::get_if<T>(&v) 获取指定类型的指针(类型不匹配返回空)
v.index() 当前存储类型索引
std::visit(f, v) 对当前存储类型执行函数
std::monostate 定义“空状态”
std::bad_variant_access 类型错误访问异常

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐