为何std::optional的operator*/operator->在无效时是UB而非抛出异常?
根据Cppreference文档,对处于std::nullopt状态的std::optional调用value()会抛出std::bad_optional_access异常,但使用operator*或operator->解引用则属于未定义行为(UB)。我测试发现,部分编译器下这种UB不会触发崩溃,反而更难调试,误用后问题隐蔽性极强。
测试代码如下:
struct I { int get() { return 1; } }; void f() { try { std::optional<std::shared_ptr<I>> x = std::nullopt; printf("%d\n", (*x)->get()); // -----> UB } catch (std::bad_optional_access & e) { printf("it throws in f\n"); } }
我的问题是:为何std::optional的operator*/operator->被设计为无效状态下产生UB,而非像value()那样抛出异常?
核心原因是性能与使用场景的权衡,具体可以从这几个角度理解:
对齐原生指针的行为习惯
std::optional的设计初衷之一是模拟"可空的原生指针",而原生指针解引用空指针本身就是UB。保持这种行为一致性,能让熟悉原生指针的开发者更快上手,也符合C++"零开销抽象"的设计理念——如果不需要异常检查的开销,就不强迫开发者承担。性能开销的取舍
抛出异常需要额外的运行时检查和异常处理机制,会带来一定的性能损耗。而operator*/operator->被设计为供已经确认optional处于有效状态的场景使用,此时开发者可以自行保证安全性,避免不必要的检查开销。如果需要安全检查,就选择value()或者先通过has_value()/operator bool()判断状态。灵活性与责任转移
C++一贯的设计思路是把选择权交给开发者:你可以选择有检查、带异常的安全调用(value()),也可以选择无检查、高性能的直接访问(operator*/operator->)。这种设计让开发者能根据具体场景做权衡,而不是被强制统一的行为限制。历史与兼容性考量
在std::optional进入标准之前,类似的第三方可选类型(比如Boost.Optional)就已经采用了这种设计。为了兼容已有代码和开发者的使用习惯,标准委员会延续了这一行为模式。
内容的提问来源于stack exchange,提问作者calvin

