简体   繁体   中英

Using RAII to manage resources from a C-style API

Resource Acquisition is Initialization (RAII) is commonly used in C++ to manage the lifetimes of resources which require some manner of cleanup code at the end of their lifetime, from delete ing new ed pointers to releasing file handles.

How do I quickly and easily use RAII to manage the lifetime of a resource I acquire from a C-style API?

In my case, I want to use RAII to automatically execute a cleanup function from a C-style API when the variable holding the C-style resource it releases goes out of scope. I don't really need additional resource wrapping beyond that, and I'd like to minimize the code overhead of using RAII here. Is there a simple way to use RAII to manage resources from a C-style API?

How to encapsulate C api into RAII C++ classes? is related, but I don't believe it's a duplicate--that question is regarding more complete encapsulation, while this question is about minimal code to get the benefits of RAII.

There is an easy way to use RAII to manage resources from a C-style interface: the standard library's smart pointers , which come in two flavors: std::unique_ptr for resources with a single owner and the team of std::shared_ptr and std::weak_ptr for shared resources. If you're having trouble deciding which your resource is, this Q&A should help you decide . Accessing the raw pointer a smart pointer is managing is as easy as calling its get member function.

If you want simple, scope-based resource management, std::unique_ptr is an excellent tool for the job. It's designed for minimal overhead, and is easy to set up to use custom destruction logic. So easy, in fact, that you can do it when you declare the resource variable:

#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 executes where you call it, at the start of the variable's lifetime. releaseResource will execute at the end of the variable's lifetime, usually when it goes out of scope. 1 Don't believe me? you can see it in action on Coliru , where I've provided some dummy implementations to the acquire and release functions so you can see it happening.

You can do much the same with std::shared_ptr , if you require that brand of resource lifetime instead:

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

Now, both of these are all well and good, but the standard library has std::make_unique 2 and std::make_shared and one of the reasons is further exception-safety.

GotW #56 mentions that the evaluation of arguments to a function are unordered, which means if you have a function that takes your shiny new std::unique_ptr type and some resource that might throw on construction, supplying a that resource to a function call like this:

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

means that the instructions might be ordered like this:

  1. call acquireResource
  2. construct ThrowsOnConstruction
  3. construct std::unique_ptr from resource pointer

and that our precious C interface resource won't be cleaned up properly if step 2 throws.

Again as mentioned in GotW #56, there's actually a relatively simple way to deal with the exception safety problem. Unlike expression evaluations in function arguments, function evaluations can't be interleaved. So if we acquire a resource and give it to a unique_ptr inside a function, we'll be guaranteed no tricky business will happen to leak our resource when ThrowsOnConstruction throws on construction. We can't use std::make_unique , because it returns a std::unique_ptr with a default deleter, and we want our own custom flavor of deleter. We also want to specify our resource acquisition function, since it can't be deduced from the type without additional code. Implementing such a thing is simple enough with the power of templates: 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};
}

Live on Coliru

you can use it like this:

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

and call func worry-free, like this:

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

The compiler can't take the construction of ThrowsOnConstruction and stick it between the call to acquireResource and the construction of the unique_ptr , so you're good.

The shared_ptr equivalent is similarly simple: just swap out the std::unique_ptr<T, Deletion> return value with std::shared_ptr<T> , and change the name to indicate a shared resource: 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};
}

Use is once again similar to the unique_ptr version:

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

and

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

Edit:

As mentioned in the comments, there's a further improvement you can make to the use of std::unique_ptr : specifying the deletion mechanism at compile time so the unique_ptr doesn't need to carry a function pointer to the deleter when it's moved around the program. Making a stateless deleter templated on the function pointer you're using requires four lines of code, placed before make_c_handler :

template <typename T, void (*Func)(T*)>
struct CDeleter{
    void operator()(T* t){Func(t);}    
};

Then you can modify make_c_handler like so:

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)...), {}};
}

The usage syntax then changes slightly, to

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

Live on Coliru

make_c_shared_handler would not benefit from changing to a templated deleter, as shared_ptr does not carry deleter information available at compile time.


1. If the value of the smart pointer is nullptr when it's destructed, it won't call the associated function, which is quite nice for libraries which handle resource release calls with null pointers as error conditions, like SDL.
2. std::make_unique was only included in the library in C++14, so if you're using C++11 you might want to implement your own --it's very helpful even if it's not quite what you want here.
3. This (and the std::make_unique implementation linked in 2) depend on variadic templates . If you're using VS2012 or VS2010, which have limited C++11 support, you don't have access to variadic templates. The implementation of std::make_shared in those versions was instead made with individual overloads for each argument number and specialization combination . Make of that what you will.
4. std::make_shared actually has more complex machinery than this , but it requires actually knowing how big an object of the type will be. We don't have that guarantee since we're working with a C-style interface and may only have a forward declaration of our resource type, so we won't be worrying about it here.

A dedicated scope guard mechanism can cleanly and concisely manage C-style resources. Since it's a relatively old concept, there are a number floating around, but scope guards which allow arbitrary code execution are by nature the most flexible of them. Two from popular libraries are SCOPE_EXIT , from facebook's open source library folly (discussed in Andrei Alexandrescu talk on Declarative Control Flow ), and BOOST_SCOPE_EXIT from (unsurprisingly) Boost.ScopeExit .

folly's SCOPE_EXIT is part of a triad of declarative control flow functionality provided in <folly/ScopeGuard.hpp> . SCOPE_EXIT SCOPE_FAIL and SCOPE_SUCCESS respectively execute code when control flow exits the enclosing scope, when it exits the enclosing scope by throwing an exception, and when it exits without throwing an exception. 1

If you have a C-style interface with a resource and lifetime management functions like this:

struct CStyleResource; // c-style resource

// resource lifetime management functions
CStyleResource* acquireResource(const char *, char*, int);
void releaseResource(CStyleResource* resource);

you may use SCOPE_EXIT like so:

#include <folly/ScopeGuard.hpp>

// my code:
auto resource = acquireResource(const char *, char *, int);
SCOPE_EXIT{releaseResource(resource);}

Boost.ScopeExit has a slightly differing syntax. 2 To do the same as the above code:

#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

You may find it suitable in both cases to declare resource as const , to ensure you don't inadvertently change the value during the rest of the function and re-complicate the lifetime management concerns you're trying to simplify.

In both cases, releaseResource will be called when control flow exits the enclosing scope, by exception or not. Note that it will also be called regardless of whether resource is nullptr at scope end, so if the API requires cleanup functions not be called on null pointers you'll need to check that condition yourself.

The simplicity here versus the use of a smart pointer comes at the cost of not being able to move your lifetime management mechanism around the enclosing program as easily as you can with smart pointers, but if you want a dead-simple guarantee of cleanup execution when the current scope exits, scope guards are more than adequate for the job.


1. Executing code only on success or failure offers a commit/rollback functionality which can be incredibly helpful for exception safety and code clarity when multiple failure points may occur in a single function, which seems to be the driving reason behind the presence of SCOPE_SUCCESS and SCOPE_FAIL , but you're here because you're interested in unconditional cleanup.
2. As a side note, Boost.ScopeExit also doesn't have built-in success/fail functionality like folly. In the documentation, success/fail functionality like that provided by the folly scope guard is instead implemented by checking a success flag that has been captured by reference. The flag is set to false at the start of the scope, and set to true once the relevant operations succeed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM