C++ 17 中的 std::variant 用法与实践

在 C++17 之后,std::variant 成为处理多类型值的一种强大工具。它是一个类型安全的联合(union),类似于 std::optional,但可以容纳多种类型。本文将从基本语法、常用成员函数、与传统 std::variant 的区别、以及实际应用场景进行详细阐述,帮助你在项目中高效使用 std::variant

1. 基础语法

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

using Result = std::variant<int, double, std::string>;

int main() {
    Result r = 42;            // 初始化为 int
    std::cout << std::get<int>(r) << '\n';

    r = std::string("Hello"); // 切换为 string
    std::visit([](auto&& arg){ std::cout << arg << '\n'; }, r);
}
  • std::variant<Ts...>:接受一个或多个类型参数。
  • 只能存储这些类型之一。若存储不兼容类型,会在编译阶段报错。
  • 默认构造函数会默认初始化第一个类型。

2. 主要成员函数

函数 说明
value() 访问当前类型值;若类型不匹配则抛出 std::bad_variant_access
value_or() 若当前类型不是所请求的,返回默认值。
index() 返回当前持有的类型索引(从 0 开始)。
`holds_alternative
()| 判断是否持有T` 类型。
`emplace
(args…)| 直接构造T` 并替换当前值。
operator= 赋值或移动操作。
swap() 与另一个 variant 交换内容。

小技巧:在访问时使用 `std::get_if

(&variant)` 可以获得指针,避免异常抛出。

3. 与 std::any 的区别

特点 std::variant std::any
类型安全 在编译期检查;只能是指定类型 运行时检查
性能 小且常量时间 动态分配
用途 用于已知有限类型集合 用于任意类型

当你需要处理一组已知类型的值时,优先使用 std::variant

4. 与传统的联合(union)的比较

union MyUnion {
    int i;
    double d;
    std::string s; // 不能使用非平凡类型
};
  • std::variant 内部维护了类型信息,避免了错误的类型访问。
  • 支持构造函数、析构函数、赋值操作,满足 RAII 要求。
  • 自动进行深拷贝、移动,降低错误率。

5. 典型使用场景

5.1 命令行参数解析

using Arg = std::variant<int, std::string, bool>;
std::map<std::string, Arg> config;
config["threads"] = 4;
config["verbose"] = true;
config["output"] = std::string("log.txt");

5.2 事件系统

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

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

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

5.3 表达式求值树

struct Literal { double value; };
struct Add { std::shared_ptr <Expr> left, right; };
using Expr = std::variant<Literal, Add>;

double evaluate(const Expr& expr) {
    return std::visit([](auto&& node){
        using T = std::decay_t<decltype(node)>;
        if constexpr (std::is_same_v<T, Literal>)
            return node.value;
        else if constexpr (std::is_same_v<T, Add>)
            return evaluate(*node.left) + evaluate(*node.right);
    }, expr);
}

6. 常见坑与优化

  1. 异常安全:`emplace ()` 会先析构旧值再构造新值,若构造抛异常会导致内部状态不一致。可以使用 `std::in_place_type_t` 进行更细粒度控制。
  2. 索引值index() 的返回值从 0 开始,可能导致误解。最好使用 `holds_alternative ()`。
  3. 性能:对大量 variant 对象进行访问时,std::visit 的类型擦除会产生函数指针开销。可以使用 std::visit 的函数指针表或 std::variant_alternative_t 手动实现。
  4. 与结构体:若 variant 只包含 struct,请确保 struct 具有默认构造、复制、移动构造。

7. 小结

std::variant 是 C++17 之后处理多态值的首选工具。它提供了类型安全、性能友好、易于使用的接口,能够替代传统联合和 std::any。掌握其基本语法、成员函数以及常见的使用模式后,你可以在项目中大幅提升代码的可维护性和安全性。希望本文能帮助你快速上手并充分利用 std::variant 的优势。

发表评论