news 2026/9/10 14:01:11

Folly `rich_exception_ptr` 设计解析:64 位位打包联合体、RTTI-free 快速路径与取消传播

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Folly `rich_exception_ptr` 设计解析:64 位位打包联合体、RTTI-free 快速路径与取消传播

Follyrich_exception_ptr设计解析:64 位位打包联合体、RTTI-free 快速路径与取消传播

【免费下载链接】follyAn open-source C++ library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly

导读

folly::result是 Folly 中用于消除「灵活的异常」与「朴素的错误码」之间二选一困境的核心错误处理组件,而rich_exception_ptr正是支撑result的能力底座:它既要像std::exception_ptr/folly::exception_wrapper一样能承载任意动态异常,又要在常见 64 位 libcxx / libstdc++ 系统上获得数量级更好的性能,从而把那些「仅因便宜而使用错误码」的 99% 非热路径代码替换掉。本文以 folly/result/docs/rich_exception_ptr.md 设计文档为主线,结合 rich_exception_ptr_storage.h、rich_exception_ptr.h 等源码与测试,完整拆解它「用一个指针同时表达动态 eptr、rich_error、不可变错误、取消信号与小型值」的位打包方案、OC(OperationCancelled)对象身份取舍、四个候选设计以及最终落地实现。读完你将掌握:rich_exception_ptr能存什么、为什么底部 3 个对齐位可以安全复用、get_exception<T>()如何做到无 RTTI 快速判定,以及folly::coro取消传播如何被一劳永逸地保护起来。

为什么需要rich_exception_ptr

folly::result的设计里(见 design_notes.md),目标不是「禁止异常」,而是与异常互操作的同时给出更便宜、更显式的替代方案。问题在于,经典的两条路各有硬伤:

  • 纯异常方案:灵活、有结构化信息,但抛出一次异常往往超过 1 微秒,且std::exception::what()返回const char*,动态消息难以避免分配。
  • 纯错误码方案:便宜,但丢失类型信息、不可携带上下文,使用体验差。

rich_exception_ptr被定位为std::exception_ptrfolly::exception_wrapper对等物(analog),但在常见 64 位 libcxx / libstdc++ 系统上拥有「质变级」更好的性能——便宜到足以在非热路径的 99% 代码中取代只给错误码的系统。它的公共形态在 rich_exception_ptr.h 中定义:

/// `rich_exception_ptr` is an analog of `exception_wrapper` or /// `std::exception_ptr`, with some extra efficiency optimizations, and /// integration with `rich_error` / `epitaph`. class [[nodiscard]] rich_exception_ptr final : public detail::rich_exception_ptr_base { // 唯一的类型查询途径:folly::get_exception<Ex>(rich_eptr) };

exception_wrapper相比,它的 API 刻意做小:唯一测试是否包含Ex类型异常的方式就是folly::get_exception<Ex>(rich_eptr)。它通常「拥有」一个std::exception_ptr,或存一个「廉价可复制」的不可变异常指针;内部私有 API 还可能在联合体里存其它状态(sigil、小型值),因此文档明确建议:终端用户代码不要直接使用rich_exception_ptr,而应使用result暴露的error_or_stopped

rich_exception_ptr能存什么

文档开宗明义:除了必须能存动态std::exception_ptr(并保留folly::exception_wrapper在 make / move 上的优化)之外,它还在内部联合体中为下面几类特殊状态提供高效的联合存储。这些缩写会在后文频繁出现。

动态std::exception_ptr(eptr)

最基本的形态:持有类型擦除的动态异常。folly::exception_wrapper已有的「make 与 move 免分配」优化被完整继承。

Rich errors(RE)

rich_error是一族实现 rich_error_base 接口的错误类型,可视为「更好的异常基类」。文档列举了生产项目几乎必然想要的几个特性:

  • 错误码:兼容std::error_code的语义,但支持constexpr
  • 日志:内建fmtoperator<<(ostream&)支持,日志免分配;对照之下std::exceptionconst char* what()API 在记录动态消息时很难避免分配。
  • epitaph:异常在传播过程中可通过epitaph()叠加 / 串联上下文信息(详见 epitaph.h 与 epitaphs.md)。该操作很便宜(约 60ns,可优化到 5ns 以下),且不改变底层异常的类型身份。

