使用RAII从C风格的API管理资源

jag*_*ire 20 c++ raii c++-faq c++11 c++14

资源获取是初始化(RAII)在C++中是常用的管理需要的清理代码某种方式在它们的寿命结束时,从资源的寿命delete荷兰国际集团new编指针释放文件句柄.

如何快速轻松地使用RAII来管理从C风格API获取的资源的生命周期?

在我的情况下,我想使用RAII从C风格的API中自动执行清理函数,当它发布的C风格资源的变量超出范围时.我不需要额外的资源包装,我想在这里最小化使用RAII的代码开销.有没有一种简单的方法可以使用RAII来管理C风格的API资源?

如何将C api封装到RAII C++类中?是相关的,但我不认为这是一个重复 - 这个问题是关于更完整的封装,而这个问题是关于获得RAII的好处的最小代码.

jag*_*ire 28

有一种简单的方法可以使用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};
Run Code Online (Sandbox Code Playgroud)

acquireResource在变量生命周期的开始处执行您调用它的位置.releaseResource将在变量的生命周期结束时执行,通常在它超出范围时执行.1不相信我?你可以在Coliru上看到它,我已经为获取和释放功能提供了一些虚拟实现,所以你可以看到它发生了.

std::shared_ptr如果您需要该品牌的资源生命周期,您可以做同样的事情:

// my code:
std::shared_ptr<CStyleResource> 
    resource{acquireResource("name", nullptr, 0), releaseResource};
Run Code Online (Sandbox Code Playgroud)

现在,这两个都很好,但是标准库中有2和和的原因之一是进一步异常安全.std::make_uniquestd::make_shared

GotW#56提到对函数参数的求值是无序的,这意味着如果你有一个函数可以获取你的闪亮的新std::unique_ptr类型和一些可能会构建的资源,那么将这个资源提供给这样的函数调用:

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

意味着可能会按如下顺序排列说明:

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

如果第2步抛出,我们宝贵的C接口资源将无法正常清理.

再次如GotW#56中所提到的,实际上有一种相对简单的方法来处理异常安全问题.与函数参数中的表达式求值不同,函数求值不能交错.因此,如果我们获取资源并将其提供给函数unique_ptr 内部,我们将保证在ThrowsOnConstruction投入构建时不会发生泄漏资源的棘手业务.我们不能使用std::make_unique,因为它返回一个std::unique_ptr默认删除器,我们想要自己的自定义删除器.我们还想指定我们的资源获取功能,因为如果没有附加代码,就无法从类型中推断出它.使用模板的强大功能实现这样的功能很简单:3

#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};
}
Run Code Online (Sandbox Code Playgroud)

住在Coliru

你可以像这样使用它:

auto resource = make_c_handler<CStyleResource>(
    acquireResource, releaseResource, "name", nullptr, 0);
Run Code Online (Sandbox Code Playgroud)

并且func无忧无虑地呼叫,像这样:

func(
    make_c_handler<CStyleResource(
        acquireResource, releaseResource, "name", nullptr, 0),
    ThrowsOnConstruction{});
Run Code Online (Sandbox Code Playgroud)

编译器无法构建ThrowsOnConstruction并将其粘贴在调用acquireResource和构造之间unique_ptr,所以你很好.

shared_ptr相当于是同样简单:只需换出std::unique_ptr<T, Deletion>有返回值std::shared_ptr<T>,并更改名称指示共享资源:4

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};
}
Run Code Online (Sandbox Code Playgroud)

使用再次类似于unique_ptr版本:

auto resource = make_c_shared_handler<CStyleResource>(
    acquireResource, releaseResource, "name", nullptr, 0);
Run Code Online (Sandbox Code Playgroud)

func(
    make_c_shared_handler<CStyleResource(
        acquireResource, releaseResource, "name", nullptr, 0),
    ThrowsOnConstruction{});
Run Code Online (Sandbox Code Playgroud)

编辑:

正如评论中所提到的,你可以对使用进行进一步的改进std::unique_ptr:在编译时指定删除机制,这样unique_ptr当它在程序中移动时不需要携带一个指向删除器的函数指针.在你正在使用的函数指针上模板化一个无状态删除器需要四行代码,放在之前make_c_handler:

template <typename T, void (*Func)(T*)>
struct CDeleter{
    void operator()(T* t){Func(t);}    
};
Run Code Online (Sandbox Code Playgroud)

然后你可以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)...), {}};
}
Run Code Online (Sandbox Code Playgroud)

然后使用语法略有变化

auto resource = make_c_handler<CStyleResource, releaseResource>(
    acquireResource, "name", nullptr, 0);
Run Code Online (Sandbox Code Playgroud)

住在Coliru

make_c_shared_handler不会因更改为模板化删除器而受益,因为shared_ptr不会在编译时携带删除信息.


1.如果智能指针的值是nullptr在它被破坏的时候,它将不会调用相关的函数,这对于处理资源释放调用的库来说非常好,这些库使用空指针作为错误条件,如SDL.
2. std::make_unique只包含在C++ 14的库中,所以如果你正在使用C++ 11,你可能想要实现自己的 - 即使它不是你想要的,它也非常有帮助.
3.这(以及std::make_unique2中链接的实现)取决于可变参数模板.如果您使用的是具有有限C++ 11支持的VS2012或VS2010,则无法访问可变参数模板.std::make_shared在这些版本中的实现是针对每个参数编号和特化组合使用单独的重载.按照你的意愿做.
4. std::make_shared实际上有比这更复杂的机器,但它实际上需要知道该类型的物体有多大.我们没有这个保证,因为我们正在使用C风格的界面,并且可能只有我们资源类型的前向声明,所以我们不会在这里担心它.