Skip to content

标准化真相:The Beman Project 与一份能跑的参考实现

上一篇讲完移动语义的坑。这一篇跳出来,看这场演讲最让笔者兴奋的部分:标准化到底是怎么进行的,以及 The Beman Project 在里面扮演的角色。最后把整条线收一下。

先用汇编验证一件事:赋值就是指针拷贝

前面几篇咱们反复说 optional<T&> 的赋值等价于指针拷贝。口说无凭,看汇编。写个函数只做一次 optional<int&> 赋值,-O2 编译:

cpp
// assign_codegen.cpp
#include <optional>
void assign_opt(std::optional<int&>& a, const std::optional<int&>& b) {
    a = b;
}
bash
g++ -std=c++26 -O2 -S assign_codegen.cpp -o assign_codegen.s

assign_opt 的函数体拎出来:

asm
_Z10assign_optRSt8optionalIRiERKS1_:
        movq    (%rsi), %rax      # 从 b 读 8 字节,就是一个指针
        movq    %rax, (%rdi)      # 写到 a
        ret

两条 movq,从 b 读一个指针,写到 a,返回。没有任何函数调用,没有分支。这就是 Steve Downey 说的"赋值通过单一函数完成,从编译器优化角度看非常透明"。标准把语义定得足够简单,实现者就有空间把代码生成做到极致,编译器能内联、能做死代码消除,所有它擅长的事都干得了。

WARNING

有一点要提醒。空状态的 optional<T&> 解引用,和空指针解引用的后果是一样的,未定义行为,而且编译器不一定会给您任何警告。千万别觉得 optional 会保护您,它只是把"有没有值"这个信息显式化了,您不检查就解引用,该崩还是崩。上一场演讲里提到的空指针和无效指针的可怕问题,这里照样适用。

The Beman Project 到底解决什么

这场演讲最让笔者兴奋的部分来了。

The Beman Project 是 2024 年 CppNow 上启动的一个项目,名字来自 Boost 的创始人之一 Beman Dawes。笔者一开始以为是编译器优化项目,后来才搞明白,它是个参考实现项目:针对正在被提议进入 C++ 标准库的组件,提供一份教科书级别的干净实现。

您可能觉得这没什么大不了,标准库实现不是 libstdc++、libc++、MSVC STL 都有吗,随便挑一个看不行吗?

不行。这里有个笔者以前完全没意识到的关键区别:厂商的实现里塞了大量历史遗留的东西。

举个笔者自己的例子。之前想搞清楚 std::optional 某个构造函数的精确行为,去翻 libstdc++ 源码,结果发现里面有不少笔者根本没见过的构造函数重载。有些是为兼容旧标准,有些是为配合特定编译器扩展,有些注释里写着"这个可能不需要但删了会破坏某些内部 ABI"。笔者当时就懵了,到底该看哪个?哪个才是"标准说的那个"?

The Beman Project 要解决的就是这个。它要做一份干净的、只包含标准提案内容的实现,没有历史包袱,没有厂商私货,没有"当年犯的错现在不敢删"的东西。LEWG(库演进工作组)明确说了,这个实现里只会有一组名字,就是标准最终指定的那些。

为什么"有个能跑的东西"这么重要

这部分笔者觉得是整场演讲里最有价值的。Steve Downey 说了一句话,大意是他还不够聪明,没法在没有实际实现参考的情况下写出标准用语。

您敢信?写标准提案的人说自己不够聪明?但仔细一想,这话挺实在。笔者写技术文档也常这样,觉得想清楚了,文字也漂亮,一动手写代码就发现这里有个边界没考虑,那里有个交互没预料。

标准用语也一样。你以为可以把某个函数标记成 const,纸面上完全合理,一实现就发现五个测试用例挂了。这五个测试用例覆盖的场景,可能是你写提案时根本想不到的,比如和某个其他标准组件的组合,或者某个特定模板上下文里的推导结果。

如果这些发现发生在提案被接受、标准发布之后呢?那就惨了,得再走一轮完整的会议周期去修,可能等好几年。但如果在参考实现里就发现呢?直接改,改完跑测试,五分钟就知道行不行。Steve Downey 说他把某个东西改成 const 之后五个测试挂了,于是知道这主意不怎么样,或者反过来,主意不错只是测试用例本身有问题。不管哪种,都比经历整整一轮会议周期之后回来说"哦对了,你让我做的那件事,结果发现不行"要好得多。

他举了个好例子。东京会议上有人提议让 optional 变成 range,这个提案到底可不可行?不是靠嘴说,不是靠在白板上画类型推导树。他的做法是把提议的函数加进 Beman 的实现里,拿出一批之前为处理零个或一个元素 range 写的测试用例一跑,全过。这就把讨论从"这到底行不行得通"变成了"我们到底想不想这么做"。技术障碍被排除,剩下的是设计品味的问题。这种工作方式比在邮件列表里来回辩论一百轮高效多了。