更进一步,rich_errorAPI 的典型用法可以完全避开 RTTI,因此比std::exception继承体系更快。

Immortalrich_error异常(immortal RE)

「不可变 rich_error」是指向不可变constexpr异常的廉价可复制指针,可以直接作为rich_exception_ptr传递,并暴露rich_errorAPI。之所以必须存在这个形态,是因为动态std::exception_ptr有四个痛点:

  1. 构造必然分配;
  2. 复制 / 析构要更新原子引用计数;
  3. 不可能是constexpr
  4. 静态变量有 SIOF(静态初始化顺序失败)风险,或需要函数调用才能初始化。

一句话:异常指针比std::error_code贵得多。这也是为什么在设计相近的boost::outcome中,outcomeresult是两个独立类型。Immortal RE 恰好弥合了这个差距——成本和错误码差不多,却能原生用于result的异常风格 API。构造方式见 immortal_rich_error.h:

static constexpr auto badFruit = immortal_rich_error<coded_rich_error<FruitCode>, FruitCode::BAD, "Rotten, moldy, or damaged"_litv>.ptr();

为什么要特判OperationCancelled(OC)

folly::coro有一个约定:子任务可以向父任务传递「因取消而停止」的信号。folly/OperationCancelled.h(当前仓库实现于 folly/OperationCancelled.h)如今是以异常形式穿过folly::coro协程传播的。子任务有多种方式发出它,但传播语义一律默认抛出

co_yield co_stopped_may_throw; throw OperationCancelled{}; // discouraged co_yield co_error{OperationCancelled{}}; // discouraged

大部分用户代码本不该直接操作取消原语——它主要是实现高层算法的工具,99% 情况下用户代码的正确行为是「尽快展开栈并返回」。但今天基于异常的实现把「取消完成」的细节暴露得过于彻底:下面这些无辜的用户代码,在没有任何特判的情况下就会打断取消传播

try { co_await task(); } catch (...) { handleError(); } try { co_await task(); } catch (const std::exception& ex) { handleError(ex); } auto res = co_await co_awaitTry(task());

后果是用户代码里充斥着大量显式、易错的取消处理,以及大量 bug。文档作者计划提出co_yield co_stopped_nothrow及配套迁移策略:既让现有代码继续工作,又鼓励新代码 / 重构代码采用新原语。

对本设计文档而言,只需知道三点:

  • 将来会有两种...OperationCancelled类型;
  • co_stopped_nothrow发出的类型在folly::coro代码中总是像被co_nothrow()包裹一样传播;一旦走出协程(如blocking_wait),因为没有更好的替代方案,它仍会被抛出;
  • 因此,两者都必须能被rich_exception_ptr表示,并且彼此之间、以及与其他类型之间都要能廉价地区分

小型值(small values)

联合存储的另一个用途是小值优化:类似result<int>result<Foo*>的类型可以只占8 字节。这是文档列出的「将来态」(源码中kImplementsSmallValue = false,见下文),但位布局已为其预留编码SMALL_VALUE_eq = 6

位打包联合体:把 8 种状态塞进一个指针

在受支持的 64 位平台上,rich_exception_ptrstd::exception_ptr尺寸相同,却塞进了上述全部额外功能。做法是:变体选择信息存在指针底部 3 个「对齐位」里(这 3 位本来是零),有时还会再检查其余 61 个「指针位」是否全零。要点是:

  • 当低 3 位告诉我们存的是小型值时,不能把「高位全零」当作特殊状态(小型值的任意位都可能是 0);
  • 但在其它所有情况下,「空指针」可以是一个被区分的状态。

设计文档特别解释了为什么用低 3 位而不是高字节:虽然 64 位指针的顶部字节(甚至更多)通常也用不到,但复用这些位会干扰内存标签(memory tagging)方案;相反,8 字节对齐保证低 3 位恒为零,复用它们既便宜又安全。

