C++23的out_ptr和inout_ptr
c++23新增了一些智能指针适配器,用来扩展和简化智能指针的使用。
这次主要介绍的是std::out_ptr和std::inout_ptr。这两个适配器用法和实现都很简单,但网上的文档都比较抱歉,还缺少一些比较重要的部分,因此单开一篇文章记录一下。
out_ptr
首先从功能最简单的out_ptr讲起。
std::out_ptr其实是一个函数,返回一个类型为std::out_ptr_t的智能指针适配器,函数签名如下:
#include <memory>
template< class Pointer = void, class Smart, class... Args >
auto out_ptr( Smart& s, Args&&... args );
这个函数主要是把各种智能指针包装成output parameter,以方便现有的接口使用,尤其是一些用c语言写的函数。
在继续之前我们先来复习一下output parameter是什么。这东西又叫传出参数,一次就是函数会把一部分数据写进自己的参数里返回给调用者。
通过参数返回是因为c语言和c++11之前的c++不支持多值返回也没有类似tuple这样方便的数据结构,导致函数无法直接返回两个以上的值,所以需要用一种额外的传递数据的方式。
比如我在以前的博客中提到的hsearch:int 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_ptr<Data> 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_ptr<Data> 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_ptr<Data> resource;
if (auto err = get_data("apocelipes", std::out_ptr(resource)); err != 0)
std::cerr << "error\n";
else
std::cout << "success, name: " << resource->name << "\n";
}
上面的代码无法通过编译:
$ clang++ -std=c++23 test.cpp
In 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_v<std::shared_ptr<Data>, 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_t<std::shared_ptr<Data>, Data *>' requested here
93 | 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_ptr<void, std::shared_ptr<Data>>' requested here
19 | if (auto err = get_data("apocelipes", std::out_ptr(resource)); err != 0)
| ^
1 error generated.
报错虽然很长但只要关注前几行就行了,错误的原因很明显,std::shared_ptr要配合out_ptr使用就必须显示提供deleter。
这是因为对于std::shared_ptr,deleter并不是类型的一部分,通常是我们通过构造函数或者reset方法穿进去的,为了能100%正确释放资源,我们需要手动把合适的deleter传进去;相对地deleter是std::unique_ptr类型的一部分,out_ptr可以直接从类型参数里得到合适的deleter从而正确释放资源。
这也是为什么out_ptr还有变长参数,这些参数就是为了std::shared_ptr或者其他有特殊要求的类似智能指针准备的。
好在上面的代码稍作修改就能正常使用:
int main()
{
std::shared_ptr<Data> resource;
- if (err = get_data("apocelipes", std::out_ptr(resource)); err != 0)
+ if (err = get_data("apocelipes", std::out_ptr(resource, std::default_delete<Data>{})); err != 0)
std::cerr << "error\n";
else
std::cout << "success, name: " << resource->name << "\n";
}
std::default_delete<T>会调用delete或者delete[]来释放资源,正好我们这里可以利用它。shared_ptr平时也默认使用的这个。
修改很简单,但网上讲这点的文档不多,因此多记一笔。另外基于out_ptr会临时转移所有权这点来看,共享所有权模型的std::shared_ptr其实并不适合使用out_ptr,虽然标准没有禁止甚至还要求额外做检测(用于初始化shared_ptr),但我仍然建议把std::shared_ptr和std::out_ptr一起使用看做一种坏味道,尽量避免这种用例。
inout_ptr
inout_ptr的名字比较抽象,但只是在out_ptr的基础上加了个“in”而已。它会返回一个std::inout_ptr_t类型的对象,函数签名如下:
#include <memory>
template< 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接口,而且我们也很难控制第三方库的代码质量,难免不会遇上类似的东西。如果想在这种接口上用智能指针,那只能说有福了:
auto resource = std::make_unique<Data>("recreate");
Data *ptr = resource.get();
resource.release(); // 释放所有权,但不释放资源
if (auto code = update_data(&ptr); code == 1)
std::cerr << "error\n";
else if (code == 2) {
resource.reset(ptr);
std::cout << "updated, name: " << resource->name << "\n";
} else {
resource.reset(ptr);
std::cout << "updated, name: " << resource->name << "\n";
}
可以看到代码会变得很复杂,而且一但忘记使用reset就会内存错误。这时候我们就需要inout_ptr帮忙了。
inout_ptr整体上和out_ptr差不多,都是让出资源的所有权然后重新把函数返回的值设置回去,但还有几个差异:
- 前面说过需要
inout_ptr的函数是需要参数的值的,因此构造inout_ptr_t时之后放弃资源的所有权,不会像out_ptr那样释放资源本身 - 资源的释放是调用的函数的责任,
inout_ptr只会把函数返回出来的值重新设置回智能指针
用inout_ptr改写后的代码如下:
auto resource = std::make_unique<Data>("recreate");
if (auto code = update_data(std::inout_ptr(resource)); code == 1)
std::cerr << "error\n";
else if (code == 2) {
std::cout << "updated, name: " << resource->name << "\n";
} else {
std::cout << "updated, name: " << resource->name << "\n";
}
代码看起来清爽多了。
另外虽然inout_ptr也有变长参数,但标准明确规定它不能配合std::shared_ptr使用,这些参数std::unique_ptr用不上,是预留给其他的第三方的类似指针对象使用的。
注意事项
除了std::shared_ptr配合out_ptr使用时需要传入deleter,还有一个注意事项。
两个适配器都不建议这么用:
auto out = std::out_ptr(resource);
func(out);
因为他们都是在析构函数里重新设置智能指针的值,如果绑定到一个局部变量或者其他存储器的变量上,函数调用结束就无法把正确的值重新设置回智能指针,这会导致严重的内存错误。
唯一建议的用法是直接使用out_ptr和inout_ptr的返回值:func(std::out_ptr(resource)),这样函数调用结束后表达式结束,返回值作为表达式中创建的临时变量会被析构,这样智能指针的值就被正常设置了。
尽管只要在转换操作符上加上一点限制就能避免误用,但标准考虑到了各种边缘情形,最终没有添加限制,所以我们只能牢记这条注意事项避免踩坑了。
总结
说实话这两个适配器有很浓的给c库函数擦屁股的意味,甚至标准文档上直接拿fopen_s做例子了,我们看下它的函数声明就能秒懂:errno_t fopen_s( FILE *restrict *restrict streamptr, const char *restrict filename, const char *restrict mode );。
另外这两个适配器虽然叫智能指针适配器,但也可以对普通裸指针使用,不过我不推荐这种用法。
最后虽然它们的用法都比较偏,但真要用的时候还都有用,所以了解一下总是没坏处的。而且它们的源代码也很简单,有兴趣可以看看libcxx的实现,虽然相比其他家的有点啰嗦,但可读性很强:
out_ptr: https://github.com/llvm/llvm-project/blob/main/libcxx/include/__memory/out_ptr.h
inout_ptr: https://github.com/llvm/llvm-project/blob/main/libcxx/include/__memory/inout_ptr.h
参考资料
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/n4950.pdf p.643
C++23的out_ptr和inout_ptr的更多相关文章
- Java开发中的23种设计模式详解
[放弃了原文访问者模式的Demo,自己写了一个新使用场景的Demo,加上了自己的理解] [源码地址:https://github.com/leon66666/DesignPattern] 一.设计模式 ...
- ILJMALL project过程中遇到Fragment嵌套问题:IllegalArgumentException: Binary XML file line #23: Duplicate id
出现场景:当点击"分类"再返回"首页"时,发生error退出 BUG描述:Caused by: java.lang.IllegalArgumentExcep ...
- CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator
CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator 我还没有用过Compute Shader,所以现在把红宝书里的例子拿来了,加入CSharpGL中. ...
- ABP(现代ASP.NET样板开发框架)系列之23、ABP展现层——异常处理
点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之23.ABP展现层——异常处理 ABP是“ASP.NET Boilerplate Project (ASP.NET ...
- Java开发中的23种设计模式详解(转)
设计模式(Design Patterns) ——可复用面向对象软件的基础 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了 ...
- C#得到某月最后一天晚上23:59:59和某月第一天00:00:00
项目需求: 某学校订单截止操作时间的上一个月最后一天晚上23:59:59 为止所有支付的订单统计: 代码: /// <summary> /// 通过学校和截止时间得到订单 /// < ...
- C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试
在上篇<C#开发微信门户及应用(22)-微信小店的开发和使用>里面介绍了一些微信小店的基础知识,以及对应的对象模型,本篇继续微信小店的主题,介绍其中API接口的封装和测试使用.微信小店的相 ...
- [转载]IIS7报500.23错误的解决方法
原文出处: 原文作者:pizibaidu 原文链接:http://pizibaidu.blog.51cto.com/1361909/1794446 背景:今天公司终端上有一个功能打开异常,报500错误 ...
- [MySQL Reference Manual] 23 Performance Schema结构
23 MySQL Performance Schema 23 MySQL Performance Schema 23.1 性能框架快速启动 23.2 性能框架配置 23.2.1 性能框架编译时配置 2 ...
- Error:failed to find Build Tools revision 23.0.0 rc3
解决,选择AS里有的版本就可以了,已有的我这就一个23.0.3,导入的项目是23.0.2 Donate:)
随机推荐
- 5000行js db
https://www.cnblogs.com/lavezhang/p/13777018.html
- Java中使用正则表达式的正确打开方式
正则表达式基础语法 Java正则表达式基于java.util.regex包,核心类是Pattern和Matcher.基本语法遵循标准正则规范: . 匹配任意单个字符(除换行符) \d 匹配数字,等价于 ...
- [C++ Primer] IO库
IO库 IO类 为了支持不同种类的IO处理操作,标准库定义了一些IO类型,分别定义在三个独立的头文件中:iostream定义了用于读写流的基本类型,fstream定义了读写命名文件的类型,sstrea ...
- 【iOS】检测项目中是否包含UIWebView
在今年apple审核中已经明确说明不再支持UIWebView,督促开发者使用WKWebView.但是由于老项目的过大,我们一般直接搜索UIWebview来看有多少类中使用了UIWebView.但是这样 ...
- U3D动作游戏开发读书笔记--2.2 编辑器本身的基础知识
2.2 编辑器本身的基础知识 项目顺利开发离不开对开发工具的打磨,为此需要对Unity Editor进行拓展功能的开发,包括一些诸如常量生成器这样辅助性的功能开发,以及通过引擎自带的插件与其他3D软件 ...
- 一纪风华,凝练出华为Pura 70系列
「 华为P系列走过十二年.一纪风华,都凝练在新升级的华为Pura 70之上. 」 品牌焕新,是一次跃迁,更是一次华丽转身. 用户从来都是非常敏锐的,好的产品也都是自带流量的.线上:1分钟售罄:线下:多 ...
- SystemVerilog总结
SystemVerilog总结 过了两个月的时间,把这本<SystemVerilog for Design (Edition 2)>基本上读完了.对SystemVerilog也建立了一些认 ...
- Synaptics蠕虫病毒查杀方法-OK
1.Synaptics蠕虫病毒感染表现 (1)当插上移动硬盘后,总是弹出"Synaptics.exe - 损坏的映像"对话框,关闭后过一会又会弹出,并且对话框弹出时无法进行其他操作 ...
- Fastjson之数据脱敏
很多业务数据在展示上需要进行脱敏处理,保护重要的敏感信息.如电话号码脱敏,期望展示的数据格式是156****7837:如身份证号码脱敏,期望展示的数据格式是420***********113X. 当然 ...
- spring.profiles.active=@profiles.active@用法
1.引言 当在多配置文件中,需要切换配置文件时,通常的做法都是修改激活的文件名称,而spring.profiles.active=@profiles.active@ 是配合 maven profile ...