C++ 以”类型安全”著称,但在 C++17 之前,“值可能不存在” 和 “类型不确定” 一直是痛点。std::optional / variant / any 就是答案。
前言
想象一个场景:你在写一个配置文件解析器,某些字段可能存在,可能不存在。传统做法是:
- 返回
nullptr —— 类型系统无法区分”合法的空值”和”错误” - 返回
std::pair<T, bool> —— 笨拙,难以组合 - 抛出异常 —— 流程控制,异常用于”异常情况”
C++17 的三剑客给出了优雅的答案。
一、std::optional:表示”可能不存在”的值
1.1 基本用法
1 2 3 4 5 6 7 8 9 10
| std::optional<std::string> find_user(int id) { if (id > 0) return "user_" + std::to_string(id); return std::nullopt; }
auto user = find_user(1); if (user.has_value()) { std::cout << "Found: " << *user << "\n"; } std::cout << "or: " << user.value_or("anonymous") << "\n";
|
1.2 与其他方案的对比
| 方案 | 类型安全 | 语法简洁 | 语义清晰 |
|---|
返回 nullptr | ❌ T* vs T 混淆 | ✅ | ❌ 无法区分”合法空”和”错误” |
返回 pair<T, bool> | ✅ | ❌ 访问繁琐 | ❌ |
| 抛出异常 | ✅ | ❌ 流程被打断 | ❌ 只用于异常 |
std::optional<T> | ✅ | ✅ | ✅ |
1.3 应用场景:Map 查询
1 2 3 4 5 6 7 8 9 10
| std::optional<int> get_value(const std::map<int,int>& m, int key) { auto it = m.find(key); if (it != m.end()) return it->second; return std::nullopt; }
std::map<int,int> m{{1,10}, {2,20}}; if (auto val = get_value(m, 1)) { std::cout << "m[1] = " << *val << "\n"; }
|
二、std::variant:类型安全的 union
2.1 基本概念
std::variant<A, B, C> 是一个类型安全的 union——它持有 A/B/C 中之一,且始终有效(不像 union 需要自己追踪类型)。
1 2 3 4
| using Shape = std::variant<Circle, Rectangle>;
Shape c = Circle{2.0}; Shape r = Rectangle{3.0, 4.0};
|
2.2 std::visit: visitation 模式
如何处理不同类型?用 std::visit + visitor 函数:
1 2 3 4 5 6 7 8 9 10
| template<class... Ts> struct overload : Ts... { using Ts::operator()...; }; template<class... Ts> overload(Ts...) -> overload<Ts...>;
auto area = [](const Shape& s) { return std::visit(overload{ [](const Circle& c) { return c.r * c.r * 3.14159; }, [](const Rectangle& r) { return r.w * r.h; } }, s); };
|
2.3 为什么不用 void* 或 union?
1 2 3 4 5 6 7 8 9
| union Data { int i; double d; const char* s; }; Data u; u.d = 3.14;
|
1 2 3 4
| std::variant<int, double, std::string> v = 3.14; double x = std::get<double>(v); double y = std::get<int>(v);
|
三、std::any:任意类型的容器
3.1 基本用法
1 2 3 4 5
| std::any a = 42; std::cout << std::any_cast<int>(a) << "\n";
a = std::string("hello"); std::cout << std::any_cast<std::string&>(a) << "\n";
|
3.2 类型不安全,需要检查
1 2 3 4 5
| try { std::any_cast<double>(a); } catch (const std::bad_any_cast& e) { std::cout << "bad_cast: " << e.what() << "\n"; }
|
与 variant 的区别:variant 的类型列表是编译期已知的;any 的类型是运行时才确定的。
四、类型安全三剑客对比
| 维度 | std::optional | std::variant | std::any |
|---|
| 用途 | 值可能不存在 | 类型列表已知的多态 | 类型完全不确定 |
| 类型安全 | ✅ 编译期 | ✅ 编译期 | ⚠️ 运行时检查 |
| 内存开销 | sizeof(T) + 1 | 最大成员 + alignment | 动态分配 + type erasure |
| 访问方式 | *, .value(), value_or() | std::visit | std::any_cast |
| 空状态 | std::nullopt | ❌ 始终有效 | std::any (empty) |
| 典型场景 | 可选参数、缺失值 | 类型有限的联合 | 插件系统、脚本绑定 |
五、Mermaid 图:类型安全流程
graph TB
subgraph "场景选择"
Q{"需要表示<br/>'可能不存在'的值?"}
Q2{"类型列表<br/>编译期已知?"}
Q3{"需要存储<br/>任意类型?"}
end
Q -->|"是"| OPT["✅ std::optional<T>"]
Q -->|"否"| Q2
Q2 -->|"是"| VAR["✅ std::variant<A,B,C>"]
Q2 -->|"否"| Q3
Q3 -->|"是"| ANY["⚠️ std::any"]
OPT --> OPT_USE["Map查找<br/>可选返回值"]
VAR --> VAR_USE["类型有限的<br/>多态"]
ANY --> ANY_USE["插件/脚本<br/>集成"]
style Q fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
style Q2 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
style Q3 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
style OPT fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
style VAR fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
style ANY fill:#FFDAB9,stroke:#FFAB76,stroke-width:2px,color:#333
六、bonus:std::string_view
虽然不是”三剑客”之一,但 std::string_view 是 C++17 另一个零成本抽象的代表:
1 2 3 4 5
| std::string s = "hello world"; std::string_view sv = s; sv.remove_prefix(6); sv.remove_suffix(1); std::cout << "sv: " << sv << "\n";
|
警告:string_view 不拥有数据,要注意生命周期——原字符串 s 必须保持有效。
七、完整可运行代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| #include <iostream> #include <optional> #include <variant> #include <any> #include <string> #include <map> #include <vector> #include <stdexcept>
template<class... Ts> struct overload : Ts... { using Ts::operator()...; }; template<class... Ts> overload(Ts...) -> overload<Ts...>;
struct Circle { double r; }; struct Rectangle { double w, h; };
std::optional<std::string> find_user(int id) { if (id > 0) return "user_" + std::to_string(id); return std::nullopt; }
std::optional<int> get_value(const std::map<int,int>& m, int key) { auto it = m.find(key); if (it != m.end()) return it->second; return std::nullopt; }
using Shape = std::variant<Circle, Rectangle>;
double area(const Shape& s) { return std::visit(overload{ [](const Circle& c) { return c.r * c.r * 3.14159; }, [](const Rectangle& r) { return r.w * r.h; } }, s); }
int main() { auto user = find_user(1); if (user.has_value()) { std::cout << "Found: " << *user << "\n"; } std::cout << "or: " << user.value_or("anonymous") << "\n"; std::map<int,int> m{{1,10}, {2,20}}; if (auto val = get_value(m, 1)) { std::cout << "m[1] = " << *val << "\n"; } Shape c = Circle{2.0}; Shape r = Rectangle{3.0, 4.0}; std::cout << "circle area: " << area(c) << "\n"; std::cout << "rect area: " << area(r) << "\n"; std::any a = 42; std::cout << std::any_cast<int>(a) << "\n"; a = std::string("hello"); std::cout << std::any_cast<std::string&>(a) << "\n"; try { std::any_cast<double>(a); } catch (const std::bad_any_cast& e) { std::cout << "bad_cast: " << e.what() << "\n"; } std::string s = "hello world"; std::string_view sv = s; sv.remove_prefix(6); sv.remove_suffix(1); std::cout << "sv: " << sv << "\n"; auto pos = sv.find("rl"); std::cout << "found at: " << pos << "\n"; }
|
编译验证:
1
| g++ -std=c++17 -o demo demo.cpp && ./demo
|
总结
| 工具 | 解决的问题 | 一句话总结 |
|---|
std::optional<T> | 值可能不存在 | nullable value — 类型安全的 T* 替代品 |
std::variant<...> | 类型列表已知的多态 | type-safe union — 编译期穷尽检查 |
std::any | 类型完全不确定 | type erasure container — 运行时类型安全 |
行动建议:在 C++ 代码中,优先用 optional 替代 nullptr、用 variant 替代 union,只有在真正需要”任意类型”时才用 any。这三个工具让 C++ 的类型系统更加严密,bug 更难藏身。
📚 C++17 新特性 系列导航
本文是《C++17 新特性》系列第 5/8 篇。
📖 全部 8 篇目录(点击展开)
- (一)Structured Bindings
- (二)if constexpr
- (三)Inline Variables 与 constexpr 加强
- (四)Fold Expressions
- (五)std::optional / variant / any ← 当前
- (六)std::apply / std::invoke
- (七)Filesystem 大全
- (八)Attribute 新增