C++ 进阶(24):可选值、变体、任意类型——optional、variant、any 实战
更新时间:2026-09-01。本文是
languages/cpp/intermediate/进阶第 24 篇,接 random 随机数库。C++17 引入了三种实用类型:std::optional表示"可能有值也可能没有",std::variant是类型安全的联合体,std::any可以存放任意类型。
本文要回答的问题
- optional 什么时候用?和裸指针、哨兵值比有什么优势?
- variant 和 union 有什么区别?为什么类型安全?
- any 什么时候用?性能开销多大?
- 三种类型各适合什么场景?
一、optional:可选值
cpp
#include <optional>
// 读取配置文件,可能不存在
std::optional<int> read_config(const std::string& key) {
if (key == "port") return 8080;
return std::nullopt; // 没有值
}
// 使用
auto port = read_config("port");
if (port) {
std::cout << *port << "\n"; // 解引用
} else {
std::cout << "port not set\n";
}
// 值或默认值
int p = port.value_or(80); // 有值返回,没有返回 80
// 转型
auto v = port.transform([](int x) { return x * 2; }) // 有值则变换
.or_else([] { return std::optional<int>(80); }); // 无值则默认和裸指针 / 哨兵值对比:
cpp
// ❌ 哨兵值:-1 表示无效,但 -1 可能是有效值
int find() { return -1; }
// ❌ 输出参数:语义不清晰,必须检查返回值
bool find(int& out);
// ✅ optional:明确表示"可能有值"
std::optional<int> find();二、variant:类型安全联合体
cpp
#include <variant>
// 定义:可以是 int、float 或 string
using Value = std::variant<int, float, std::string>;
Value v1 = 42;
Value v2 = 3.14f;
Value v3 = std::string("hello");
// 访问:用 visit
auto visitor = [](auto&& arg) {
std::cout << arg << "\n";
};
std::visit(visitor, v1); // 42
// 检查类型
if (std::holds_alternative<int>(v1)) {
std::cout << std::get<int>(v1) << "\n"; // 42
}
// 按索引访问
std::cout << std::get<0>(v1) << "\n"; // 42(int 是第 0 个)和 union 对比:
cpp
// ❌ union:不知道当前存的是什么类型,UB
union { int i; float f; } u;
u.i = 42;
// u.f 是未定义行为
// ✅ variant:类型安全,知道当前类型
std::variant<int, float> v = 42;
// 安全访问三、any:任意类型
cpp
#include <any>
// 可以存任何类型
std::any a = 42;
a = 3.14;
a = std::string("hello");
// 安全取出
if (a.type() == typeid(std::string)) {
auto s = std::any_cast<std::string>(a);
std::cout << s << "\n";
} else {
// 类型不匹配,抛出 std::bad_any_cast
}
// 指针形式(避免异常)
auto* p = std::any_cast<std::string>(&a);
if (p) {
std::cout << *p << "\n";
}性能开销: any 内部使用类型擦除,可能分配堆内存(小对象优化除外)。比直接存类型慢。
四、场景对比
| 类型 | 场景 | 替代方案 |
|---|---|---|
optional | 函数返回值可能为空 | 哨兵值、输出参数、裸指针 |
variant | 几种固定类型之一 | union、继承体系 |
any | 任意类型,不固定 | void*、类型擦除 |
经验:
- 能用
optional不用哨兵值 - 能用
variant不用 union - 能用
variant不用 any(any 类型不固定,更容易出错) - 少用
any,除非真的需要任意类型
五、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| optional 解引用时没有检查 | 未定义行为 | 用 value_or 或检查 has_value |
| variant 类型不匹配时 get | 抛出 bad_variant_access | 用 holds_alternative 检查或用 visit |
| any_cast 类型不匹配 | 抛出 bad_any_cast | 用指针形式或先检查 type() |
| 在性能关键路径上用 any | 堆分配,慢 | 用 variant 或模板 |
相关与延伸
下一篇:span 与 string_view——不拥有所有权的视图;C++17 的三种 utility 类型,和 Rust 的 Option/Result 对比,见 Rust 基础。
一句话总结
C++17 optional/variant/any:optional 表示"可能有值"(替代哨兵值和输出参数);variant 是类型安全的联合体(替代 union),用 std::visit 访问;any 存任意类型(替代 void*),用 any_cast 取出;性能上 optional 几乎无开销,variant 略大于 union,any 最慢(可能堆分配);能用 optional 不用哨兵值,能用 variant 不用 any。