RAII + C ++ variadic templates = win

  • Tutorial
I recently looked closely at C ++ Variadic Templates and unexpectedly invented the new RAII Scoped Resource Manager.
It turned out briefly and effectively.

For example, with C-style memory allocation:
// Аллоцируем ресурс в блоке.
{
    ha::scoped_resource mem(::malloc, 1, ::free);
    ::memset(mem, 65, 1);
}


When you exit the block, the resource will be freed automatically.

Or else like this, you can grab ownership of the file descriptor resource:
// Захватываем ресурс в блоке.
{
    ha::scoped_resource fd(
        [&filename]()
        {
            return ::open(filename.c_str(), O_RDONLY);
        },
        ::close);
    assert(fd != -1);
    std::vector buff(1024);
    ssize_t rc = ::read(fd, &buff[0], 1024);
}


Upon exiting the resource block will be released automatically, even after the call, for example throw std::exception().

Or the second example can be rewritten even more clearly without the use of a lambda:
{
    ha::scoped_resource fd(::open, filename.c_str(), O_RDONLY, ::close);
    if (fd == -1)
        throw std::runtime_error(std::string("open() failed: ") + ::strerror(errno));
    std::vector buff(1024);
    ssize_t rc = ::read(fd, &buff[0], 1024);
}


That is, in general, have templeytny class that is instantiated resource type and its constructor takes two std::functions: initializer_t и finalizer_t.

Between the initializer and the finalizer are the parameters for the initializer, which are part of the template qualifiers.

The destructor simply calls the finalizer for the captured resource.

For raw access to a resource, a resource type operator exists.
{
    ha::scoped_resource
        
            resource
                (ititializer, param1, ..., finalizer);
    resource_t
        plain_resource =
            resource.operator resource_t();
}


What is the advantage over other RAII implementations of resource wrappers?

  1. The initializer is not called during the reduction of constructor parameters, but in the constructor itself. This, for example, allows you to implement a “normal” initializer transfer, which makes it possible to capture a resource in lazy-style, before the first call operator resource_t(). This also allows you to create named initializers, thereby reusing them.
  2. You can explicitly pass any number of parameters to the initializer. Here, perhaps, there is another second useful mechanism - std::initializer_list.
  3. If point 2. is not applicable for some reason, you can pass a lambda as an initializer, which closes all the parameters of the initializer to itself.
  4. The deinitializer has a single parameter - the type of resource, but if necessary it can also be a lambda, locking on itself additional parameters of deinitialization.
  5. It is much easier to implement than std::shared_ptr(T* ptr, deleter d).


Disadvantages?
Sometimes it’s still more efficient to write a full resource wrapper.

Need more examples? I have them:

Creating an AVFormatContext Context:
ha::scoped_resource formatctx
    (ffmpeg::avformat_alloc_context, ffmpeg::avformat_free_context);


This is an analogue of the following:
std::shared_ptr formatctx =
    std::shared_ptr
        (ffmpeg::avformat_alloc_context(), ffmpeg::avformat_free_context);


Or else, a composite deinitializer is used here:
ha::scoped_resource codecctx(
        ffmpeg::avcodec_alloc_context,
        [](ffmpeg::AVCodecContext* c)
        {
            ffmpeg::avcodec_close(c),
                ffmpeg::av_free(c);
        });


And this example is interesting in that a resource is captured that does not need to be freed:
ha::scoped_resource codec(
        ffmpeg::avcodec_find_decoder,
        codecctx->codec_id,
        [](__attribute__((unused)) ffmpeg::AVCodec* c)
        {
        });


And finally the easiest oneliner:
ha::scoped_resource frame(ffmpeg::avcodec_alloc_frame, ffmpeg::av_free);


Which is analogous to the following:
std::shared_ptr frame =
    std::shared_ptr(ffmpeg::avcodec_alloc_frame(), ffmpeg::av_free);

But is it really all about naked plain-C resources? And where are the examples with valid C ++?
And here:
ha::mutex mutex;
ha::scoped_resource scoped_lock(
    [](ha::mutex* m) -> ha::mutex*
    {
        return m->lock(), m;
    },
    &mutex,
    [](ha::mutex* m) -> void
    {
        m->unlock();
    }
);

Ok, but where is the implementation?
The scoped_resource class implementation is so simple and elegant that it even reminded me of Y-combinator 's idea .
That is, it is possible to easily implement something similar, simply starting with the declaration of the constructor scoped_resource::scoped_resource(initializer_t, finalizer_t);and then building up the variadic part for the parameters.

OK, but where is the implementation anyway, huh?
template 
class scoped_resource
{
    public:
        typedef std::function initializer_t;
        typedef std::function finalizer_t;
        typedef T resource_t;
        scoped_resource(initializer_t finit, A... args, finalizer_t final)
            : finit_(finit), final_(final), resource_(finit_(args...)) { };
        ~scoped_resource() { final_(resource_); }
        template 
        Y get() const { return static_cast(resource_); }
        T get() const { return resource_; }
        operator T() const { return get(); }
        // No copy, no move
        scoped_resource(const scoped_resource&) = delete;
        scoped_resource(scoped_resource&&) = delete;
        scoped_resource& operator=(const scoped_resource&) = delete;
        scoped_resource& operator=(scoped_resource&&) = delete;
    private:
        const initializer_t finit_;
        const finalizer_t final_;
        T resource_;
};


Something like that. image


Also popular now: