C++ 的 out_ptr 和 inout_ptr

1 问题的起因

1.1 T**T&*

​ C++ 的智能指针可以通过 get() 和 * 的重载得到原始指针 T*,遇到这样的 C 风格的函数的时候:

void Process(Foo *ptr);

std::unique_ptr<Foo> sp = ...;

Process(sp.get()); //调用 Process 函数

Process() 函数以非抢夺的方式使用 Foo *,大家都相安无事。但是,C++ 的智能指针都是非侵入式的智能指针,如果要修改指针自身,则只能通过显式的手段,比如 reset() 成员函数。所以 C++ 的智能指针遇到这样的 C 风格的函数时,就很棘手:

bool MakeObject(Foo **pptr);
bool RefreshObject(Foo& *pptr);

没有 const 约束,大家都明白这样的接口是返回一个指针或重置一个指针。遇到这种情况,C++ 只能分步做这个事情:

std::unique_ptr<Foo> sp;
Foo *ptr = nullptr;
if(MakeObject(&ptr)) {
    sp.reset(ptr);
}

但是这么做就有个问题,在 MakeObject() 函数完成一个 Foo * 的初始化,到外部 sp 托管这个指针之间就有一段间隙,在这期间发生任何异常,Foo 对象和 Foo 对象分配的存储空间就随风而去,自由地飞翔。

1.2 传统解决方案

​ Raymond Chen 在资料 [2] 中给出了一种代理类的解决方法,通过代理类在智能指针和对象指针之间建立一个桥梁。就上一节的 MakeObject() 函数的使用,借助于 Raymond Chen 的思路,我们可以这样设计一个代理类:

template<typename T>
struct UniquePtrProxy {
    UniquePtrProxy(std::unique_ptr<T>& output)
        : m_output(output) { }

    ~UniquePtrProxy() { m_output.reset(m_rawPtr); }
    operator T** () { return &m_rawPtr; }

    UniquePtrProxy(const UniquePtrProxy&) = delete;
    UniquePtrProxy& operator=(const UniquePtrProxy&) = delete;

    std::unique_ptr<T>& m_output;
    T *m_rawPtr = nullptr;
};

UniquePtrProxy 类通过构造函数关联一个 T 类型的智能指针,通过对 T** 的重载,使得这个类可以适配需要 T** 参数的场合,给出的 T** 可被修改,并且在代理销毁的时候 reset 关联的智能指针。

std::unique_ptr<Foo> spFoo;
if (MakeObject(UniquePtrProxy<Foo>(spFoo))) {
    std::cout << "value = " << spFoo->GetValue() << std::endl;
}

​ 貌似天衣无缝,即使 MakeObject 内部出现了异常,只要 Foo * 的指针是有效的,利用 RAII,UniquePtrProxy 都可以正确设置智能指针,从而避免资源泄露。不过,正如 Raymond Chen 在资料 [2] 中给出的例子那样,用户如果这样写代码就很郁闷了:

if (MakeObject(UniquePtrProxy<Foo>(spFoo)) && spFoo) {
    std::cout << "value = " << spFoo->GetValue() << std::endl;
}

这其实是一种很合理的做法,在使用智能指针之前先检查一下指针的有效性。但是 if 表达式中的整个求值完成之前,UniquePtrProxy 创建的临时对象在 MakeObject() 函数调用完成后,会像鬼魂一样继续飘荡一段时间,当检测 spFoo 的时候,它还没有析构,spFoo 还没有被 reset,结果就是 if 代码块永远也走不到。

​ 资料 [4] 提出一种侵入式智能指针,允许直接修改内部指针,比如资料 [1] 的 retain_ptr 的实现。但是更多的库采用的是智能指针适配器的方式,通过适配器完成智能指针的侵入式操作。典型的就是 WRL 库的 ComPtrRef ,它其实是对 ComPtr 的适配器。C++ 的标准库采用的就是适配器方案。

2 C++ 23 的智能指针适配器

2.1 out_ptr_t 和 inout_ptr_t

​ C++ 23 引入了两个智能指针适配器,即 out_ptr_t 和 inout_ptr_t,分别用于应对上一节的 MakeObject() 类型 C 函数和 RefreshObject() 类型 C 函数(有时候 RefreshObject() 类型的 C 函数也是用 T** 类型参数)。对于 MakeObject() 类型 C 函数,我们可以这样使用 std::out_ptr_t 适配器:

int main() {
    std::unique_ptr<FooTest> spFoo;
    if (MakeObject(std::out_ptr_t<std::unique_ptr<FooTest>, FooTest*>(spFoo))) {
        std::cout << "value = " << spFoo->GetValue() << std::endl;
    }
}

2.2 out_ptr 和 inout_ptr

​ 正如上一节的例子,直接使用适配器需要显式指定模板参数,非常麻烦,所以 标准库还提供了两个全局函数,std::out_ptr() 和 std::inout_ptr(),这两个函数的作用就是根据函数参数推导参数类型,然后返回一个相应的适配器对象,可以简化这两个适配器的使用,比如上一节的例子,可以改成这样:

int main() {
    std::unique_ptr<FooTest> spFoo;
    if (MakeObject(std::out_ptr(spFoo))) {
        std::cout << "value = " << spFoo->GetValue() << std::endl;
    }
}

2.3 注意事项

​ C++ 标准并不建议直接使用 std::out_ptr_t 或 std::inout_ptr_t 构建一个有声明周期的临时对象,因为这样很容易导致问题,比如 2.1 节的例子,如果代码写成这样:

int main() {
    std::unique_ptr<FooTest> spFoo;
    
    auto&& rrr = std::out_ptr_t<std::unique_ptr<FooTest>, FooTest *>(spFoo);
    if (MakeObject(rrr)) {
        std::cout << "value = " << spFoo->GetValue() << std::endl;
    }
}

编译器不抱怨,但是运行异常,因为 rrr 延长了 std::out_ptr_t 临时对象的生命周期,使得它的析构在使用 spFoo 指针之后,导致 spFoo 在它消失之前一直是无效的状态。

​ 另外需要注意的是 std::inout_ptr_t 做的事情是释放智能指针原来的所有权,然后重新初始化这个智能指针。这样的操作需要独占所有权的智能指针,所以它不能用于 std::shared_ptr。还有就是 1.2 节所提到的代理或适配器的生命周期问题,std::out_ptr_t 或 std::inout_ptr_t 也存在,所以这样的代码依然是有问题的:

int main() {
    std::unique_ptr<FooTest> spFoo;
    if (MakeObject(std::out_ptr(spFoo)) && spFoo) {
        std::cout << "value = " << spFoo->GetValue() << std::endl;
    }
}

这也是使用智能指针适配器需要注意的地方。实际上,资料 [2] 中作者提出了几种解决方案,但是都不是很优雅的方案,大家感兴趣的话可以看一下这篇文章。

3 总结

​ 智能指针适配器的引入,起到了三个作用:

  • 安全封装原生指针的传递

    为许多 C 函数提供安全适配器,避免手动管理指针和资源的所有权

  • 与智能指针无缝协作

    允许 C++ 的智能指针直接用于需要 T** 和 T&* 参数的函数交互,解决智能指针需要手动释放和重置的问题,解决了此类 C 风格的 API 安全返回资源的问题

  • 统一资源管理接口

    将现有的 C 风格的资源获取和释放行为整合到 C++ 的 RAII 模型中,逐步替换为 RAII 风格,减少资源泄露的风险

总之,这些适配器工具是 C++ 进一步强化与 C 互操作性和资源管理的重要改进,通过对智能指针的自动适配,降低此类开发场景的资源泄漏风险,通过对智能指针的隐式管理,也使得代码更简洁。

参考资料

[1] retain_ptr: https://github.com/slurps-mad-rips/retain-ptr

[2] Raymond Chen: Spotting problems with destructors for C++ temporaries

[3] ComPtrRef Class: From Microsoft Windows Runtime Library

[4] P0468: A Proposal to Add an Intrusive Smart Pointer to the C++ Standard Library, 2018

[5] P1132: out_ptr - a scalable output pointer abstraction

关注作者的算法专栏
https://blog.youkuaiyun.com/orbit/category_10400723.html

关注作者的出版物《算法的乐趣(第二版)》
https://www.ituring.com.cn/book/3180

