std::optional¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++17 | 3/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-24 |
编译器支持:GCC 7+ · Clang 5+ · MSVC 19.11+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 为空状态输出 none,有值时输出 optional(value) | ev-fmt-optional-formatter-001 |
| JSON null 映射为 nullopt,其他值 emplace | ev-json-optional-from-json-002 |
| 有值写值,空状态写 null | ev-json-optional-to-json-003 |
本站证据页:ev-fmt-optional-formatter-001 · ev-json-optional-from-json-002 · ev-json-optional-to-json-003
解决什么问题¶
“可能没有值”在 C++17 之前通常用三种方式表达:返回指针并用 nullptr 表示失败;返回 bool 并通过输出参数写结果;或者抛异常。指针引入所有权与生命周期问题,输出参数让调用点冗长,异常不适合“正常但为空”的情况(例如查找未命中)。
std::optional<T> 把“有值/无值”编码进类型。它按值存储 T,不分配堆内存;调用方必须显式检查状态,或在确定有值时解引用。它表达的是值可能缺失,不是操作失败的原因。
基础用法¶
#include <charconv>
#include <iostream>
#include <optional>
#include <string_view>
std::optional<int> parse_int(std::string_view text) {
int value = 0;
auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
if (ec != std::errc{} || ptr != text.data() + text.size()) {
return std::nullopt;
}
return value;
}
int main() {
if (auto parsed = parse_int("42")) {
std::cout << *parsed << '\n';
}
std::cout << parse_int("nope").value_or(-1) << '\n';
}
完整示例见 examples/cpp17/optional.cpp:
示例把 optional 用在配置读取上:查找可能缺失的键、解析可能失败的数字、用 value_or 提供默认值,并单独演示 optional<bool> 的“有值”与“值为真”不是一回事。
进阶用法¶
解引用不检查。 *opt 与 opt->member 在空状态下是未定义行为;value() 会抛 std::bad_optional_access。性能敏感路径用 has_value() 或 if (opt) 分支,异常路径用 value()。
if (opt) 判断的是“是否有值”。 对 std::optional<bool>,if (opt) 为真只说明里面装了一个 bool,不代表该 bool 为 true;要读值写 if (opt && *opt)。这是最常见的误用。
比较语义是“空小于有值”。 两个 optional 比较时,先比状态,再比值;nullopt < optional{0}。与 T 比较时,空状态不等于任何值。不要用比较来判断“值是否满足业务条件”,先检查状态。
optional<T&> 不合法。 标准明确禁止可选引用;需要“可能没有的引用”时用 T*(可空、不拥有)或 std::reference_wrapper<T> 的 optional。两者语义不同:指针可为空,reference_wrapper 一旦构造就必须绑定有效对象。
emplace 会先销毁旧值。 若新值的构造函数抛异常,optional 处于空状态,旧值不会恢复。需要强异常保证时,先构造临时对象再赋值/emplace。
不分配堆内存,但有空间开销。 optional<T> 通常包含一个 bool 标志与对齐后的 T 存储;sizeof(std::optional<char>) 往往大于 1。对超大数组,用 std::vector<T> 加单独的位图可能更省空间。
C++23 才有单子操作。 and_then、transform、or_else 在 C++23 加入;C++17 需要手写 if 链。它们不会自动处理异常,只处理空状态。
与 std::expected 的分工。 optional 只表达“没有值”,不携带失败原因;需要错误码或错误消息时用 expected<T, E>(C++23)。不要为了塞错误信息把 optional 的 T 变成 pair<bool, Error>。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| fmtlib/fmt | include/fmt/std.h#L416-L427 |
为空状态输出 none,有值时输出 optional(value) |
把可选值的两种状态显式呈现,避免日志里出现歧义 |
| nlohmann/json | include/nlohmann/detail/conversions/from_json.hpp#L49-L62 |
JSON null 映射为 nullopt,其他值 emplace |
反序列化时把“字段缺失/为空”统一成类型状态 |
| nlohmann/json | include/nlohmann/detail/conversions/to_json.hpp#L267-L281 |
有值写值,空状态写 nullptr |
序列化时保持双向映射,避免空值变成未定义字段 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。
陷阱与边界¶
- 空状态解引用是未定义行为。
*opt不抛异常;只有value()会抛。 optional<bool>的双重真值。if (opt)检查是否装载,if (*opt)检查装载的值;两者不要混。value_or会复制默认值。 对昂贵类型,value_or(T{})即使有值也可能构造临时对象;用value_or的引用重载或先判断。optional<T>不传播const。const optional<T>的operator*返回const T&,但optional<const T>是另一种类型;不要用后者表达“只读值”。- 比较不会短路求值。
opt == expensive()在空状态下仍可能构造expensive();需要短路时先检查has_value()。 emplace后旧值不可恢复。 构造函数抛异常时optional为空,不提供回滚。optional不是错误处理的全能替代。 它不携带原因、不携带错误码;需要诊断信息时用expected或variant<T, Error>。optional<T>要求T可析构。 存储按值,T的析构函数会在reset、赋值与析构时调用;不要把需要特殊清理的资源直接塞进去。
库联动¶
- fmt:
formatter<std::optional<T>>在T可格式化时可用,输出optional(42)或none。调试日志里能直接区分“空”和“装了 0”。 - nlohmann/json:
from_json/to_json在 C++17 下把nullopt与 JSONnull互转。from_json的重载只在未启用隐式转换时提供;to_json在 C++17 下始终提供。升级库版本或切换JSON_USE_IMPLICIT_CONVERSIONS时要确认实际走哪条路径。 - spdlog:日志参数直接支持可格式化类型;把
optional交给日志前确认T可格式化,否则编译期报错。 - 与
std::variant的分工:optional<T>只有“有/无”两态;variant<T, E>可以表达多个具名状态。状态超过两个时不要用嵌套optional硬凑。
横向对比¶
| 方案 | 空状态 | 携带错误原因 | 所有权 | 主要代价 |
|---|---|---|---|---|
std::optional<T> |
nullopt |
否 | 值语义 | 解引用需检查;有额外空间 |
T* |
nullptr |
否 | 不拥有 | 生命周期与所有权需额外约定 |
std::unique_ptr<T> |
nullptr |
否 | 独占 | 堆分配;不适合小值 |
std::variant<T, E> |
由替代类型表达 | 是 | 值语义 | 类型与访问代码更重 |
std::expected<T, E> |
错误状态 | 是 | 值语义 | C++23 起;错误类型需设计 |
| 异常 | 抛出 | 是 | — | 异常路径成本与禁用异常场景不适用 |
跨标准演进:C++17 引入 std::optional;C++20 增加三路比较与 constexpr 改进;C++23 增加 and_then、transform、or_else 与 monadic 接口。C++26 未改变核心语义。