std::variant 是 C++17 标准库中引入的一个强类型联合体(Sum Type),它可以在同一变量中存放多种不同类型的值,并保证类型安全。相比于传统的 boost::variant 或手动实现的类型擦除方案,std::variant 的语法更简洁、性能更优,并且与标准库的其他组件配合得更好。下面通过几个典型案例,展示 std::variant 在实际项目中的使用方式,并给出一系列最佳实践建议。
1. 基本用法
#include <variant>
#include <iostream>
#include <string>
using MyVariant = std::variant<int, double, std::string>;
int main() {
MyVariant v = 42; // 整型
std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v);
v = 3.14; // 双精度
std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v);
v = std::string("Hello"); // 字符串
std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v);
}
在上述代码中,std::visit 用于对当前存储的值执行访问操作。每一次访问都必须提供一个可以处理所有可能类型的 lambda 或函数对象。
2. 组合 std::variant 与 std::optional
当你需要表示“可能不存在”且“类型可变”的情况时,可以把 std::variant 包装在 std::optional 中。
using OptionalVariant = std::optional<std::variant<int, std::string>>;
OptionalVariant opt_v;
// 赋值为 int
opt_v = 10;
// 赋值为字符串
opt_v = std::string("optional");
// 清空
opt_v.reset();
3. 使用 std::visit 的重载
C++20 引入了 std::overload 工具,使得编写多态访问器更简洁。
#include <variant>
#include <string>
#include <iostream>
struct Overload {
void operator()(int i) const { std::cout << "int: " << i << '\n'; }
void operator()(double d) const { std::cout << "double: " << d << '\n'; }
void operator()(const std::string& s) const { std::cout << "string: " << s << '\n'; }
};
int main() {
std::variant<int, double, std::string> v = 5.6;
std::visit(Overload{}, v);
}
如果你使用的是 C++17,可自定义一个简单的 overload:
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;
4. 递归型 variant(std::variant 与 std::recursive_wrapper)
std::variant 本身不能直接存储递归类型。可以借助 std::recursive_wrapper 或 std::unique_ptr 来实现。
#include <variant>
#include <memory>
struct Node {
std::variant<int, std::recursive_wrapper<Node>> value;
};
int main() {
Node root{1};
Node child{root};
}
5. 性能注意事项
| 场景 | 建议 |
|---|---|
| 频繁访问 | 使用 std::visit 对每个元素进行访问时,若访问量巨大,考虑将访问器改为结构体以避免重复编译。 |
| 类型切换 | 当变体频繁切换类型时,std::variant 的构造和析构成本较低,优于 std::variant+std::shared_ptr。 |
| 堆分配 | 对于大型数据结构,最好使用 std::unique_ptr 包装,避免在 variant 内部复制大对象。 |
6. 与 std::any 的区别
| 特性 | std::variant | std::any |
|---|---|---|
| 类型安全 | 编译期保证,只能访问已知类型 | 运行时需要强制转换 |
| 性能 | 轻量级、对齐优化 | 可能涉及 heap 分配 |
| 用途 | 需要多态但类型已知 | 需要真正的“任意类型” |
当你能预知可能的类型集合时,优先使用 std::variant。如果类型不确定或动态扩展,才考虑 std::any。
7. 实战案例:简单 JSON 解析
#include <variant>
#include <string>
#include <vector>
#include <map>
#include <iostream>
using JsonValue = std::variant<std::nullptr_t, bool, int, double, std::string,
std::vector <JsonValue>, std::map<std::string, JsonValue>>;
JsonValue parse(const std::string& str); // 简化实现
int main() {
std::string raw = R"({"name":"ChatGPT","active":true,"scores":[99, 97, 100]})";
JsonValue doc = parse(raw);
// 访问示例
if (auto p = std::get_if<std::map<std::string, JsonValue>>(&doc)) {
if (auto name = std::get_if<std::string>(&(p->at("name"))))
std::cout << "Name: " << *name << '\n';
}
}
此示例演示了如何使用 std::variant 构建递归型数据结构,天然支持 JSON 的多种值类型。
8. 最佳实践总结
- 类型集合明确:当业务范围内可预知所有可能类型时,首选
std::variant。 - 使用
std::visit或重载:访问变体时,尽量避免手动std::get,以免遗漏类型。 - 结合
std::optional:需要“可能为空”且“可变类型”的情形,用std::optional<std::variant<...>>。 - 递归结构使用
std::recursive_wrapper或智能指针:避免无限递归。 - 保持变体不变形:尽量不要在运行时频繁改变变体的类型,除非业务需要。
- 避免深层嵌套:深层嵌套会导致编译器产生大量模板实例化,影响编译时间。
- 性能测量:在性能敏感场景下,使用
std::variant与std::any或自定义实现做对比。
通过上述技巧与实践,你可以在 C++ 项目中安全、高效地使用 std::variant,让代码既简洁又易于维护。