使用 RAII 从 C 样式 API 管理资源

Using RAII to manage resources from a C-style API

提问人:jaggedSpire 提问时间:8/27/2016 最后编辑:CommunityjaggedSpire 更新时间:2/26/2020 访问量:3613

问:

资源获取即初始化 (RAII) 在 C++ 中通常用于管理资源的生存期,这些资源在其生存期结束时需要某种形式的清理代码,从控制指针到释放文件句柄。deletenew

如何快速轻松地使用 RAII 来管理从 C 样式 API 获取的资源的生存期?

就我而言,我想使用 RAII 在保存它释放的 C 风格资源的变量超出范围时自动从 C 风格 API 执行清理函数。除此之外,我真的不需要额外的资源包装,我想在这里最大限度地减少使用 RAII 的代码开销。有没有一种简单的方法可以使用 RAII 从 C 风格的 API 管理资源?

如何将C api封装到RAII C++类中?是相关的,但我不相信它是重复的——这个问题是关于更完整的封装,而这个问题是关于获得 RAII 好处的最少代码。

C 11 C++14 RAII C++-FAQ

评论

0赞 jaggedSpire 8/27/2016
我之所以发布这篇文章,是因为我遇到了多个问题,其中 OP 在处理 C API 时试图消除资源泄漏问题。我不是简单地告诉他们使用智能指针,或者每次都输入有关如何使用智能指针完成任务的完整解释(就像我在这里所做的那样),而是在将来遇到此类问题时将其链接到。
1赞 chris 8/27/2016
SCOPE_EXIT可以很好地实现此目的。它看起来比构建智能指针更好,并且不需要编写完整 RAII 类型的代码和工作。
0赞 chris 8/27/2016
老实说,我现在没有时间把一些东西充实得太好。至少视频是一个很好的资源。你要找的实现是 。<folly/ScopeGuard.h>
0赞 jaggedSpire 8/27/2016
@chris 发布了一个带有范围保护的答案,由愚蠢和 Boost.ScopeExit 主演,因为一些开发人员可能能够轻松获得其中一个的许可,但无法获得另一个的许可。我担心它最终可能会隐藏在智能指针答案的可怕背后。随意添加评论/批评,尽管我要上床睡觉了,可能要到 UTC 时间 14:00 左右才会回复
0赞 chris 8/27/2016
看起来很棒,谢谢!

答:

36赞 jaggedSpire 8/27/2016 #1

使用 RAII 从 C 样式界面管理资源有一种简单的方法:标准库的智能指针,它有两种风格:std::unique_ptr 用于具有单个所有者的资源,std::shared_ptrstd: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_unique2std::make_shared,原因之一是进一步的异常安全。

GotW #56 提到函数参数的计算是无序的,这意味着如果你有一个函数,它采用了你闪亮的新类型和一些可能在构造上抛出的资源,那么向函数调用提供该资源,如下所示:std::unique_ptr

func(
    std::unique_ptr<CStyleResource, decltype(&releaseResource)>{
        acquireResource("name", nullptr, 0), 
        releaseResource},
    ThrowsOnConstruction{});

意味着指令可能按如下顺序排列:

  1. acquireResource
  2. 构建ThrowsOnConstruction
  3. 从资源指针构造std::unique_ptr

并且如果步骤 2 抛出,我们宝贵的 C 接口资源将无法正确清理。

同样,正如 GotW #56 中提到的,实际上有一种相对简单的方法来处理异常安全问题。与函数参数中的表达式计算不同,函数计算不能交错。因此,如果我们获得一个资源并将其提供给一个内部的函数,我们将保证在进行建设时不会发生泄露资源的棘手业务。我们不能使用 ,因为它返回一个带有默认删除器的,并且我们想要我们自己的自定义风格的删除器。我们还想指定我们的资源获取函数,因为如果没有额外的代码,就无法从类型中推导出它。借助模板的强大功能,实现这样的事情非常简单:3unique_ptrThrowsOnConstructionstd::make_uniquestd::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};
}