理论上用 4 位可表达2**4 - 2 = 14种 error-or-stopped 状态、存 7 种 error-or-stopped 指针。不过除了「小型值」外,当前设计只需要8 个状态 + 6 种指针,这给了位打包 / 解包更多的 CPU 优化空间。文档明确说:这一节是为了解释「为什么最终是现在的状态集和位表示」,如果只想看位表,直接跳到「Idea 1」的表格。

源码中的具体佐证见 rich_exception_ptr_storage.h:bits_t枚举定义了全部位值,且严禁使用大于 7 的值

enum bits_t : uintptr_t { // 该位为 1 表示联合体是我们拥有、需要析构的 std::exception_ptr OWNS_EXCEPTION_PTR_and = 1, // immortal_storage_ 为空时表示空 exception_ptr IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq = 0, // 将来小值优化:把 <=61 位的平凡 T(如 int、void&)内联进来 SMALL_VALUE_eq = 6, // 存 private_rich_exception_ptr_sigil(值在 ...uintptr_ 中) SIGIL_eq = 2, // eptr_ref_guard_ 里是一份「非拥有」的泄漏单例 StoppedNoThrow 副本 NOTHROW_OPERATION_CANCELLED_eq = 4, // 用于快速类型查询的掩码(位 1-2) mask_FAST_PATH_TYPES = 6, // 快速路径判定值(masked_eq 语义): IS_RICH_ERROR_BASE_masked_eq = 0, // rich_error_base 位于偏移 0 IS_OPERATION_CANCELLED_masked_eq = 4, // OperationCancelled 快速路径 // 仅当 OWNS_EXCEPTION_PTR 置位时使用: owned_eptr_KNOWN_NON_FAST_PATH_TYPE_masked_eq = 6, // 已知类型但非快速路径 owned_eptr_UNKNOWN_TYPE_masked_eq = 2, // 慢路径,需要 RTTI };

存储布局:packed 与 separate 两套实现

同一份bits_t背后有两套存储访问器:

  • rich_exception_ptr_packed_storagedata_t data_;单成员,位藏在data_.uintptr_低 3 位。static_assert(sizeof(...) == sizeof(std::exception_ptr))保证与std::exception_ptr同尺寸。
  • rich_exception_ptr_separate_storagedata_t data_; bits_t bits_;分开存放,作为 packed 不受支持时的回退。static_assert(sizeof(...) == sizeof(std::exception_ptr) + sizeof(uintptr_t))

rich_exception_ptr_base(rich_exception_ptr.h 末尾)按is_supported在两者间选择:

using rich_exception_ptr_base = rich_exception_ptr_impl< rich_exception_ptr, conditional_t< rich_exception_ptr_packed_storage::is_supported, rich_exception_ptr_packed_storage, rich_exception_ptr_separate_storage>>;

packed 是否可用由编译期常量判定(is_supported),要求三者的对齐度都 ≥ 8(3 位):

static constexpr bool is_supported = alignof(detail::immortal_exception_storage) >= min_alignment_for_packed_rich_exception_ptr && // 8 alignof(std::max_align_t) >= min_alignment_for_packed_rich_exception_ptr && alignof(std::exception) >= min_alignment_for_packed_rich_exception_ptr;
  • immortal_exception_storage*,直接检查其对齐度;
  • std::exception_ptr,依赖平台实现细节:Itanium ABI 下__cxa_allocate_exception只接收thrown_size,必然以最大基础对齐存放异常对象;Windows 下对齐未公开文档化,但至少须满足std::exception的对齐要求(源码用alignof(std::exception)探测)。GNU 系统上exception_ptr含 1 个指针,MSVC 上含 2 个(detail_num_ptrs_in_exception_ptr有对应的static_assert)。

联合体成员本身也值得注意——rich_exception_ptr_base_storage::data_t是一个匿名联合:

union alignas(std::exception_ptr) data_t { uintptr_t uintptr_; // 位访问 / sigil / 小型值 const detail::immortal_exception_storage* immortal_storage_{}; // immortal RE mutable_eptr_ref_guard eptr_ref_guard_; // owned eptr / nothrow OC 单例 };

其中eptr_ref_guarduintptr_t数组 +std::launder/reinterpret_cast进行类型双关——注释说明这是刻意为之:如果直接用匿名联合放std::exception_ptr,类型就无法用于常量求值(constant-evaluated code),所以只能冒reinterpret_cast的风险。这也是整套「位打包」能同时服务constexpr与运行时性能的关键。

位打包设计的输入:状态、效率优先级与 OC 身份

需要表示的状态

设计文档完整列出如下状态集:

