从零开始编写你自己的 C++ 标准库:Pystd 实践
Nibble Stew
Jussi Pakkanen 的开发思考集。有些人可能知道他是 Meson 构建系统的创建者。jpakkane at gmail dot com
2025年3月24日 星期一
从零开始编写你自己的 C++ 标准库
C++ 标准库(也称为 STL)无疑是一项惊人的工作。它的范围、性能和令人难以置信的向后兼容性,是世界上许多最优秀的程序员几十年努力的成果。 我向所有为之做出贡献的人致敬。
所有这些并不是说它没有问题。最大的问题是绝对糟糕的编译时间,但不可读性,以及由严格的向后兼容性造成的某些次优情况也名列前茅。事实上,可以认为,人们真正不喜欢 C++ 的地方,大部分是 STL 的特性,而不是语言本身。幸运的是,使用 STL 不是强制性的。如果你足够疯狂,你可以完全禁用它,并以最好的 Bender 风格构建你自己的标准库。
作为一名自愿失业的开源开发者,主要优势之一是如果你愿意,你可以做所有这些事情。 没有不称职的中层管理者在你肩上盘旋,以确保你“产生直接的客户价值”,而不是“在无用的润色上浪费时间,而这些润色不会产生直接的客户价值”。
这是_我的_时间,我想浪费就浪费!
其中包含什么?
标准库最大的设计问题是范围和 API 的“感觉”。与其花时间在设计上,不如直接抄。因此,当有疑问时,阅读 Python stdlib 文档并复制它。因此,该库的名称为 pystd。
测试应用
为了使范围有意义,我们首先只编写足够的 stdlib 来构建一个应用程序,该应用程序读取文本文件,将其验证为 UTF-8,将内容拆分为单词,统计每个单词在文件中出现的次数,并打印所有单词及其出现的次数,按计数递减排序。
这至少需要:
- 文件处理
- 字符串
- UTF8 验证
- 哈希表
- 向量
- 排序
脱掉辅助轮
代码可在 这个 GitHub 仓库 中找到,供那些想在家尝试的人使用。
禁用 STL 相当容易(至少在 Linux+GCC 上),只需要这两个 Meson 语句:
add_global_arguments('-nostdinc++', language: 'cpp') add_global_link_arguments('-nostdlib++', '-lsupc++', language: 'cpp')
根据 Stackoverflow 的说法,supc++ 库是 GCC 实现核心语言特性所需的支持库。现在 stdlib 已关闭,是时候用木棍、石头和胶带实现一切了。
结果
一旦你实现了上面讨论的所有内容以及诸如哈希框架之类的辅助内容,主应用程序看起来就像这样。
最终结果在 Valgrind 和 Asan 中都是干净的。有一块未释放的内存,但它来自 supc++。实现中可能存在 UB。但它应该是好的 UB,如果它实际上不起作用,将会破坏整个 Linux 用户空间,因为_一切_都依赖于它“按预期”工作。
所有这些在库本身中只用了不到 1000 行代码(包括一个实际上没有使用的正则表达式实现)。 相比之下,仅仅从 STL 中包含 vector 就会引入 2.7 万行代码。
与 STL 版本的比较
将此代码转换为使用 STL 相当简单,只需要更改一些类型并微调 API。 主要区别在于 STL 版本不验证输入是否为 UTF-8,因为它没有内置函数来执行此操作。 现在我们可以比较两者。
在我的小型测试文件中,两者的运行时均为 0.001 到 0.002 秒。 Pystd 的速度并不比 STL 版本慢多少,这对于我们的目的来说已经足够了。它几乎肯定会更糟糕,因为它没有进行任何性能方面的工作。
使用 -O2 编译 pystd 版本需要 0.3 秒,而 STL 版本需要 1.2 秒。 这些测量是在 Ryzen 7 3700X 处理器上进行的。
STL 的未剥离可执行文件大小为 349k,pystd 为 309k。剥离后的大小分别为 23k 和 135k。 大约 100k 的 pystd 可执行文件来自 supc++。 在 STL 版本中,这可能来自 libstdc++ (在这台机器上,它占用 2.5 MB)。
完美的 ABI 稳定性
设计标准库非常困难,因为你永远无法真正更改它。 有人,在某个地方,依赖于其中的每一个错误功能,因此它们永远无法更改。
Pystd 的设计既支持完美的 ABI 稳定性,_又_使其能够在将来以任意方式更改。 如果你从头开始,这最终会变得相当简单。
上面的示例代码使用了 pystd 命名空间。 它实际上并不存在。 相反,它在 cpp 文件中定义如下:
#include <pystd2025.hpp>
namespace pystd = pystd2025;
在 pystd 中,所有代码都在一个带有年份的命名空间中,并存储在具有相同年份的头文件中。 想法是,然后,每年创建一个新版本。 这涉及将所有 stdlib 头文件复制到具有新年份的文件中,并使用正则表达式匹配命名空间声明。 旧代码现在被永久冻结(除了错误修复),而新代码可以随意更改,因为_没有现有代码行依赖于它_。
最终用户现在可以选择何时更新其代码以使用较新的 pystd 版本。 更好的是,如果有一个无法更新的旧库,则任何旧版本都可以并行使用。 例如:
pystd2030::SomeType foo;
pystd2025::SomeType bar(foo.something(), foo.something_else());
因此,如果没有代码被更新,一切都会继续工作。 如果所有代码都一次更新,一切都会工作。 如果只有部分代码被更新,仍然可以通过一些胶水代码使其工作。 这会将维护负担放在那些项目无法更新的人身上,而不是世界上所有其他开发人员身上。 这应该是这样,并且也会促使那些依赖项损坏的人花费更多的精力来修复它们。