对C++内存池的进一步思考
关于C++内存池的更多思考
September 30, 2025
nullprogram.com/blog/2025/09/30/
(作者目前正在美国寻求就业机会。)
2025年10月更新:进一步改进。 Patrice Roy的新书《C++ Memory Management》让我对对象的生命周期有了更清晰的认识。C++在生命周期方面比C更严格,而在C中是安全的、教科书式的常见内存管理,在C++中则不那么安全——这一点我以前认识得不够深刻。这本书还提出了一种被严重削弱的内存池分配形式,以至于它无法享受到任何好处。(尽管书的后半部分在其他方面很精确,但也充斥着缺乏适当检查的整数溢出,并且在接近结尾处有一些指针溢出,导致检查无效。)不过,我很感激这些新的见解,这让我重新审视了自己的C++内存池分配。在这种新的视角下,我发现自己之前也犯了一些细微的错误!
对于大多数C++程序员来说可能出乎意料,但对于语言专家(language lawyers)来说并不奇怪:直到最近,习惯性的C内存分配在C++中都是不合法的:
|
|
这段程序为一个对象分配了内存,但从未开始其生命周期。在没有生命周期的情况下进行赋值是无效的。指针转换在C++中更可疑,并且由于生命周期语义,在许多情况下都表明代码不正确。(需要明确的是,我并非支持这些语义,而是基于实际情况进行推理。)C++20为malloc及其相关函数开辟了特殊的例外,但解决这类问题的普遍方法是使用全新的start_lifetime_as(及类似功能)、稍早的construct_at,或者经典的placement new。它们都用于开始生命周期。最后一种方法如下所示:
|
|
这作为C/C++多语言程序并不好,不过根据不同的旧语义,没有宏本来也不可能实现。宏基本上算是一种“作弊”。一个重要细节是:修正后的版本没有类型转换,并且返回的是new的结果。这很重要,因为只有new返回的指针被赋予了指向新生命周期的身份,而不是r。没有副作用影响r的来源(provenance),就语言而言,r仍然指向原始内存。
考虑到这一点,让我们重新审视我上次的内存池,它不一定从最近的变更中受益,因为它不属于特殊的C标准库函数:
|
|
嘿,看,placement new!我这样做是为了提供一个更好的接口,但我也碰巧正确地开始了生命周期。只是它返回了错误的指针。这个分配器丢弃了被赋予新生命周期的指针。两个指针具有相同的地址但不同的来源。这很重要。但是我多次调用了new,那我该如何修复这个问题呢?用数组new,没错。
|
|
哇……这实际上好多了。没有显式类型转换,没有循环。为什么我一开始没想到呢?问题在于我无法像emplace风格那样转发构造函数参数——这正是我在完美转发方面遇到的麻烦——但这样其实更好。多次转发是不安全的,使用new[]能更清楚地看到这一点。
需要注意:这仅在C++20及以后版本中有效,并且严格来说需要使用operator new[](size_t, void *)。任何其他形式的placement new[]可能需要数组开销——例如,它会预先添加一个数组大小,以便delete[]能够运行非平凡的析构函数——这是不可知的,因此不可能提供或正确对齐。placement new[]的开销当然是荒谬的,但截至撰写本文时,所有三大主流C++编译器都这样做,实质上破坏了自定义的placement new[]。
既然我在思考生命周期,那另一头呢?我的内存池按设计不调用析构函数,并在技术上仍然存活的对象之上开始新的生命周期。这是未定义行为吗?据我所知,这是允许的,即使对于非平凡的析构函数也是如此,但需要注意的是它可能会泄漏资源。在这种情况下,资源是由内存池管理的内存,所以这当然没问题。
因此,解决指针来源问题也产生了更好的定义。阅读那本书真是带来了很棒的结果!在研究过程中,我注意到Jonathan Müller(他曾亲自对我之前的文章提出了很好的建议和反馈)在几周后也谈到了生命周期。我推荐这两者。
cpp
对此文章有评论吗?请通过发送电子邮件至~skeeto/public-inbox@lists.sr.ht到我的公共收件箱开始讨论,或查看现有讨论。
«
使用字符串驻留进行分层字段排序
»
关于内存池和非平凡析构函数的思考
null program Chris Wellons
wellons@nullprogram.com (PGP) ~skeeto/public-inbox@lists.sr.ht (查看)
索引 标签 订阅 关于 工具 玩具 GitHub
除非另有说明,本博客上的所有信息均在此释放到公共领域,无任何权利保留。