29. C++17/20/23 内存管理新特性:std::pmr、std::observer_ptr、std::out_ptr、std::inout_ptr
聊到现代C++的内存管理,很多人第一反应就是shared_ptr和unique_ptr。没错,它们是基石。但C++17、20、23这三个标准,又给咱们塞了不少新玩具。说实话,我刚看到std::pmr的时候,第一反应是「这玩意儿能解决啥实际问题?」后来在项目里被内存碎片折磨得够呛,才明白它的价值。
今天咱们就一口气把这四个特性捋清楚:std::pmr、std::observer_ptr、std::out_ptr、std::inout_ptr。它们各自解决不同场景下的痛点,但都指向同一个目标——让内存管理更可控、更安全。
一、std::pmr:多态分配器
std::pmr(Polymorphic Memory Resource)是C++17引入的一套机制。说白了,它让你能自定义容器(比如std::vector、std::string)的内存分配策略,而且这些策略可以在运行时切换。
为什么需要这个?我举个例子。你有一个高频交易系统,里面大量使用std::string。默认分配器每次都要向操作系统要内存,碎片化严重,性能抖动很大。这时候,你可以用一个std::pmr::monotonic_buffer_resource,预分配一大块内存,然后所有字符串都在这个缓冲区里分配,用完一次性释放。速度飞快,碎片为零。
核心组件一览:
std::pmr::memory_resource:抽象基类,定义allocate和deallocate接口。std::pmr::monotonic_buffer_resource:只增不减的分配器,适合短期大量分配的场景。std::pmr::pool_resource:固定大小块的内存池,减少碎片。std::pmr::synchronized_pool_resource:线程安全的池版本。std::pmr::unsynchronized_pool_resource:非线程安全,性能更高。std::pmr::polymorphic_allocator<T>:适配容器的分配器包装。
来看一段实际代码:
#include <memory_resource>
#include <vector>
#include <string>
#include <cstddef>
void process_with_pmr() {
// 预分配 1MB 缓冲区
std::array<std::byte, 1024 * 1024> buffer;
std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size());
// 所有字符串都在 pool 中分配
std::pmr::vector<std::pmr::string> vec(&pool);
for (int i = 0; i < 10000; ++i) {
vec.emplace_back("hello world");
}
// 离开作用域时,pool 一次性释放所有内存
// 没有碎片,没有多次 free
}
我个人习惯在写中间件或游戏引擎时,大量使用pmr。尤其是那些「请求-响应」模式的服务,每个请求的生命周期很短,用monotonic_buffer_resource简直完美。你想想看,每次请求来了,从预分配池里拿内存,处理完直接扔掉整个池子,连析构都省了。
小技巧:如果你需要线程安全,但又不希望全局锁,可以给每个线程分配一个独立的monotonic_buffer_resource。这样既避免了锁竞争,又享受了池化分配的好处。
二、std::observer_ptr:非拥有型观察者
C++20引入了std::observer_ptr。它的作用很简单:表示一个「只观察、不拥有」的指针。说白了,它就是一个裸指针的包装,但语义更清晰。
你可能会问:「直接用裸指针不就行了?」嗯,问题在于裸指针的语义太模糊了。你看到一个Widget*,你不知道它是不是拥有权。是new出来的?还是别人传进来的?该不该delete?
observer_ptr明确告诉你:我只是看看,不负责销毁。它不能调用delete,也不能被delete。它就是一个安全的、非拥有的观察者。
#include <memory>
class Widget {
public:
void do_something() const { /* ... */ }
};
void process_widget(std::observer_ptr<const Widget> ptr) {
if (ptr) {
ptr->do_something();
}
// 不能 delete ptr,编译错误
}
int main() {
auto w = std::make_unique<Widget>();
process_widget(std::observer_ptr<const Widget>(w.get()));
// w 离开作用域时自动销毁
}
我在项目中遇到过一种情况:一个类内部持有另一个对象的指针,但那个对象的生命周期由外部管理。以前我用裸指针,每次代码审查都要解释「这个指针不负责释放」。后来换成observer_ptr,语义一目了然,审查也快了。
注意:observer_ptr不能防止悬空指针。如果被观察的对象已经被销毁,你仍然会访问到野指针。它只解决「所有权语义」的问题,不解决生命周期安全问题。使用时仍需配合生命周期管理策略。
三、std::out_ptr 与 std::inout_ptr:C风格API的桥梁
C++23带来了std::out_ptr和std::inout_ptr。这两个工具专门用来处理那些「输出参数是指针的指针」的C风格API。比如很多Windows API、OpenGL、或者老旧的C库,它们会这样写:
// 典型的C风格输出参数
int create_buffer(void** out_buffer, size_t* out_size);
以前我们怎么处理?先声明一个裸指针,调用API,再把它包装成智能指针。代码又臭又长,还容易漏掉错误处理。
std::out_ptr和std::inout_ptr就是来解决这个问题的。它们让你直接把unique_ptr或shared_ptr传给C风格API,自动完成「取地址→调用→包装」的过程。
区别很简单:
out_ptr:API会分配新对象,覆盖原有指针。调用前原有指针会被重置。inout_ptr:API可能修改也可能重新分配对象。调用前原有指针不会被重置,API可以复用旧对象的内存。
#include <memory>
#include <cstdio>
// 模拟一个C风格API
extern "C" int create_buffer(void** buf, size_t* size) {
*buf = malloc(1024);
*size = 1024;
return 0;
}
void modern_usage() {
std::unique_ptr<void, decltype(&std::free)> buffer(nullptr, std::free);
size_t size = 0;
// C++23 写法:直接传 unique_ptr
if (int err = create_buffer(std::out_ptr(buffer), &size)) {
// 错误处理
return;
}
// buffer 现在拥有分配的内存,离开作用域自动 free
// 不需要手动管理
}
说实话,我第一次看到out_ptr的时候,觉得这玩意儿有点鸡肋。直到有一次我接手一个遗留系统,里面全是这种C风格API,手动包装了上百处。改完以后,代码量减少了三分之一,而且再也不用担心忘记free了。
避坑指南:使用out_ptr时,确保智能指针的删除器与API的释放方式匹配。如果API用free,删除器就用std::free;如果API用delete,删除器就用std::default_delete。我曾经因为混用了malloc和delete,排查了半天内存泄漏。
四、知识体系总览
下面这张图把四个特性的定位和关系梳理了一下。你可以看到,它们分别覆盖了「分配策略」、「所有权语义」、「C互操作」三个维度。
五、实际项目中的选择建议
说了这么多,到底什么时候用哪个?我根据自己的经验,整理了一个简单的决策表:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 大量短生命周期对象分配 | std::pmr::monotonic_buffer_resource |
零碎片,一次释放,性能极高 |
| 需要线程安全的池分配 | std::pmr::synchronized_pool_resource |
内置锁,开箱即用 |
| 只想观察对象,不拥有 | std::observer_ptr |
语义清晰,防止误删除 |
| 调用C风格API获取新对象 | std::out_ptr |
自动包装成智能指针 |
| 调用C风格API可能复用对象 | std::inout_ptr |
保留原有值,灵活处理 |
嗯,这里要注意一点:不要为了用新特性而用新特性。如果你的项目全是现代C++,没有C互操作需求,那out_ptr可能根本用不上。同样,如果你的分配模式很简单,pmr带来的复杂度可能得不偿失。
我个人习惯是:先分析项目的内存分配热点,再决定是否引入pmr。对于observer_ptr,我建议在接口设计中强制使用,尤其是那些接受指针参数的函数。至于out_ptr和inout_ptr,只有在对接遗留代码时才需要,新项目尽量用纯C++接口。
核心总结:
std::pmr让你掌控分配策略,适合性能敏感场景。std::observer_ptr让所有权语义一目了然,适合接口设计。std::out_ptr和std::inout_ptr是C互操作的救星,减少手动管理。
这四个特性,每一个都解决了一个具体的痛点。用好了,你的代码会更安全、更高效、更易维护。
公众号:蓝海资料掘金营,微信deep3321