WARNING

这也意味着基于 The Beman Project 写的代码,ABI 会不稳定。今天能编的代码,下个月可能编不过,因为委员会可能改了某个函数名、把某个参数改成 const、把某个构造函数标成 explicit。这不是 bug,是重点。他们就是要通过这种折腾来验证标准用语的正确性。所以 Beman 的定位是标准化参考,不是让您在生产里依赖的稳定库。

大而全还是小而专

Steve Downey 提到,C++ 标准库倾向于提供一个"大而全"的东西,而不是像其他语言生态那样提供几个相近但不同的类型让您选。optional 变成 range 之后,标准里就只有一个"类似 optional 的东西",而不是两三个针对不同用例优化的近亲类型。这跟 std::string 的思路一脉相承,把所有功能塞进一个接口,您不需要纠结"我到底该用这个还是用那个"。

笔者对这种思路是矛盾的。一方面理解,选择少确实降低认知负担,尤其对新手。另一方面,std::string 的教训就摆在那,接口太臃肿,很多函数存在价值存疑,因为要兼顾所有用例,每个用例都做不到最优。

其他语言怎么做?Rust 有 Option<T> 和各种零成本抽象的迭代器适配器,Go 有"零值"哲学根本不需要 optional,Swift 有 Optional 但它的语言层面集成都和 C++ 不同。每个生态都有自己的取舍,C++ 的取舍就是"给您一个万能的",代价就是"这个万能的可能在每个具体场景里都不是最优的"。作为使用者,至少得意识到这个取舍存在。

optional&lt;T> 和 optional&lt;T&> 必须是一个整体

聊完 The Beman Project,看一个更深层的问题,也是笔者一开始没意识到的:如果 optional<T&> 进了标准库,它必须和已有的 optional<T> 无缝协作。

最典型的场景是 monadic 操作(transformand_then 这些,C++23 起的 P0798)。假设您有一个 optional<string>,对它调用 transform,传入的函数返回 optional<string&>,这时候会发生什么?您希望自动展平,但如果 optional<T>optional<T&> 的实现是割裂的,它们互相不认识,这里大概率返回嵌套的 optional,您就得手动 flatten,非常反直觉。

流行的 polyfill 库 tl::optional 不存在这个问题,因为它根本不支持 optional<T&>,monadic 函数不需要处理这种跨类型交互。但一旦进了标准库,optional<T>optional<T&> 必须是一个连贯的集合(Steve Downey 用的词是 coherent set),它们得知道对方的存在,在类型转换、monadic 操作、比较运算所有场景下都做正确的事。这也是为什么不能简单在现有 optional 上打补丁,得从设计层面把 TT& 两种特化统一起来。

顺便说说 reference_wrapper 的定位。它不会消失,在 tuple 和 bind 的场景下仍然有用,标准库里已经有它,肯定有人依赖。但它不是也不应该是"可选引用"的答案。它最早是给 tuple 的 DSL 做适配的,API 有怪癖,它自带的隐式转换到 T& 在重载决议场景下会咬人,它完全不关心悬空问题。optional<T&> 是个全新的、从语义出发的设计,不是在 reference_wrapper 上修修补补能做到的。底层存储可能差不多,都是个指针,但语义和安全保证是完全不同层次的,就像有裸指针了还要 unique_ptr 干嘛一样。

写在最后

把整条线收一下。std::optional 从 2005 年提出,2017 年值版本进 C++17,引用版本因为 assign-through 和 rebind 的争吵被搁置,中间走了很多弯路,最终由 P2988 在 2025 年 Sofia 会议投票通过,进入 C++26。它内部就是一个带约束的指针,赋值永远重绑定,const 是浅层的,value_or 总返回值,operator* 对引用版本不传播移动。

这些决定每一个单独看都有点反直觉,但您只要记住一件事:咱们在对指针建模。赋值改的是指针的指向,const 只锁指针本身,移动不穿透到被指向的对象。指针的规则平移过来,再注意几个边界情况,整个 optional<T&> 就说得通了。

至于它为什么花二十年,不是技术上做不到,是这些边界情况的语义太容易让人踩坑,需要一个精心设计的提案把所有细节理清楚,还需要 The Beman Project 这样一份能跑的参考实现来验证标准用语真的站得住脚。标准化不是一群人在会议室里吵架然后甩出一份文档,它是"想清楚、写下来、实现出来跑一遍、不行再改"的循环。这么朴素的道理,放在标准化这么高大上的场景里,居然也是最有效的。

v0.10.0-6-gbcee94e · bcee94e · 2026-08-20