在 Coliru 上直播

你可以像这样使用它:

auto resource = make_c_handler<CStyleResource>(
    acquireResource, releaseResource, "name", nullptr, 0);

并调用无忧,如下所示:func

func(
    make_c_handler<CStyleResource>(
        acquireResource, releaseResource, "name", nullptr, 0),
    ThrowsOnConstruction{});

编译器不能接受 的构造并将其粘在 的调用和构造之间,所以你很好。ThrowsOnConstructionacquireResourceunique_ptr

等效项同样简单:只需将返回值换成 ,并更改名称以指示共享资源:4shared_ptrstd::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_ptrunique_ptrmake_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);

在 Coliru 上直播

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 样式的接口,并且可能只有资源类型的正向声明,所以我们在这里不用担心。

评论

1赞 milleniumbug 8/28/2016
使用自定义函数对象类型是比使用函数指针更好的方法,因为函数对象是无状态的,因此不需要与指针一起携带。这也意味着您不需要将删除程序传递给 的构造函数。std::unique_ptrstd::unique_ptr
0赞 jaggedSpire 8/28/2016
@milleniumbug更多,为了?unique_ptr
0赞 milleniumbug 8/28/2016
是的。有时这还不够,所以你可能想写自己的专用删除程序,但对于我说 98% 的情况来说,它已经足够体面了。
0赞 jaggedSpire 8/31/2016
@milleniumbug在删除器指针上集成模板化到答案中。make_c_handler
1赞 davidA 2/25/2020
这是一个很好的答案 - 谢谢。一条评论,代码块紧挨着“和调用func无忧,像这样:”是缺少一个结束字符,我想?>
4赞 jaggedSpire 8/27/2016 #2

专用的示波器保护机制可以干净简洁地管理 C 型资源。由于这是一个相对较古老的概念,因此有许多浮动,但允许任意代码执行的范围保护本质上是最灵活的。来自流行库的两个是 ,来自 facebook 的开源库 folly在 Andrei Alexandrescu 关于声明式控制流的演讲中讨论过),以及来自(不出所料)Boost.ScopeExitSCOPE_EXITBOOST_SCOPE_EXIT

folly 是 <folly/ScopeGuard.hpp> 中提供的声明性控制流功能三元组的一部分。 和分别在以下情况下执行代码:控制流退出封闭范围时,通过抛出异常退出封闭范围时,以及退出时不抛出异常。1SCOPE_EXITSCOPE_EXITSCOPE_FAILSCOPE_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

您可能会发现在这两种情况下都适合声明为 ,以确保在函数的其余部分不会无意中更改该值,并使您试图简化的生存期管理问题重新复杂化。resourceconst

在这两种情况下,当控制流退出封闭作用域时,无论是否例外,都将调用。请注意,无论是否在作用域末尾,它都会被调用,因此,如果 API 要求清理函数不在 null 指针上调用,则需要自己检查该条件。releaseResourceresourcenullptr

与使用智能指针相比,这里的简单性是无法像使用智能指针那样轻松地在封闭程序中移动生命周期管理机制,但是如果您希望在当前作用域退出时对清理执行进行非常简单的保证,则作用域保护对于这项工作来说绰绰有余。


1. 仅在成功或失败时执行代码提供了提交/回滚功能,当单个函数中可能发生多个故障点时,这对于异常安全和代码清晰度非常有帮助,这似乎是存在SCOPE_SUCCESSSCOPE_FAIL背后的驱动原因,但你在这里是因为你对无条件清理感兴趣。
2. 顺便说一句,Boost.ScopeExit 也没有像 folly 那样内置的成功/失败功能。在文档中,成功/失败功能(如愚蠢的范围保护提供的功能)是通过检查通过引用捕获的成功标志来实现的。该标志在作用域开始时设置为 false,并在相关操作成功后设置为 true