提问人:jaggedSpire 提问时间:8/27/2016 最后编辑:CommunityjaggedSpire 更新时间:2/26/2020 访问量:3613
使用 RAII 从 C 样式 API 管理资源
Using RAII to manage resources from a C-style API
问:
资源获取即初始化 (RAII) 在 C++ 中通常用于管理资源的生存期,这些资源在其生存期结束时需要某种形式的清理代码,从控制指针到释放文件句柄。delete
new
如何快速轻松地使用 RAII 来管理从 C 样式 API 获取的资源的生存期?
就我而言,我想使用 RAII 在保存它释放的 C 风格资源的变量超出范围时自动从 C 风格 API 执行清理函数。除此之外,我真的不需要额外的资源包装,我想在这里最大限度地减少使用 RAII 的代码开销。有没有一种简单的方法可以使用 RAII 从 C 风格的 API 管理资源?
如何将C api封装到RAII C++类中?是相关的,但我不相信它是重复的——这个问题是关于更完整的封装,而这个问题是关于获得 RAII 好处的最少代码。
答:
使用 RAII 从 C 样式界面管理资源有一种简单的方法:标准库的智能指针,它有两种风格:std::unique_ptr 用于具有单个所有者的资源,std::shared_ptr
和 std
::weak_ptr
团队用于共享资源。如果你在决定你的资源是什么时遇到困难,这个问答应该可以帮助你做出决定。访问智能指针正在管理的原始指针就像调用其 get
成员函数一样简单。
如果您想要简单的、基于范围的资源管理,是完成这项工作的绝佳工具。它旨在将开销降至最低,并且易于设置以使用自定义销毁逻辑。事实上,非常简单,当你声明资源变量时,你就可以做到这一点:std::unique_ptr
#include <memory> // allow use of smart pointers
struct CStyleResource; // c-style resource
// resource lifetime management functions
CStyleResource* acquireResource(const char *, char*, int);
void releaseResource(CStyleResource* resource);
// my code:
std::unique_ptr<CStyleResource, decltype(&releaseResource)>
resource{acquireResource("name", nullptr, 0), releaseResource};
acquireResource
在变量生存期开始时调用它的位置执行。 将在变量生存期结束时执行,通常是在变量超出范围时执行。1 不相信我?您可以在 Coliru 上看到它的实际效果,我在其中为 acquire 和 release 函数提供了一些虚拟实现,以便您可以看到它的发生。releaseResource
如果您需要该品牌的资源生存期,则可以对 执行大致相同的操作:std::shared_ptr
// my code:
std::shared_ptr<CStyleResource>
resource{acquireResource("name", nullptr, 0), releaseResource};
现在,这两个都很好,但是标准库有 std::make_unique
2 和 std::make_shared
,原因之一是进一步的异常安全。
GotW #56 提到函数参数的计算是无序的,这意味着如果你有一个函数,它采用了你闪亮的新类型和一些可能在构造上抛出的资源,那么向函数调用提供该资源,如下所示:std::unique_ptr
func(
std::unique_ptr<CStyleResource, decltype(&releaseResource)>{
acquireResource("name", nullptr, 0),
releaseResource},
ThrowsOnConstruction{});
意味着指令可能按如下顺序排列:
- 叫
acquireResource
- 构建
ThrowsOnConstruction
- 从资源指针构造
std::unique_ptr
并且如果步骤 2 抛出,我们宝贵的 C 接口资源将无法正确清理。
同样,正如 GotW #56 中提到的,实际上有一种相对简单的方法来处理异常安全问题。与函数参数中的表达式计算不同,函数计算不能交错。因此,如果我们获得一个资源并将其提供给一个内部的函数,我们将保证在进行建设时不会发生泄露资源的棘手业务。我们不能使用 ,因为它返回一个带有默认删除器的,并且我们想要我们自己的自定义风格的删除器。我们还想指定我们的资源获取函数,因为如果没有额外的代码,就无法从类型中推导出它。借助模板的强大功能,实现这样的事情非常简单:3unique_ptr
ThrowsOnConstruction
std::make_unique
std::unique_ptr
#include <memory> // smart pointers
#include <utility> // std::forward
template <
typename T,
typename Deletion,
typename Acquisition,
typename...Args>
std::unique_ptr<T, Deletion> make_c_handler(
Acquisition acquisition,
Deletion deletion,
Args&&...args){
return {acquisition(std::forward<Args>(args)...), deletion};
}
你可以像这样使用它:
auto resource = make_c_handler<CStyleResource>(
acquireResource, releaseResource, "name", nullptr, 0);
并调用无忧,如下所示:func
func(
make_c_handler<CStyleResource>(
acquireResource, releaseResource, "name", nullptr, 0),
ThrowsOnConstruction{});
编译器不能接受 的构造并将其粘在 的调用和构造之间,所以你很好。ThrowsOnConstruction
acquireResource
unique_ptr
等效项同样简单:只需将返回值换成 ,并更改名称以指示共享资源:4shared_ptr
std::unique_ptr<T, Deletion>
std::shared_ptr<T>
template <
typename T,
typename Deletion,
typename Acquisition,
typename...Args>
std::shared_ptr<T> make_c_shared_handler(
Acquisition acquisition,
Deletion deletion,
Args&&...args){
return {acquisition(std::forward<Args>(args)...), deletion};
}
使用再次与版本相似:unique_ptr
auto resource = make_c_shared_handler<CStyleResource>(
acquireResource, releaseResource, "name", nullptr, 0);
和
func(
make_c_shared_handler<CStyleResource>(
acquireResource, releaseResource, "name", nullptr, 0),
ThrowsOnConstruction{});
编辑:
正如注释中提到的,您可以对以下命令的使用进行进一步的改进:在编译时指定删除机制,因此当删除器在程序中移动时,它不需要携带指向删除器的函数指针。在您使用的函数指针上创建模板化的无状态删除器需要四行代码,放在:std::unique_ptr
unique_ptr
make_c_handler
template <typename T, void (*Func)(T*)>
struct CDeleter{
void operator()(T* t){Func(t);}
};
然后你可以像这样修改:make_c_handler
template <
typename T,
void (*Deleter)(T*),
typename Acquisition,
typename...Args>
std::unique_ptr<T, CDeleter<T, Deleter>> make_c_handler(
Acquisition acquisition,
Args&&...args){
return {acquisition(std::forward<Args>(args)...), {}};
}
然后,用法语法略有变化,改为
auto resource = make_c_handler<CStyleResource, releaseResource>(
acquireResource, "name", nullptr, 0);
make_c_shared_handler
不会从更改为模板化删除器中受益,因为不会携带编译时可用的删除器信息。shared_ptr
1. 如果智能指针的值在被销毁时为 nullptr
,则它不会调用关联的函数,这对于处理以空指针作为错误条件的资源释放调用的库(如 SDL)来说非常好。
2. std::make_unique
仅在 C++14 中包含在库中,所以如果您使用的是 C++11,您可能希望实现自己的 - 即使它不是您想要的,它也非常有用。
3. 这(以及 2 中链接的 std::make_unique
实现)依赖于可变参数模板。如果您使用的是 VS2012 或 VS2010,它们对 C++11 的支持有限,则您无权访问可变参数模板。在这些版本中,std::make_shared
的实现是为每个参数编号和专用化组合使用单独的重载进行的。随心所欲。
4. std::make_shared
实际上有比这更复杂的机制,但它需要真正知道该类型的对象有多大。我们没有这种保证,因为我们使用的是 C 样式的接口,并且可能只有资源类型的正向声明,所以我们在这里不用担心。
评论
std::unique_ptr
std::unique_ptr
make_c_handler
>
专用的示波器保护机制可以干净简洁地管理 C 型资源。由于这是一个相对较古老的概念,因此有许多浮动,但允许任意代码执行的范围保护本质上是最灵活的。来自流行库的两个是 ,来自 facebook 的开源库 folly
(在 Andrei Alexandrescu 关于声明式控制流的演讲中讨论过),以及来自(不出所料)Boost.ScopeExit。SCOPE_EXIT
BOOST_SCOPE_EXIT
folly 是 <folly/ScopeGuard.hpp>
中提供的声明性控制流功能三元组的一部分。 和分别在以下情况下执行代码:控制流退出封闭范围时,通过抛出异常退出封闭范围时,以及退出时不抛出异常。1SCOPE_EXIT
SCOPE_EXIT
SCOPE_FAIL
SCOPE_SUCCESS
如果你有一个 C 风格的接口,其中包含如下资源和生存期管理功能:
struct CStyleResource; // c-style resource
// resource lifetime management functions
CStyleResource* acquireResource(const char *, char*, int);
void releaseResource(CStyleResource* resource);
您可以这样使用:SCOPE_EXIT
#include <folly/ScopeGuard.hpp>
// my code:
auto resource = acquireResource(const char *, char *, int);
SCOPE_EXIT{releaseResource(resource);}
Boost.ScopeExit 的语法略有不同。2 执行与上述代码相同的操作:
#include <boost/scope_exit.hpp>
// my code
auto resource = acquireResource(const char *, char *, int);
BOOST_SCOPE_EXIT(&resource) { // capture resource by reference
releaseResource(resource);
} BOOST_SCOPE_EXIT_END
您可能会发现在这两种情况下都适合声明为 ,以确保在函数的其余部分不会无意中更改该值,并使您试图简化的生存期管理问题重新复杂化。resource
const
在这两种情况下,当控制流退出封闭作用域时,无论是否例外,都将调用。请注意,无论是否在作用域末尾,它都会被调用,因此,如果 API 要求清理函数不在 null 指针上调用,则需要自己检查该条件。releaseResource
resource
nullptr
与使用智能指针相比,这里的简单性是无法像使用智能指针那样轻松地在封闭程序中移动生命周期管理机制,但是如果您希望在当前作用域退出时对清理执行进行非常简单的保证,则作用域保护对于这项工作来说绰绰有余。
1. 仅在成功或失败时执行代码提供了提交/回滚功能,当单个函数中可能发生多个故障点时,这对于异常安全和代码清晰度非常有帮助,这似乎是存在SCOPE_SUCCESS
和SCOPE_FAIL
背后的驱动原因,但你在这里是因为你对无条件清理感兴趣。
2. 顺便说一句,Boost.ScopeExit 也没有像 folly 那样内置的成功/失败功能。在文档中,成功/失败功能(如愚蠢的范围保护提供的功能)是通过检查通过引用捕获的成功标志来实现的。该标志在作用域开始时设置为 false
,并在相关操作成功后设置为 true
。
评论
SCOPE_EXIT
可以很好地实现此目的。它看起来比构建智能指针更好,并且不需要编写完整 RAII 类型的代码和工作。<folly/ScopeGuard.h>