首先从功能最简单的out_ptr讲起。

📅 2026/7/2 1:59:20
首先从功能最简单的out_ptr讲起。
std::out_ptr其实是一个函数返回一个类型为std::out_ptr_t的智能指针适配器函数签名如下#include memorytemplate class Pointer void, class Smart, class... Args auto out_ptr( Smart s, Args... args );这个函数主要是把各种智能指针包装成output parameter以方便现有的接口使用尤其是一些用c语言写的函数。在继续之前我们先来复习一下output parameter是什么。这东西又叫传出参数一次就是函数会把一部分数据写进自己的参数里返回给调用者。通过参数返回是因为c语言和c11之前的c不支持多值返回也没有类似tuple这样方便的数据结构导致函数无法直接返回两个以上的值所以需要用一种额外的传递数据的方式。比如我在以前的博客中提到的hsearchint hsearch_r(ENTRY item, ACTION action, ENTRY **retval, struct hsearch_data *htab)。这个函数用来在哈希表里创建或者查找数据查找失败的时候会返回错误码而查找成功的时候函数返回0并把找到的数据设置给retval。这个retval就是output parameter承载了函数除了错误码之外的返回数据。c里现在很少用指针类型作为output parameter了但还有更本地化的做法——引用int func(const char *name, Data retval)。这类函数有几个特点不在乎output parameter里有什么值函数调用期间完全享有output parameter和其资源的所有权函数返回后output parameter通常被设置为新值在c提倡少用裸指针的今天我们越来越习惯使用shared_ptr和unique_ptr但不管哪种智能指针都很难直接适配上面这些函数看个例子就明白了int get_data(const std::string name, Data **retval){if (!check_name(name)) {return ErrCheckFailed;}*retval make_data(name);return 0;}// 使用裸指针Data *data_ptr nullptr;if (auto err get_data(name, data_ptr); err ! 0) {错误处理} else {这里可以使用data_ptr}使用裸指针的时候代码比较简单我们再来看看使用智能指针的时候std::unique_ptrData resource;Data *data_ptr nullptr;if (auto err get_data(name, data_ptr); err ! 0) {错误处理} else {resource.reset(data_ptr);这里可以使用resource}代码会变得啰嗦而且如果我们忘记了调用reset那么资源就可能泄漏了还有最重要的一点我们主动使用了裸指针而这正是我们想避免的。这时候就需要out_ptr了。out_ptr生成的适配器会先放弃智能指针持有资源的所有权并将旧资源释放因为如前面所说我们要调用的函数会接管资源的所有权接着构造出的std::out_ptr_t有自动的类型转换方法可以把智能指针转换成我们需要的T**交给函数使用最后在函数调用结束之后再把新的资源设置回智能指针。所以上面的例子可以改成std::unique_ptrData resource;if (auto err get_data(name, std::out_ptr(resource)); err ! 0) {错误处理} else {这里可以使用resource无需reset}除了代码更简洁out_ptr还保证异常安全即使在调用get_data的过程中抛出了异常也不会出现资源泄漏。利用out_ptr我们可以在使用智能指针的同时兼容老旧接口。out_ptr和shared_ptr如果只看函数签名很多人会觉得out_ptr也可以直接配合std::shared_ptr使用然而现实是多变的struct Data {std::string name;};int get_data(const std::string name, Data **retval){if (name )return 1;*retval new Data{name};return 0;}int main(){std::shared_ptrData resource;if (auto err get_data(apocelipes, std::out_ptr(resource)); err ! 0)std::cerr error\n;elsestd::cout success, name: resource-name \n;}上面的代码无法通过编译$ clang -stdc23 test.cppIn file included from test.cpp:2:In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c/v1/memory:948:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c/v1/__memory/out_ptr.h:38:17: error: static assertion failed due to requirement !__is_specialization_vstd::shared_ptrData, shared_ptr || sizeof...(_Args) 0: Using std::shared_ptr without a deleter in std::out_ptr is not supported.38 | static_assert(!__is_specialization_v_Smart, shared_ptr || sizeof...(_Args) 0,| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c/v1/__memory/out_ptr.h:93:10: note: in instantiation of template class std::out_ptr_tstd::shared_ptrData, Data * requested here93 | return std::out_ptr_t_Smart, _Ptr, _Args...(__s, std::forward_Args(__args)...);| ^test.cpp:19:48: note: in instantiation of function template specialization std::out_ptrvoid, std::shared_ptrData requested here19 | if (auto err get_data(apocelipes, std::out_ptr(resource)); err ! 0)| ^1 error generated.报错虽然很长但只要关注前几行就行了错误的原因很明显std::shared_ptr要配合out_ptr使用就必须显示提供deleter。这是因为对于std::shared_ptrdeleter并不是类型的一部分通常是我们通过构造函数或者reset方法穿进去的为了能100%正确释放资源我们需要手动把合适的deleter传进去相对地deleter是std::unique_ptr类型的一部分out_ptr可以直接从类型参数里得到合适的deleter从而正确释放资源。这也是为什么out_ptr还有变长参数这些参数就是为了std::shared_ptr或者其他有特殊要求的类似智能指针准备的。好在上面的代码稍作修改就能正常使用int main(){std::shared_ptrData resource;- if (auto err get_data(apocelipes, std::out_ptr(resource)); err ! 0) if (auto err get_data(apocelipes, std::out_ptr(resource, std::default_deleteData{})); err ! 0)std::cerr error\n;elsestd::cout success, name: resource-name \n;}std::default_deleteT会调用delete或者delete[]来释放资源正好我们这里可以利用它。shared_ptr平时也默认使用的这个。修改很简单但网上讲这点的文档不多因此多记一笔。另外基于out_ptr会临时转移所有权这点来看共享所有权模型的std::shared_ptr其实并不适合使用out_ptr虽然标准没有禁止甚至还要求额外做检测用于初始化shared_ptr但我仍然建议把std::shared_ptr和std::out_ptr一起使用看做一种坏味道尽量避免这种用例。inout_ptrinout_ptr的名字比较抽象但只是在out_ptr的基础上加了个“in”而已。它会返回一个std::inout_ptr_t类型的对象函数签名如下#include memorytemplate class Pointer void, class Smart, class... Args auto inout_ptr( Smart s, Args... args );这个“in”是指使用output parameter的函数在重新设置参数的值之前会先使用他们因此这些函数的特点是非常在乎output parameter里有什么值根据这些值执行不同的操作函数调用期间完全享有output parameter和其资源的所有权函数返回后output parameter不变或者被设置为新值还是看例子我们对Data增加一个update_data函数如果name是recreate则删除原来的对象重新创建一个int update_data(Data **data){if (data nullptr || *data nullptr)return 1;if ((*data)-name recreate) {delete *data;*data new Data{apocelipes};return 2; // 代表已修改}return 0;}现实中没人这么写代码但存在很多类似的c接口而且我们也很难控制第三方库的代码质量难免不会遇上类似的东西。如果想在这种接口上用智能指针那只能说有福了