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

在 C++17 之后,标准库新增了两种用于处理多类型值的容器:std::variantstd::any。虽然它们都能保存任意类型的数据,但两者的设计哲学、类型安全、性能开销以及使用场景各有侧重。下面将从实现原理、使用方式、类型安全、性能表现以及典型应用等方面对比两者,并给出实战建议。


1. 基本概念

std::variant std::any
类型安全 静态类型安全,编译期确定可存储的类型列表 运行时类型安全,需手动检查和转换
典型用途 多态值、代替联合、状态机、函数参数的多种形式 存储任何类型的数据,类似脚本语言的“任何值”
存储方式 内部使用联合 + 活跃成员索引 动态分配内存,存储对象的拷贝或移动
性能 对象大小固定,拷贝/移动成本可控 需要动态内存管理,拷贝/移动成本高

2. 语法与基本操作

2.1 std::variant

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

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

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

    v = std::string("hello");   // 赋值为 string
    std::cout << std::get<std::string>(v) << '\n';

    // 访问时可使用 std::visit
    std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v);
}

2.2 std::any

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

int main() {
    std::any a = 10;                    // 初始化为 int
    std::cout << std::any_cast<int>(a) << '\n';

    a = std::string("world");           // 赋值为 string
    std::cout << std::any_cast<std::string>(a) << '\n';

    // 运行时检查类型
    if (a.type() == typeid(std::string)) {
        std::cout << "a holds a string\n";
    }
}

3. 类型安全与错误处理

std::variant std::any
错误捕获 访问错误时抛出 std::bad_variant_access 访问错误时抛出 std::bad_any_cast
运行时检查 `std::holds_alternative
(v)|a.type() == typeid(T)`
编译时约束 必须预先列出所有合法类型 任何类型均可,无编译期约束
  • variant 通过模板参数列表明确可存储的类型,编译器可以在编译期检查类型合法性,避免不匹配的赋值。
  • any 则是完全运行时决定类型,适合需要在运行时动态决定存储类型的情况,但也容易导致类型错误。

4. 性能比较

std::variant std::any
内存占用 固定大小(最大类型大小 + 对齐) 至少为指针大小 + 对象管理元数据
复制/移动 O(1) 或 O(n) 取决于类型 需要堆分配,O(n)
访问成本 O(1) O(1)(但涉及类型检查)
对齐 自己管理 std::any 负责
  • 对于需要频繁访问或复制的值,variant 更高效。
  • 对于一次性存取或需要非常灵活的类型容器,any 更适合。

5. 常见应用场景

5.1 std::variant

  1. 状态机
    用于描述有限状态集合的值,例如 State = std::variant<Idle, Running, Paused>;
  2. 函数重载实现
    通过 std::visit 对不同类型做不同处理。
  3. JSON/YAML 解析
    std::variant<std::nullptr_t, bool, int, double, std::string, std::vector<...>, std::map<...>>
  4. 多值返回
    当函数可能返回多种不同类型时,使用 variant 统一返回。

5.2 std::any

  1. 插件系统
    插件之间需要传递任意类型的数据,使用 any 作为通用容器。
  2. 属性系统
    对象属性可以是任意类型,使用 any 存储属性值。
  3. 脚本与数据绑定
    需要把 C++ 对象暴露给脚本语言时,用 any 封装可序列化的数据。
  4. 临时存储
    在不知道类型的情况下临时存储,后续通过 typeidany_cast 再转回。

6. 组合使用的技巧

  • variant
    variant 的合法类型中嵌套 any,既能保证某些字段类型已知,又能在某些字段上使用任意类型。

  • 多态继承 + std::variant
    若对象是基类指针,可在 variant 中存储指向基类的 `std::shared_ptr

  • std::variant 与 std::optional 的组合
    std::optional<std::variant<...>> 既能表示“无值”,又能容纳多种合法类型。


7. 实战示例:简单的属性系统

#include <any>
#include <unordered_map>
#include <string>
#include <iostream>

class PropertyBag {
    std::unordered_map<std::string, std::any> props;
public:
    template<typename T>
    void set(const std::string& key, T value) {
        props[key] = std::move(value);
    }

    template<typename T>
    T get(const std::string& key) const {
        auto it = props.find(key);
        if (it == props.end())
            throw std::runtime_error("Property not found");

        return std::any_cast <T>(it->second);
    }

    bool contains(const std::string& key) const {
        return props.find(key) != props.end();
    }
};

int main() {
    PropertyBag bag;
    bag.set("id", 123);
    bag.set("name", std::string("Alice"));
    bag.set("active", true);

    std::cout << "id: " << bag.get<int>("id") << '\n';
    std::cout << "name: " << bag.get<std::string>("name") << '\n';
    std::cout << "active: " << bag.get<bool>("active") << '\n';
}

8. 结论

  • std::variant:适合已知类型集合、需要类型安全、性能敏感的场景;通过 std::visit 可以优雅地处理多态值。
  • std::any:适合需要在运行时决定存储类型、灵活性极高的场景;但需承担类型检查成本和内存开销。

在实际项目中,常见的做法是:在内部实现层使用 variant,在对外暴露接口时或与脚本/插件交互时使用 any。两者配合可以既保持类型安全,又兼顾灵活性。

发表评论