`std::unique_ptr` `std::shared_ptr` 是 C++ 中两种主要的智能指针类型,它们都用于自动管理动态分配的对象,但在**所有权语义**、**资源管理方式**、**性能特性**等方面有显著区别。 --- ## ✅ 一、`std::unique_ptr` 与 `std::shared_ptr` 的核心区别 | 特性 | `std::unique_ptr` | `std::shared_ptr` | |------|--------------------|-------------------| | 所有权模型 | 独占所有权(只能有一个指针拥有资源) | 共享所有权(多个指针共享资源) | | 可复制性 | 不可复制,但可以移动(move) | 可复制(引用计数增加) | | 内存开销 | 小(仅一个指针) | 稍大(需要维护引用计数) | | 性能 | 更快,适合局部资源管理 | 稍慢,适用于共享资源 | | 适用场景 | 资源生命周期单一、不需要共享 | 资源需在多个对象间共享 | | 循环引用 | 不涉及 | 可能导致内存泄漏,需配合 `std::weak_ptr` 使用 | --- ## ✅ 二、代码示例对比 ### 示例 1:`std::unique_ptr` 示例 ```cpp #include <iostream> #include <memory> int main() { std::unique_ptr<int> uptr = std::make_unique<int>(42); // 不能复制 // std::unique_ptr<int> uptr2 = uptr; // ❌ 编译错误 // 可以移动 std::unique_ptr<int> uptr2 = std::move(uptr); if (uptr == nullptr) { std::cout << "uptr is null after move" << std::endl; } std::cout << "uptr2 value: " << *uptr2 << std::endl; } ``` > 输出: ``` uptr is null after move uptr2 value: 42 ``` ### 示例 2:`std::shared_ptr` 示例 ```cpp #include <iostream> #include <memory> int main() { std::shared_ptr<int> sptr1 = std::make_shared<int>(100); // 可以复制 std::shared_ptr<int> sptr2 = sptr1; std::cout << "sptr1 use count: " << sptr1.use_count() << std::endl; // 输出 2 std::cout << "sptr2 use count: " << sptr2.use_count() << std::endl; { std::shared_ptr<int> sptr3 = sptr1; std::cout << "Inside scope, use count: " << sptr1.use_count() << std::endl; // 输出 3 } std::cout << "After scope, use count: " << sptr1.use_count() << std::endl; // 输出 2 } ``` > 输出: ``` sptr1 use count: 2 sptr2 use count: 2 Inside scope, use count: 3 After scope, use count: 2 ``` --- ## ✅ 三、更复杂示例:类成员使用不同智能指针 ### 示例:使用 `unique_ptr` 管理独占资源 ```cpp #include <iostream> #include <memory> class Resource { public: Resource() { std::cout << "Resource created\n"; } ~Resource() { std::cout << "Resource destroyed\n"; } }; class Owner { private: std::unique_ptr<Resource> res; public: Owner() : res(std::make_unique<Resource>()) {} }; int main() { { Owner o1; } // o1 离开作用域后,Resource 会被自动销毁 } ``` > 输出: ``` Resource created Resource destroyed ``` ### 示例:使用 `shared_ptr` 共享资源 ```cpp #include <iostream> #include <memory> class SharedResource { public: SharedResource() { std::cout << "SharedResource created\n"; } ~SharedResource() { std::cout << "SharedResource destroyed\n"; } }; int main() { std::shared_ptr<SharedResource> ptr1 = std::make_shared<SharedResource>(); { std::shared_ptr<SharedResource> ptr2 = ptr1; std::cout << "In inner scope\n"; } std::cout << "Out of inner scope\n"; } ``` > 输出: ``` SharedResource created In inner scope Out of inner scope SharedResource destroyed ``` --- ## ✅ 四、总结对比 | 比较点 | `unique_ptr` | `shared_ptr` | |--------|--------------|--------------| | 所有权 | 独占 | 共享 | | 可复制 | 否 | 是 | | 可移动 | 是 | 是 | | 内存开销 | 小 | 稍大 | | 是否线程安全 | 否(引用计数是原子操作) | 是(引用计数线程安全) | | 循环引用 | 不涉及 | 需配合 `weak_ptr` | | 适用场景 | 资源生命周期单一,如局部变量、工厂函数返回值 | 多个对象共享资源,如缓存、观察者模式 | --- ##
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王晓华-吹泡泡的小猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值