  • 小型值(small value)——必须允许任意 61 位(全零或非零皆可)。
  • Try——为了让Try未来能用result重新实现。
  • exception_ptr——最好所有位全为零;可以共享 immortal RE 的 3 位编码(空的不存在,无歧义)。

以下状态要求高位存非空指针:

  • Immortalrich_error——底部 3 位必须000,因为 C++20 不允许在constexpr代码里对指针做位操作(源码中 packed 存储的get_bits()std::is_constant_evaluated()时直接返回 0,apply_bits...在 consteval 下要求 bits 为 0,正是这条约束的体现)。
  • 未知类型的exception_ptr
  • 指向rich_errorexception_ptr
  • 指向两种OperationCancelled变体的exception_ptr
    • Legacy / 被抛出的异常:目前存动态exception_ptr
    • 新的co_stopped_nothrow:目前存一个泄漏单例指针。
  • 已知类型但非快速路径的exception_ptr

为什么「已知类型非快速路径」要与「未知类型」区分?因为在「快乐路径」上,一个错误只由rich_exception_ptr处理,我们希望get_exception<Ex>()Exrich_error_baseOperationCancelled时始终走无 RTTI 快速路径。仅仅回答「是的,它派生自Ex」不够,位还必须能回答「它肯定不是Ex」——这就是快速否定(fast negative)能力,用于把不匹配的查询廉价挡掉。

运行时效率优先级(从高到低)

实现(Idea 1)满足以下优先级,每条都对应源码中的一行位测试:

  • has_dynamic_exception_ptr()是极常见的检查,必须快——测试bits_ & 0x1(即OWNS_EXCEPTION_PTR_and,1 个周期);
  • 「是不是 rich error 指针」的廉价测试——!(bits_ & 0x6)(即快速路径位为 0 =IS_RICH_ERROR_BASE_masked_eq);
  • has_stopped()要胜过 RTTI——4 == bits_ & 0x6IS_OPERATION_CANCELLED_masked_eq)。

源码get_exception_from_owned_eptr()正是这么做的:先取mask_FAST_PATH_TYPES & bits,命中IS_RICH_ERROR_BASE_masked_eqIS_OPERATION_CANCELLED_masked_eq就走exception_ptr_get_object静态转换;命中「已知非快速路径」直接返回nullptr;只有UNKNOWN_TYPE才 fall through 到需要 RTTI 的慢路径。

必须保留OperationCancelled的对象身份吗?

理想世界中(参考 P1667 的讨论),OperationCancelled不应被视为异常,而只是一个「尽快拆栈」的简单 sigil——sigil不需要对象身份。可惜 legacyOperationCancelled经常以exception_wrapper形式存在,能暴露OperationCancelled*,且该指针在受支持平台上历史上是稳定的(除非被重新抛出)。设计文档给出的调查结论是:几乎不可能有现存用户代码依赖对象身份,那样的代码看起来就很奇怪:

OperationCancelled* innerPtr = nullptr; auto ew = co_await co_awaitTry([&innerPtr]() -> now_task<> { auto innerEw = make_exception_wrapper<OperationCancelled>(); innerPtr = get_exception<OperationCancelled>(innerEw); co_yield co_error(std::move(innerEw)); }()).exception(); auto* outerPtr = get_exception<OperationCancelled>(ew); // Outcome A: assert(innerPtr == outerPtr); // Outcome B: assert(innerPtr != outerPtr); // AND, `innerPtr` is no longer valid

OperationCancelled从「背后是exception_ptr」迁移成「简单 sigil」有不错的效率收益,问题是:从今天的「Outcome A」迁到更便宜的「Outcome B」是否存在不可接受的(现在或将来)风险。实际风险看似很低,但std::exception_ptr_cast提案(P2927)支持「在不抛异常的代码里保留异常对象身份」的想法(Windows 上被抛出的异常可能被复制)——接受该提案等于祝福「不 rethrow 也能处理 eptr」的泛型代码,这类代码会理所当然地假设指针稳定。

最终实现(Idea 1)以最保守的方式解决这个难题:

