C++17 中的 std::variant 与 std::any 的区别与使用场景

在 C++17 之前,处理不同类型数据的常用方法是使用 void*、继承或手写多态,但这些方法往往缺乏类型安全或代码冗长。C++17 引入了两种新的类型擦除容器:std::variantstd::any。它们虽然都可以存储任意类型,但在语义、使用场景和性能方面有着明显差异。

1. 基本概念

  • std::variant
    代表一组预先确定的类型中的任意一种。编译器在编译期知道所有可能的类型,并对其进行联合存储。访问时需要通过 std::getstd::visitindex() 等机制获取当前类型,并强制转换为具体类型。

  • std::any
    代表任意类型的数据,完全在运行时决定。std::any 通过类型擦除(type erasure)实现,对任何可拷贝或可移动的类型都能存储。访问时通过 `std::any_cast

    ` 获取指定类型的引用,若类型不匹配会抛出异常。

2. 主要区别

特点 std::variant std::any
类型列表 编译期固定 运行时动态
内存布局 单一联合 + 活跃索引 通过 heap 或 small-buffer optimization
访问方式 std::visit、index、std::get std::any_cast
类型安全 编译时检查 运行时检查
性能 较快,避免 heap 分配 可能涉及堆分配,存在性能损耗
用法 多态替代、状态机 需要统一存储不确定类型的接口

3. 使用场景

3.1 std::variant

  • 状态机:一个状态变量只会在有限几种类型之间切换。
    using State = std::variant<InitState, RunningState, ErrorState>;
    State current;
  • 函数返回多种可能类型:返回值既可能是错误码,也可能是成功结果。
    std::variant<std::string, std::error_code> parse(const std::string& s);
  • 配置参数:某个配置值可为 int、double 或 std::string,但只能是这些类型之一。
    using ConfigValue = std::variant<int, double, std::string>;

3.2 std::any

  • 插件系统:插件提供任意类型的数据,主程序不需要预先知道。
    std::any data = loadPluginData(); // 可能是 std::vector <int> 或 MyCustomType
  • 通用事件系统:事件携带不同类型的负载。
    struct Event { std::string type; std::any payload; };
  • 临时缓存:在不想定义多个容器的情况下,暂存任意类型的临时数据。

4. 示例代码

4.1 variant 示例:状态机

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

struct InitState  { std::string msg = "init"; };
struct RunningState { int counter = 0; };
struct ErrorState  { std::string err; };

using State = std::variant<InitState, RunningState, ErrorState>;

void process(State& s) {
    std::visit(overloaded{
        [](InitState& st){ std::cout << "Init: " << st.msg << '\n'; },
        [](RunningState& st){ std::cout << "Running: " << st.counter++ << '\n'; },
        [](ErrorState& st){ std::cout << "Error: " << st.err << '\n'; }
    }, s);
}

// Helper for overloaded lambdas
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

4.2 any 示例:事件系统

#include <any>
#include <iostream>
#include <string>
#include <vector>

struct Event {
    std::string type;
    std::any payload;
};

void dispatch(const Event& e) {
    if (e.type == "numbers") {
        try {
            const auto& vec = std::any_cast<const std::vector<int>&>(e.payload);
            for (int n : vec) std::cout << n << ' ';
            std::cout << '\n';
        } catch (const std::bad_any_cast&) {
            std::cerr << "Bad cast for numbers event\n";
        }
    } else if (e.type == "message") {
        try {
            std::cout << std::any_cast<std::string>(e.payload) << '\n';
        } catch (const std::bad_any_cast&) {
            std::cerr << "Bad cast for message event\n";
        }
    }
}

5. 性能对比

  • variant:所有成员类型已在编译期确定,内存可以在栈上布局,避免堆分配,访问通过 indexstd::visit 进行分支判断,性能优于 any
  • any:需要动态分配内存(或使用 small buffer optimization),并在访问时进行类型检查,产生额外开销。适合类型不确定、数量极少或对性能要求不高的场景。

6. 小结

  • 当你已知所有可能类型且数量有限时,首选 std::variant
  • 当类型完全未知、或者需要统一接口来存储任意类型时,使用 std::any
  • 两者都提供了更安全、更简洁的方式来代替传统的 void* 或手写多态方案。正确选型能让代码更易读、维护成本更低,也能提升运行时性能。

发表评论