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.csdn.net/orbit/category_10400723.html
关注作者的出版物《算法的乐趣(第二版)》
https://www.ituring.com.cn/book/3180