optional / expected:用类型表达可失败结果
std::optional 与 expected/Outcome 表达「可能没有」与「失败原因」。

1. 返回值 -1,调用方忘查
标定读 yaml 失败返回 -1,上层当成功继续跑——现场地图全 NaN,复盘找不到哪一步失败。注释「记得查返回值」拦不住 merge 后的新人。optional 表达本可能没有值;expected 附带失败原因,比 exception 轻、比 magic number 可 grep。类型系统逼调用方分支,比 wiki 约定可靠。机器人栈里 bool foo(T* out) 反模式仍常见——库内先 expected,adapter 层再转 ROS success 字段,两层勿混。[[nodiscard]] 标在返回 expected/optional 的函数上,未处理结果编译告警。magic number 失败码在 grep 时与 errno、HTTP 状态码混在一起,domain enum 一眼可读,录包聚合也按 enum 分桶。
2. optional:只表缺失
parse_range 失败返回 nullopt,成功返回 double。optional<bool> 表达三态是反模式——用 enum class。解析失败若需区分格式错与超范围,升级 expected<double, ParseErr>,别 optional + 全局 last_error 字符串——线程不安全、丢上下文、单测难 mock。本帧没有跟踪结果用 optional<Pose>;失败原因用 expected<Pose, TrackFail>,别混成一个类型。optional 的 value() 在未检查 has_value 时抛 bad_optional_access——生产路径用 value_or 或 if (auto v = opt) 模式。C++23 std::expected 与 tl::expected 在机器人栈里任选其一,但同一 repo 内 domain error enum 应统一命名,避免节点 A 用 int、节点 B 用 string 表示同一种 CRC 失败。
3. expected 与 domain 错误
DecodeError:Truncated、CrcBad、VersionMismatch。decode 返回 expected<Pose, DecodeError>,pkt 太短则 unexpected(Truncated)。SLAM 语义失败用 enum class;filesystem errno 可用 expected<void, error_code>。expected<void, E> 表达只有副作用的成功(calibrate),别 optional<bool> 三态歧义——false 是成功还是失败?error enum 保持小而稳,breaking 改 enum 等于改 wire 契约,应版本化或扩展而非改语义。
enum class DecodeError { Truncated, CrcBad, VersionMismatch };
using PoseResult = std::expected<Pose, DecodeError>;
PoseResult decode(BufferView v) {
if (v.bytes < kHeaderSize) return std::unexpected(DecodeError::Truncated);
// ...
return pose;
}4. monadic 链与可读性
and_then / or_else 适合多步解析 YAML 或二进制;两次检查用 if 更清晰,别 overuse。链式失败短路,日志在 or_else 集中打。expected<optional<T>, E> 过绕——拆成两个 API 或统一分层。transform 映射成功值,and_then 串联可能失败的步骤;团队 style guide 可规定「超过三步链式才用 monadic,否则 if 优先可读」。
5. 与 ROS 边界分层
算法库 expected<Pose, DecodeError>,节点 map 成 RCLCPP_WARN + skip frame——别 throw 穿越 rclcpp callback。service 的 success + message 是 wire 层;内部 C++ 用 expected,边界再转换。[[nodiscard]] 标在返回 expected 的函数上,未处理结果编译告警。录包复盘时 error enum 可聚合——Truncated 突增说明链路或驱动版本问题。adapter 层统一 map:unexpected(E) → log + 可选 diagnostic topic,别每个节点各自字符串拼接。
6. 与 exception 取舍
可恢复、预期失败(CRC 错、缺字段)用 expected;编程错误(不变量破坏)仍 assert/exception。别把 expected 当通用异常替代——error enum 爆炸说明 API 粒度有问题。TL expected polyfill 与 C++23 std::expected 均可,关键是 domain 类型一致。 noexcept 边界:expected 本身不抛,适合 callback;exception 仅用于 truly exceptional,且文档写清哪些路径可能 throw。
7. 失败症状与案例
value() 抛 bad_optional_access:未 has_value 就解包。只 nullopt 无 E:线上无法区分 CRC 与截断。标定 yaml 错键名只返回 nullopt,现场换了三版 yaml 仍 NaN——改成 expected 带 MissingKey 后,启动日志一行指到键名。bool + out 参数:调用方传 nullptr 或忘查 return false,ASan 与逻辑 bug 并存——库内禁止新增此模式。
8. 验收
单测覆盖每个 unexpected(E) 与 nullopt。fuzz:无 UB,错误 enum 可统计。日志带可读 E 名。review:库内无新增 -1/nullptr 表示业务失败。CI 可 grep 「return -1」在算法库目录应为零(adapter 除外)。启动失败路径:expected 错误应出现在启动摘要,别 silent 用默认值继续。fuzz 输入应覆盖 truncated packet 与 crc flip,assert error enum 分布可预期——回归时 enum histogram 漂移即告警。
9. 与录包、运维对齐
事故袋应带 decode 失败 enum histogram,否则只能猜丢帧原因。类型表达失败——编译器逼你处理,注释拦不住 merge,类型可以。运维面板展示 Truncated/CrcBad 比例,与驱动版本、网线质量联动分析,比「感觉丢帧多了」可量化。expected 让失败可聚合、可告警、可复盘,是机器人栈 error handling 的默认形状。库边界统一 expected 后,节点层只做 map 与 log,算法同学改 error enum 不必协调十个节点的字符串文案——契约集中在类型里,维护成本随调用方数量线性而非爆炸。
相关
也可以看看
johan's blog