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

std::variant 和 std::any 都是 C++17 标准库提供的类型安全的“容器”,用来存放不同类型的值。它们在实现上类似,都可以实现“多态”,但它们的用途、语义以及使用方式有着明显区别。本文将从设计初衷、类型安全、性能以及实际应用场景四个维度,对 std::variant 与 std::any 进行对比,帮助你在实际编码中做出更合理的选择。

1. 设计初衷

std::any

  • 通用性:任何类型(无论是否可拷贝、可移动)都可以存放。
  • 运行时类型信息:存放的对象类型信息在运行时动态决定,访问时需要使用 any_cast
  • 轻量级:内部实现通常为“类型擦除” + 内存分配,存取时没有编译期的类型检查。

std::variant

  • 固定类型集合:在声明时就确定了可接受的类型集合(如 variant<int, double, std::string>)。
  • 编译期类型检查:访问时需要知道确切的类型或使用访问器(std::get / std::visit),编译器可以保证类型安全。
  • 无运行时开销:因为类型集合已知,内部实现一般是“联合体 + 活动成员索引”,没有动态分配。

2. 类型安全与访问方式

std::any std::variant
类型信息 运行时保存类型信息 编译期已知类型
访问方式 `any_cast
std::any_cast(需指定类型) |std::getstd::getstd::visit`
错误处理 访问错误抛 bad_any_cast 访问错误抛 bad_variant_access
编译器检查 只能在运行时检查 编译时可以检测访问错误(如使用错误的索引)

Tip:如果你想在代码中使用 switch 语法遍历多种类型,std::variantstd::visit 更为合适;若你需要将不同类型的对象统一存放在容器里并在运行时决定类型,std::any 是更好的选择。

3. 性能比较

  • 内存占用std::variant 只需存放最大的成员尺寸加上活动索引,通常比 std::any 低。
  • 复制与移动std::variant 复制/移动时只调用对应类型的拷贝/移动构造,开销小;std::any 复制/移动时需要进行类型擦除和动态分配,稍显昂贵。
  • 访问速度std::variant 访问时不涉及动态分配,速度更快;std::any 访问需通过 any_cast 检查类型,存在一定开销。

4. 典型使用场景

需求 推荐类型 说明
需要存储多种不确定类型对象,类型决定在运行时 std::any 如配置文件解析、插件系统等
需要在编译期确定可接受的类型集合,且访问时需要编译期安全 std::variant 如解析 JSON 对象、实现状态机等
想要“模式匹配”式的访问 std::variant + std::visit 代码更简洁、易读
需要存储非拷贝构造/移动构造的对象 std::any 但需自行管理生命周期
需要容器里存放多种类型元素 std::vector<std::variant> 但容器元素类型固定,适合类型集合已知的情况

5. 实战示例

5.1 使用 std::variant 实现 JSON 解析的值类型

#include <variant>
#include <string>
#include <vector>
#include <map>

using JsonValue = std::variant<
    std::nullptr_t,
    bool,
    double,
    std::string,
    std::vector <JsonValue>,
    std::map<std::string, JsonValue>
>;

void printJson(const JsonValue& v, int indent = 0) {
    std::visit([&](auto&& val) {
        using T = std::decay_t<decltype(val)>;
        if constexpr (std::is_same_v<T, std::nullptr_t>) {
            std::cout << "null";
        } else if constexpr (std::is_same_v<T, bool>) {
            std::cout << (val ? "true" : "false");
        } else if constexpr (std::is_same_v<T, double>) {
            std::cout << val;
        } else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << '"' << val << '"';
        } else if constexpr (std::is_same_v<T, std::vector<JsonValue>>) {
            std::cout << "[\n";
            for (const auto& e : val) {
                std::cout << std::string(indent + 2, ' ');
                printJson(e, indent + 2);
                std::cout << ",\n";
            }
            std::cout << std::string(indent, ' ') << "]";
        } else if constexpr (std::is_same_v<T, std::map<std::string, JsonValue>>) {
            std::cout << "{\n";
            for (const auto& [k, v] : val) {
                std::cout << std::string(indent + 2, ' ') << '"' << k << "\": ";
                printJson(v, indent + 2);
                std::cout << ",\n";
            }
            std::cout << std::string(indent, ' ') << "}";
        }
    }, v);
}

5.2 使用 std::any 处理插件系统中的不确定参数

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

struct Plugin {
    void (*execute)(std::vector<std::any>& args);
};

void fooPlugin(std::vector<std::any>& args) {
    // 假设插件需要 int 与 std::string
    int n = std::any_cast <int>(args[0]);
    std::string msg = std::any_cast<std::string>(args[1]);
    std::cout << "fooPlugin: " << n << " - " << msg << '\n';
}

int main() {
    Plugin p{fooPlugin};

    std::vector<std::any> params;
    params.emplace_back(42);
    params.emplace_back(std::string("hello"));

    p.execute(params); // 运行时根据 std::any 访问参数
}

6. 小结

  • std::any:灵活、通用、运行时类型决定;适合插件、配置等动态类型场景。
  • std::variant:类型集合固定、编译期安全、性能更佳;适合需要“模式匹配”或状态机等场景。

在实际项目中,先评估“类型是否固定”,再决定使用哪个容器。若你需要把“任何类型”放进一个统一容器,使用 std::any;若你已经确定了可接受的类型集合,并想要编译期检查,std::variant 是更合适的选择。祝你编码愉快!

发表评论