  • legacy(被抛出的)OperationCancelled:保留动态对象身份
  • 新的co_stopped_nothrow:使用泄漏单例,但仍保存其指针——万一两个 DSO 各自实例化了不同的单例,对象身份依然得以保留(Meyer 单例天然是「每个 DSO 一份」)。

源码中rich_exception_ptr_impl(StoppedNoThrow)构造函数正是这个单例模式:局部InitializedSingleton继承mutable_eptr_ref_guard,首次使用时构造exception_ptr,通过静态局部变量形成线程安全的 Meyer 单例并故意泄漏以避免 SDOF(静态析构顺序失败);复制时按位拷贝而不增加引用计数(NOTHROW_OPERATION_CANCELLED_eq状态由bits_store_eptr()识别为非拥有关系)。对应地,rich_exception_ptr.h 里的桩类型也写得很清楚:StoppedNoThrow不是std::exceptionStoppedMayThrow才是。

被否决或无限期搁置的需求

设计文档明确列出了「占着额外位但最终不做」的几个想法及理由:

  • RTTI-free 的std::exception:对 immortal RE 已天然成立,对动态 eptr 也容易做到,但被否决——一是实现有取舍,无法与 Idea 1 结合而不伤害「is eptr?」的性能;二是它唯一能带来的是让 RTTI-free 代码去 logwhat(),而what()是糟糕的 API,记录带详细上下文与 epitaph 栈的rich_error才应被强烈推荐。
  • 支持非rich_error类型的不可变异常:技术上直截了当,但被阻塞——直到 C++(或所有相关工具链)允许改写 constexpr 指针的对齐位;且大多数有意义的使用场景是派生自std::exception的错误,而std::exception要到 C++26 才 constexpr。另外,让你的不可变错误会说rich_error协议通常只有好处没有坏处。
  • 在 epitaph 栈中缓存「无底层错误」:正如rich_error_base的 docblock 所述,如果rich_exception_ptr能再省出一个空闲位,就能缓存 epitaph 栈中不存在底层错误这一事实。但当前方案已相当不错,且再挤出一个跨平台位并不容易,故搁置。

设计空间:四个候选方案的取舍

Idea 0:为两种 OC 都存动态 eptr

给两种 OC 各分配一个 3 位槽位,都保留动态 eptr。

  • 未采用——「has dynamic eptr?」将不再是 1 位测试;
  • 而且正常co_stopped_nothrow用法也不想分配动态 eptr。

Idea 1:只为 legacy 抛出的 OC 存动态 eptr(当前实现)

  • 用一个 3 位槽位让 legacy 抛出 OC 的 eptr 可变,匹配现有行为、简化迁移;
  • 新的 nothrow OC 也存一个 eptr,但其构造函数被限制为只能指向泄漏单例

CAVEAT:如果你抛出其中一个,在exception_ptr里接住,再把它 ingest 进rich_exception_ptr绝不能把它放进 RTTI-free 状态,否则会丢失对象身份。

