C++ 标准库在过去十五年间一直在自我撤步,证据公开

Lobsters Hottest 新闻

摘要

一份详细的目录,列出了从 C++11 到 C++26 期间被正式弃用、非正式不推荐或由于 ABI 约束实际上已损坏但无法修复的 C++ 标准库特性。文章指出,C++ 委员会推出一系列替代品来替换其自身特性的模式始终如一,其中包含一个基准测试,显示 Rust 和 C++ 标准库容器之间的 P99 延迟差异高达 58 倍。

<p><a href="https://lobste.rs/s/7jt1ix/c_standard_library_has_been_walking">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/05 02:14

# C++ 标准库的十五年自我纠错史,证据全部公开 来源:https://hftuniversity.com/post/the-c-standard-library-has-been-walking-itself-back-for-fifteen-years-and-the-receipts-are-public ## C++ 标准库的十五年自我纠错史,证据全部公开 Sandor Dargo 本月关于 `std::copyable_function` 的博文(https://www.sandordargo.com/blog/2026/05/20/cpp26-copyable-function)末尾附有一张速查表。四个可调用包装器,每个都有一条推荐用法,而表格底部有一项足以让任何有经验的 C++ 工程师心头一紧: > `std::function`:遗留项。新代码中应避免使用。 `std::function` 于 C++11 中推出。委员会花了十五年时间推出了本应取代它的包装器。最新的 `std::copyable_function` 将于 C++26 中落地。而写在这新成员上方的推荐用法并非“当你需要可拷贝的可调用对象时使用它”,而是“不要使用最初的版本”。 这并不罕见。自 C++11 问世以来,C++ 委员会就一直在对自己推出的特性写出这样的判语。有时这份判语是正式的(一篇论文编号、标准中的弃用声明、一个周期后的移除)。有时则是每位资深工程师在第一天就会告诉每位新人的那句话(“永远别用那个,应该用这个代替”)。而有时,这份判语甚至无法写入标准,因为那个有问题的东西已被 ABI 兼容性锁死,于是它只能继续留在标准库中,作为每个教程都会默认推荐、每个生产代码库却都悄悄替换掉的存在。 这种模式如此一致,以至于值得为它建立一个专属目录,每一项旁边都附上论文编号——这样下次有人告诉你新 C++ 特性就是未来时,你就可以问问他们,预计要过多久才会出现下一篇论文将其弃用。 本文就是这份目录,分为三个层级。第一层是委员会已正式书面记录的纠错项。第二层是委员会尚未正式化、但“所有人都知道要避开”的纠错项。第三层则最为致命,因为它涉及的是几乎每个 C++ 代码库每天都在使用、而委员会却因无法打破 ABI 而无法修复的标准库容器。我们在自己进行的 Rust 对比 C++ 多书基准测试(https://hftuniversity.com/post/eviscerated-by-rust-58x-stdlib-handicap)中拿到了第三层的证据,该测试发现,在相同的工作负载和相同的隔离条件下,Rust 标准库与 C++ 标准库之间的 **P99 延迟相差 58 倍**,而这一差距被追溯到了三个委员会从未正式宣告有缺陷的容器。 ## 第一层:委员会已正式书面记录的纠错项 以下每一项都指向工作组实际采纳的论文。这些不是论点,而是白纸黑字的承认。 最干净利落的历史案例是 **`std::auto_ptr`**,这个 C++98 智能指针的“拷贝即移动”语义从它问世那天起就破坏了泛型代码和标准容器。于 C++11 中被弃用,C++17 中通过 **N4190 "Removing auto_ptr, random_shuffle(), And Old ``Stuff" (https://isocpp.org/files/papers/N4190.txt)**(Stephan T. Lavavej)移除。同一篇论文还一并移除了整个 C++98 的适配器动物园:`std::bind1st`、`std::bind2nd`、`std::ptr_fun`、`std::mem_fun`、`std::mem_fun_ref`、`std::unary_function`、`std::binary_function`、`std::pointer_to_unary_function`、`std::pointer_to_binary_function`。替代品是 lambda,一个早两个周期就已落地的语言特性,它让整个适配器框架变得毫无意义。 **`std::random_shuffle`** 与它们一同被移除,于 C++14 中弃用,C++17 中移除,由 `std::shuffle` 替代,因为原始版本依赖于 `std::rand` 和全局状态。 **动态异常规范**(`throw(X, Y)`)是 C++98 中声明函数可能抛出哪些异常的机制。于 C++11 中弃用,C++17 中通过 **P0003R5 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0003r5.html)**(Alisdair Meredith)移除。替代品:`noexcept`。作为 `noexcept(true)` 同义词的残余 `throw()` 一直存活到 C++20,然后被 **P1152 (https://wg21.link/p1152r4)** 最终消灭。整整十八年的标准文本,都在忙于撤销一个异常模型。 **`std::iterator`**,这个所有《Effective C++》书籍都教你继承的 C++98 基类,于 C++17 中通过 **P0174R2 (https://www.open-std.org/JTC1/SC22/wg21/docs/papers/2016/p0174r1.html)**("Deprecating Vestigial Library Parts in C++17",Meredith)被弃用。现已在 C++26 中通过 **P3365R1 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3365r1.pdf)** 提议移除。替代品是“自己定义那五个类型别名”,而这正是大多数工程师已经在做的事情,因为从 `std::iterator` 继承从来就没带来过任何实际好处。 **`std::aligned_storage`** 和 **`std::aligned_union`** 于 C++11 中推出,C++23 中通过 **P1413R3 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1413r3.pdf)**(CJ Johnson, Google)被弃用。论文的理由值得引用,因为它捕捉到了委员会对自己作品设计错误的承认。被弃用的类型需要 `typename ::type` 样板代码,需要 `reinterpret_cast` 来访问内容,将 `Len == 0` 视为未定义行为,并且不是 constexpr 的。替代品是“直接使用 `alignas(T) std::byte[sizeof(T)]`”,而这大概本应是标准一开始就该推出的方式。 **`std::not1`/`std::not2`** 以及 `unary_negate`/`binary_negate` 适配器于 C++17 中弃用,C++20 中移除,由 `std::not_fn` 替代(P0005 (https://wg21.link/p0005))。 **`std::get_temporary_buffer`** 和 **`std::raw_storage_iterator`** 于 C++17 中通过 P0174 弃用,C++20 中通过 P0619 移除。 **`register`** 关键字于 C++11 中弃用,C++17 中通过 **P0001 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0001r1.html)** 移除。 **三字符组**在 C++17 中被移除,结束了它们作为语言中一个累赘长达三十年的历史。 正式纠错目录中最尴尬的条目是 **C++11 垃圾回收接口**。委员会在 C++11 中推出了 `std::declare_reachable` 及其同类。没有哪个主流实现曾为这些入口点提供过真正的垃圾回收器。该接口在 C++23 中通过 **P2186R2 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2186r2.html)**(JF Bastien)被移除,历时十二年,从添加到移除,其间从未按宣传那样发挥过任何作用。十二年的标准文本,都在忙于撤销一个没人用过的特性。 然后是 **技术规范的回滚**,即标准化流程中委员会在合并前直接否决的部分。**Concepts TS** 为 C++20 重新设计(P0734 系列)。**Modules TS** 通过合并模块提案 **P1103R3 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1103r3.pdf)** 重新设计。**Coroutines TS** 在采纳前经过了大幅修改。**Reflection TS** 被完全否决,取而代之的是用于 C++26 的 **P2996 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2996r4.html)**,一个完全不同的基于值的方案。**Executors TS** 经历了多轮否决,最终成为 C++26 中的 **P2300 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2300r10.html)** sender/receiver。**Networking TS** 已被推迟了如此多次,以至于截至 C++26 它仍未进入标准。以上每一个,都是用不同方式写出的同一份承认:“之前的设计不管用,下面是下一个。” 最后是引发整篇文章的导火索。**`std::function`** 于 C++11 中推出。它的 `const operator()` 会调用非 const 的可调用对象,这是一个 const 正确性的缺陷,已在标准中存在了十五年,且无法在不破坏 ABI 的情况下修复。委员会的应对措施是在相邻周期中推出一系列替代品:**`std::move_only_function`** 于 C++23 通过 **P0288R9 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0288r9.html)**,**`std::copyable_function`** 于 C++26 通过 **P2548R6 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2548r6.pdf)**,以及 **`std::function_ref`** 于 C++26 通过 **P0792R14 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p0792r14.html)**。最初的 `std::function` 仍留在标准中。Dargo 的表格告诉你避开它。任何你能审计的现役 C++ 代码库也同样如此。 ## 第二层:委员会尚未正式化、但“所有人都知道要避开”的纠错项 这些特性仍留在标准中。没有一个是正式弃用的。行业内的每一位资深 C++ 工程师都会在上班第一天告诉每一位新人要避开它们。 **`std::regex`** 于 C++11 中推出。委员会自己的论文 **P1844R1 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1844r1.html)** 白纸黑字记录着:“C++ 委员会注意到 std::regex 性能相对于其他可用解决方案非常差”,并劝阻不要在实现上投入精力。标准库推出的这个特性,其首要的已记录品质就是委员会承认它太慢以至于不能用。替代品是 Hana Dusíková 的 **CTRE**(编译时正则表达式),标准化尝试在 **P1433R0 (https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2019/p1433r0.pdf)** 中。标准之外的替代品是 Boost.Regex、RE2 或 PCRE2。生产代码使用其中之一。标准的 `std::regex` 只存在于教程中。 **`std::async`** 于 C++11 中推出。返回的 future 的析构函数会阻塞,直到异步操作完成。**N3679 (https://open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3679.html)** 记录了由此导致的死锁陷阱。替代品是整个 sender/receiver 工作成果,最终在 C++26 中通过 P2300 落地,距 `std::async` 首次以有缺陷的状态推出已过去十五年。与此同时,每个在线的低延迟代码库都使用线程池、直接使用 `std::thread` 或平台特定的异步原语。`std::async` 存在于标准中,是为了让入门教科书有东西可写。 **``** 于 1998 年推出。它速度慢、受区域设置影响、格式化时不安全线程,并且产生的错误消息被广泛认为是 C++ 新人的入门仪式。委员会在 C++20 中推出了 **P0645 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0645r10.html)** `std::format`,在 C++23 中推出了 **P2093 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2093r14.html)** `std::print`/`std::println`。两者都没有弃用 ``。委员会不会写下那句话。但每一位在职工程师在代码审查中只要提交了 printf 风格的调试输出,就会被告知那句话。 **`std::list`** 是本层级的经典条目。Bjarne Stroustrup 在 2012 年的 GoingNative 主题演讲(https://learn.microsoft.com/en-us/shows/goingnative-2012/keynote-bjarne-stroustrup-cpp11-style)中展示了 `std::vector` 即使对于教科书上说的“在大型容器中间插入”的工作负载也胜过 `std::list`,因为线性扫描主导了性能,而指针追逐则惩罚了缓存。后续的博文标题带有刻意的强调:*Are lists evil?* (https://isocpp.org/blog/2014/06/stroustrup-lists)。答案是肯定的。`std::list` 没有被弃用。它存在于标准中。但每一位在职 C++ 工程师都被告知永远不要用它。 **`std::deque`** 是下一个条目。微软 STL 维护者有一个公开问题,microsoft/STL#147 (https://github.com/microsoft/STL/issues/147),标题是 "``: Needs a major performance overhaul",承认标准规定的块大小太小,设计需要在下次 ABI 断裂时重建。在此之前,`std::deque` 在每个标准库中都以同样的、人人皆知已有二十年之久的不良缓存行为发布。 **`std::valarray`** 于 1998 年作为一个具有表达式模板优化潜力的数值容器推出。优化工作从未完成。当前的 cppreference 文本指出,实现“似乎没有任何特殊代码”,它只是一个普通容器。Eigen、xtensor 和 Blaze 填补了这个空缺。`std::valarray` 存在于标准中是出于考古学原因。 **`std::vector`** 是著名的案例。Howard Hinnant 的 *On`vector`* (https://howardhinnant.github.io/onvectorbool.html) 是权威分析。位压缩存储确实有用;问题在于这个类型名字像 `std::vector` 的一个特化,但却悄悄无法满足 `std::vector` 的接口,因此接受 `vector&` 的泛型代码在 `T = bool` 时行为错误。修复方法是重命名,但委员会不会这样做,因此这个陷阱继续留在标准中。每位在职工程师都学会写 `std::deque` 或使用其他容器。 **`std::shared_ptr`** 的默认原子引用计数、**`std::initializer_list`** 与 `auto` 的交互、**`std::function`** 的实现定义的小缓冲区优化(在 libstdc++、libc++ 和 MSVC STL 上产生不同的运行时性能)、**`std::random_device`**(标准明确允许其可以是确定性的)。以上每一个都有命名的专家评论称其设计为缺陷,但没有一个是正式弃用的。它们就是你交付到生产环境的标准库,然后用你自己的辅助函数、你自己的约定和你自己的代码审查规则去绕开它们。 volatile 的传奇值得单独一行。**`volatile`** 在 C++20 中通过 **P1152R4 (https://wg21.link/p1152r4)** 针对复合操作和参数/返回值被弃用,在 C++23 中通过 **P2327R1 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2327r1.pdf)** 在嵌入式社区的反对后部分取消弃用,并在 C++26 中通过 **P2866R0 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2866r0.pdf)** 安排了进一步的弃用移除。委员会弃用了一个特性,受影响的社区提出反对,委员会撤销了弃用,现在又有一篇论文要撤销部分撤销。这就是对一个八个字母的关键字进行十五年标准工作所呈现的样子。 ## 第三层:每个人都在用、但没人能修复的容器 最致命的层级是那些 C++ 委员会无法弃用的标准库容器,因为它们是每本教科书都教的默认选项,而且 ABI 兼容性将它们牢牢冻结。这些是 C++ 初学者在第一天编写真实代码时就会使用的容器。其中有三个容器,按照其他所有人都已认同的标准来看,已经明确错误。我们有证据。 **`std::unordered_map`** 是典型情况。C++11 规范强制要求的桶和迭代器稳定性,实际上禁止了开放寻址法——而开放寻址法正是其他所有人十五年来已经标准化的缓存友好型哈希表架构。Matt Kulukundis 在 CppCon 2017 上的演讲 *Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step* 展示了 Google 的 SwissTable 架构比 `std::unordered_map` 快大约 3 倍。Folly 的 F14、Boost 的 `unordered_flat_map`、`ankerl::unordered_dense` 以及 Martin Ankerl 的其他变体,都以类似的优势胜出。Rust 的 `HashMap`,它使用的是 hashbrown SwissTable 移植版,在标准库中默认就是缓存友好型架构。C++ 委员会在 C++23 中通过 **P0429R9 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p0429r9.pdf)** 添加了 `std::flat_map` 和 `std::flat_set`,但无法修复 `std::unordered_map` 本身。默认的选项仍然是有缺陷的。 **`std::map`** 和 **`std::set`** 是红黑树,基于节点,每个节点一次堆分配,每次遍历都涉及指针追逐。B 树在速度和缓存友好性上已经打败了红黑树。

相似文章

C++26:标准库强化

Lobsters Hottest

C++26 引入了标准化的库强化机制,用于在运行时捕获常见的未定义行为(如越界访问)。基于 Google 的生产经验,此举仅带来 0.30% 的性能开销,同时将段错误减少了 30%。

C++26 发布了一个无人要求的 SIMD 库

Lobsters Hottest

文章批评了 C++26 中的新 std::simd 库,认为它比标量循环慢,编译速度慢,并且被自动向量化器和 Google Highway 等替代库超越,质疑其在经过十年标准化过程后的价值。