29. C++17/20/23 内存管理新特性:std::pmr、std::observer_ptr、std::out_ptr、std::inout_ptr

聊到现代C++的内存管理,很多人第一反应就是shared_ptrunique_ptr。没错,它们是基石。但C++17、20、23这三个标准,又给咱们塞了不少新玩具。说实话,我刚看到std::pmr的时候,第一反应是「这玩意儿能解决啥实际问题?」后来在项目里被内存碎片折磨得够呛,才明白它的价值。

今天咱们就一口气把这四个特性捋清楚:std::pmrstd::observer_ptrstd::out_ptrstd::inout_ptr。它们各自解决不同场景下的痛点,但都指向同一个目标——让内存管理更可控、更安全。

一、std::pmr:多态分配器

std::pmr(Polymorphic Memory Resource)是C++17引入的一套机制。说白了,它让你能自定义容器(比如std::vectorstd::string)的内存分配策略,而且这些策略可以在运行时切换。

为什么需要这个?我举个例子。你有一个高频交易系统,里面大量使用std::string。默认分配器每次都要向操作系统要内存,碎片化严重,性能抖动很大。这时候,你可以用一个std::pmr::monotonic_buffer_resource,预分配一大块内存,然后所有字符串都在这个缓冲区里分配,用完一次性释放。速度飞快,碎片为零。

核心组件一览:

  • std::pmr::memory_resource:抽象基类,定义allocatedeallocate接口。
  • 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_ptrstd::inout_ptr。这两个工具专门用来处理那些「输出参数是指针的指针」的C风格API。比如很多Windows API、OpenGL、或者老旧的C库,它们会这样写:

// 典型的C风格输出参数
int create_buffer(void** out_buffer, size_t* out_size);

以前我们怎么处理?先声明一个裸指针,调用API,再把它包装成智能指针。代码又臭又长,还容易漏掉错误处理。

std::out_ptrstd::inout_ptr就是来解决这个问题的。它们让你直接把unique_ptrshared_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。我曾经因为混用了mallocdelete,排查了半天内存泄漏。

四、知识体系总览

下面这张图把四个特性的定位和关系梳理了一下。你可以看到,它们分别覆盖了「分配策略」、「所有权语义」、「C互操作」三个维度。

C++17/20/23 内存管理新特性总览 std::pmr (C++17) 多态分配器,运行时切换分配策略 解决:内存碎片、性能抖动 典型:monotonic_buffer_resource std::observer_ptr (C++20) 非拥有型观察者,语义清晰 解决:裸指针所有权语义模糊 不能 delete,只观察不拥有 std::out_ptr (C++23) 输出参数适配,API分配新对象 解决:C风格API与智能指针的鸿沟 调用前重置原有指针 std::inout_ptr (C++23) 输入输出参数适配,可复用内存 解决:API可能修改或重新分配 调用前不重置,保留原值 共同目标:让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_ptrinout_ptr,只有在对接遗留代码时才需要,新项目尽量用纯C++接口。

核心总结:

  • std::pmr让你掌控分配策略,适合性能敏感场景。
  • std::observer_ptr让所有权语义一目了然,适合接口设计。
  • std::out_ptrstd::inout_ptr是C互操作的救星,减少手动管理。

这四个特性,每一个都解决了一个具体的痛点。用好了,你的代码会更安全、更高效、更易维护。


公众号:蓝海资料掘金营,微信deep3321