  • 现阶段 OC 用户文档一律写成「OC 指针从不保证稳定」,以保留将来迁移到 Idea 2 的选项价值——即不承认 P2927 被接受可能带来更强的契约。

位表如下(文档原表):

Top 61 bits 1-bit 2-bit 4-bit (dynamic eptr?) (is RE?; is OC?) small value any 0 1 1 immortal RE not all zero 0 0 <-RE-> 0 empty eptr 0...0 0 0 <-RE-> 0 Constants -- empty Try sigil 0...0 0 1 0 (future sigils) not all zero 0 1 0 nothrow OC not all zero 0 0 <-OC-> 1 Dynamic eptrs -- unknown-type eptr not all zero 1 1 0 RE eptr not all zero 1 0 <-RE-> 0 thrown OC not all zero 1 0 <-OC-> 1 non-fast-path eptr not all zero 1 1 1

把该表与源码bits_t枚举一一对照,可以得到每个状态的具体 3 位码(这是从代码可验证的事实):

状态3 位码(二进制)对应的bits_t
immortal RE / 空 eptr000IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq = 0
RE eptr(owned)001OWNS_EXCEPTION_PTR_and \| IS_RICH_ERROR_BASE_masked_eq = 1
Trysigil / 将来 sigil010SIGIL_eq = 2
unknown-type eptr(owned)011OWNS_EXCEPTION_PTR_and \| owned_eptr_UNKNOWN_TYPE_masked_eq = 3
nothrow OC100NOTHROW_OPERATION_CANCELLED_eq = 4
thrown OC(owned)101OWNS_EXCEPTION_PTR_and \| IS_OPERATION_CANCELLED_masked_eq = 5
small value110SMALL_VALUE_eq = 6
已知非快速路径 eptr(owned)111OWNS_EXCEPTION_PTR_and \| owned_eptr_KNOWN_NON_FAST_PATH_TYPE_masked_eq = 7

对照表可见:位 0(1-bit列)即「dynamic eptr?」标志;位 1-2(mask_FAST_PATH_TYPES = 6)即「RE / OC」快速路径字段——00为 rich(IS_RICH_ERROR_BASE),10为 OC(IS_OPERATION_CANCELLED);空 eptr 与 immortal RE 共享编码000,靠「高位是否全零」区分,正如文档所述「空 eptr 可以共享 immortal RE 的 3 位编码,因为空的 immortal 不存在」。

Idea 2:两种 OC 都不存 eptr

两种 OC 都避免动态 eptr,但用一个既区别于「空Try」也区别于「immortal RE」的 3 位码。

  • 暂不采用——主要原因是迁移所有现存构造 legacy OC 对象的代码到「泄漏单例 eptr」语义成本太高;而且 legacy OC 哪怕只抛一次,其代价也比分配动态 eptr 贵 50 倍。

Idea 3:两种 OC 与「空Try」共享 3 位码

把两种 OC 都当作无 eptr 常量,并且Try共用同一个 3 位码。

  • 未采用——会丢弃 OC 对象身份;位不紧缺;还会让「is OC?」测试变慢。

Idea 4:两种 OC 与「空Try」共享 immortal RE 的 3 位码

分配 constexpr 指针,把 OC 与空Try都放进「immortal RE」命名空间。

  • 省下一个 3 位码,代价是每次从Try里取 RE 都要测试「is emptyTry?」;
  • 必须让 OC 遵守 RE 接口(通过「is RE? && is OC?(经 vtable)」测试);
  • 需要把 legacy OC 迁移到单例 eptr 模型;
  • 未采用——没必要,复杂度与运行时成本更高。

源码中的落地实现细节

构造、复制、移动与析构的位状态机

rich_exception_ptr.h 的rich_exception_ptr_impl完整实现了按位的生命周期管理:

