C++ 中的 std::variant 如何使用?

std::variant 是 C++17 引入的一个类型安全的联合体,用来在运行时存储多种可能类型中的一种。它的核心理念是“和类型(sum type)”,与传统的 union 不同,std::variant 具有以下优势:

  1. 类型安全:编译器能够检查你访问的类型是否合法。
  2. 构造与析构自动管理:只会调用当前持有值的构造/析构函数。
  3. 不需要显式的标记:与传统 union 需要手动维护类型标记不同,variant 内部自动记录当前值的类型索引。

下面给出几个常见的使用场景,并配以示例代码,帮助你快速掌握 std::variant 的核心语法与技巧。


1. 基础语法

#include <variant>
#include <string>
#include <iostream>

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

    v = 42;                         // 赋值 int
    std::cout << std::get<int>(v) << '\n';

    v = 3.14;                       // 赋值 double
    std::cout << std::get<double>(v) << '\n';

    v = std::string("hello");       // 赋值 std::string
    std::cout << std::get<std::string>(v) << '\n';
}
  • `std::get (v)`:按类型获取值;若类型不匹配会抛出 `std::bad_variant_access`。
  • `std::get_if (&v)`:返回指向值的指针;若类型不匹配返回 `nullptr`,因此不抛异常。

2. 获取当前索引

std::cout << "当前索引: " << v.index() << '\n';   // 0: int, 1: double, 2: std::string

index() 返回当前值所在的位置(从 0 开始)。如果你想把 variant 当作“标记枚举”使用,可以结合 index() 进行判断。


3. 访问多种类型(访问器)

std::visit 可以对 variant 进行模式匹配,像 switch 语句一样处理不同类型。

#include <variant>
#include <iostream>

int main() {
    std::variant<int, double, std::string> v = "world";

    std::visit([](auto&& arg) {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, int>) {
            std::cout << "int: " << arg << '\n';
        } else if constexpr (std::is_same_v<T, double>) {
            std::cout << "double: " << arg << '\n';
        } else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << "string: " << arg << '\n';
        }
    }, v);
}
  • std::visit 的第一个参数是一个可调用对象(函数、lambda 等),第二个参数是 variant
  • std::visit 会自动调用对应类型的函数体,避免显式判断。

4. 递归 variant(Y-combinator 风格)

如果你需要在 variant 内部存储同一类型的递归结构(比如树形结构),可以利用 std::variantrecursive_wrapper

#include <variant>
#include <vector>
#include <iostream>

struct Node;
using NodeVariant = std::variant<int, std::vector<std::variant<int, std::vector<NodeVariant>>>>;

struct Node {
    NodeVariant data;
};

int main() {
    Node root;
    root.data = std::vector <NodeVariant>{ 1, 2, std::vector<NodeVariant>{3, 4} };
    // 这里我们可以递归访问节点
}

5. 与 std::optional 配合使用

有时我们想要一个值要么存在(多种类型之一),要么不存在。可以将 std::variant 嵌套进 std::optional

std::optional<std::variant<int, std::string>> optVar;

optVar = 10;               // 有值,类型为 int
if (optVar) {
    std::visit([](auto&& val){ std::cout << val << '\n'; }, *optVar);
}

6. 性能与注意事项

  1. 大小variant 的大小为其最大成员的大小再加上足够的空间来存储索引(通常是 std::size_t)。
  2. 拷贝/移动:若所有成员都满足 CopyAssignableMoveAssignablevariant 也会相应地实现。
  3. 异常安全:构造和析构时只会对当前持有的类型操作,避免了传统 union 可能出现的未定义行为。
  4. 不可用 voidvoid 不是合法成员类型;如果需要“空”占位,请使用 std::monostate

7. 小练习:实现一个简单的事件系统

#include <variant>
#include <iostream>
#include <vector>

struct MouseEvent { int x, y; };
struct KeyEvent { char key; };

using Event = std::variant<MouseEvent, KeyEvent, std::monostate>;

void dispatch(const Event& e) {
    std::visit([](auto&& arg){
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, MouseEvent>) {
            std::cout << "Mouse at (" << arg.x << ", " << arg.y << ")\n";
        } else if constexpr (std::is_same_v<T, KeyEvent>) {
            std::cout << "Key pressed: " << arg.key << '\n';
        } else if constexpr (std::is_same_v<T, std::monostate>) {
            std::cout << "No event\n";
        }
    }, e);
}

int main() {
    std::vector <Event> events = { MouseEvent{10, 20}, KeyEvent{'A'}, std::monostate{} };
    for (const auto& e : events) dispatch(e);
}

小结

  • std::variant 是 C++17 标准库提供的类型安全多态工具,适合替代传统 unionenum 组合。
  • std::getstd::get_ifindex() 以及 std::visit 是操作 variant 的核心 API。
  • std::optional、递归类型、std::monostate 等配合使用,可以实现更丰富的场景。

熟练掌握 std::variant 能让你在编写可维护、类型安全的代码时更加得心应手。祝编码愉快!

发表评论