  • 默认构造 = 空 eptr(IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq+ 空指针);
  • 按值构造Ex ex:按Ex的静态类型选择位——派生自rich_error_baseIS_RICH_ERROR_BASE快速路径(要求rich_error_base在偏移 0,has_offset0_base静态断言),派生自OperationCancelledIS_OPERATION_CANCELLED,其余走owned_eptr_KNOWN_NON_FAST_PATH_TYPE。注释明确警告:不希望用户从类型擦除的std::exception_ptr/exception_wrapper构造,那会破坏result代码里的 RTTI 规避优化;
  • from_exception_ptr_slow(std::exception_ptr&&):把类型擦除的 eptr 设为unknown-typeOWNS | UNKNOWN_TYPE),避免急切 RTTI;注释提到将来可让可变 getter 机会式地更新bits_
  • 复制 / 移动:owned eptr 走真正的exception_ptr复制 /extract_exception_ptr移动;nothrow OC 按位复制单例(执行exception_ptr的复制构造);immortal / sigil / small value 走copy_unowned_pointer_sized_state
  • make_moved_out:带异常的状态(owned eptr、nothrow OC、immortal)移出后统一回到「空 eptr」状态;测试rich_exception_ptr_fundamentals_test.cpp断言移出后EXPECT_EQ(rich_exception_ptr{}, src)

epitaph 透明性:with_underlyingget_exception

rich_exception_ptr的一个关键承诺是「对 epitaph 透明」:get_exception<Ex>()访问的是底层原始异常,而不是存上下文的包装层;但fmt/operator<<会展示完整 epitaph 栈。实现靠两件事:

  • with_underlying(fn):如果外层是rich_error_base且存在underlying_error(),就沿链走下去再调用fn——throw_exception()to_exception_ptr_slow()都经由它,确保 rethrow 时丢弃 epitaph 包装、直接命中用户catch的目标类型;
  • get_exception<Ex>()返回rich_ptr_to_underlying_error<Ex>(一个指针样对象,而非裸Ex*),内部保留top_rich_error_,这样LOG(INFO) << ex能同时给出「底层类型」与「完整 epitaph 栈」。把返回值转成裸Ex*就会丢失 epitaph,头文件 docblock 明确劝阻。

operator==:比较底层异常对象指针

相等比较在with_underlying之后进行,忽略 epitaph 包装:

  • immortal 对 immortal:比较get_immortal_storage()指针(假定每个 immortal 异常有唯一的 storage);
  • immortal 对 owned:检查 immortal 的可变单例是否已创建,若已创建再比较其中的 eptr;
  • owned 对 owned / nothrow OC:比较底层exception_ptr

文档注释还记录了一个多 DSO 警告:同一immortal_rich_error<...>::ptr()在多个 DSO 实例化会产生多份底层错误,指针比较会不等;解决办法是指定单一链接单元实例化,通过头文件只暴露rich_exception_ptr

格式化输出:format_tooperator<<

rich_exception_ptr.cpp 的format_to是位状态机的「渲染端」,逐状态处理:

  • SIGIL_eq:打印[result has value]/[empty Try]/[unknown sigil]
  • NOTHROW_OPERATION_CANCELLED_eq:打印folly::detail::StoppedNoThrow
  • immortal 且指针为空:[empty]
  • rich error:rex->format_with_epitaphs(out)输出完整 epitaph 栈;
  • owned 非 rich 异常:exception_ptr_get_type拿 typeid、folly::demangle(try-catch 包裹,避免格式化时因 OOM 抛异常)、再叠加what(),形如TypeName: message

operator<<则通过detail::ostream_write_via_fmt复用format_to,因此fmt::format与流式输出行为完全一致(fmt::formatter<folly::rich_exception_ptr>也已特化)。

空 eptr 的 UB 防护

按标准 rethrow 空 eptr 是 UB,因此throw_exception_impl在非法位状态 / 空 eptr 时走terminate_on_empty_or_invalid_eptr,与exception_wrapper的行为对齐。bad_result_access_singleton()则用「空析构函数的 union + Meyer 单例 + 故意泄漏」的技巧构造bad_result_access_errorexception_ptr,供 sigil / small-value 状态的错误访问兜底。

测试与基准:设计如何被验证

folly/result/test/rich_exception_ptr_fundamentals_test.cpp 覆盖了基本操作语义:

  • checkEptrRoundtrip:对 owned rich error、owned 非 rich error(如std::runtime_error)、immortal、nothrow OC 做std::exception_ptr往返(from_exception_ptr_slowto_exception_ptr_slow),并验证「复制往返不改变源对象」「移动往返后源对象等于空 eptr」;
  • 相等语义:新建的 owned rich error 对象指针不同故不等;同一immortal_rich_error的所有实例共享同一对象指针故相等;immortal 往返后变成「指向其可变单例的 owned eptr」,但聪明的比较逻辑依然判等;
  • nothrow OC 的往返同样保持逻辑相等。

同目录还有 rich_exception_ptr_constexpr.cpp(验证 constexpr 约束:immortal 位必须为 0)、rich_exception_ptr_immortal_test.cpp、rich_exception_ptr_misc_test.cpp、rich_exception_ptr_fmt_test.cpp 以及基准 rich_exception_ptr_bench.cpp。

性能预期可对照 rich_error.md 的 Performance 一节(同为仓库文档):immortal rich error<5ns完成「复制构造→比较→析构」,get_exception查询2-5ns;动态 rich error 构造 / 析构约60ns;无 RTTI 优化下部分get_exception查询3-10ns;对照基线是exception_wrapper的构造30ns、复制~7ns、移动~1ns。设计文档给出的 epitaph 成本60ns(可降至<5ns)也与之一致。

小结

rich_exception_ptr的设计本质上是一次指针位级的信息论练习:用 8 字节同时表达「动态异常 / rich_error / 不可变错误 / 取消信号 / 小型值 / 内部 sigil」6 大类、8 种具体状态,并为最常见的运行时查询(is dynamic eptr?、is rich error?、has stopped?、is rich_error_base?)各留一条单指令路径。整套设计由三条硬约束驱动:

  1. constexpr友好:C++20 不允许 constexpr 指针位操作,因此 immortal 错误必须是000,常量求值时代码路径必须退化为「bits=0」;
  2. 对象身份取舍:legacy OC 保留动态对象身份,nothrow OC 用泄漏单例且保留指针以应对多 DSO;
  3. RTTI 规避:快速路径既要能回答「是」,也要能回答「肯定不是」,把 RTTI 死死限制在 unknown-type 慢路径上。

最终落地为bits_t枚举 + packed / separate 双存储 +with_underlying/get_exception/format_to一套完整位状态机(见 rich_exception_ptr_storage.h、rich_exception_ptr.h、rich_exception_ptr.cpp)。对使用者而言,这条设计链路的收益是具体的:folly::resulterror_or_stopped可以在不分配、不碰 RTTI的前提下承载富错误与取消信号,而folly::coro中「catch 一切」的用户代码再也无法无意中打断取消传播——这正是「灵活异常」与「廉价错误码」两种范式的合流点。

【免费下载链接】follyAn open-source C++ library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 14:00:08

好用还专业!盘点2026年最强的AI论文写作软件

一天写完毕业论文在2026年已不再是天方夜谭。2026年AI论文写作软件彻底改写学术写作规则&#xff0c;覆盖选题构思、文献综述、数据整理、格式排版等全流程场景&#xff0c;实测提速超300%&#xff0c;高效搞定论文不再是梦想。 一、全流程王者&#xff1a;一站式搞定论文全链路…

作者头像 李华
网站建设 2026/9/10 13:59:57

hyperframe:面向多传感器融合的时空数据组织架构

我从去年年底开始&#xff0c;一直在折腾一套多传感器数据采集与融合的中间件&#xff0c;就是在做自动驾驶测试车的数据闭环。真正让我决定深入研究 hyperframes 的导火索&#xff0c;是一次让人抓狂的激光雷达点云对齐问题&#xff1a;两个 Velodyne 和一台相机&#xff0c;明…

作者头像 李华
网站建设 2026/9/10 13:59:24

2026年AI 科研平台推荐,沁言学术一站式满足科研各类需求

引言&#xff1a;随着人工智能技术持续渗透科研领域&#xff0c;越来越多高校师生与企业研发人员开始借助 AI 科研平台简化日常工作流程。然而&#xff0c;市面上的工具五花八门&#xff0c;多数产品仅能处理单一环节的任务&#xff0c;难以适配完整的科研工作流。进入 2026 年…

作者头像 李华