📜 ⬆️ ⬇️

RAII + C ++ variadic templates = win

Recently I looked closely at C ++ Variadic Templates and unexpectedly invented a new RAII Scoped Resource Manager.
It turned out briefly and effectively.

For example, with C-style memory allocation:
//    . { ha::scoped_resource<void*, size_t> mem(::malloc, 1, ::free); ::memset(mem, 65, 1); } 


When exiting the block, the resource will be released automatically.
')
Or another way you can take possession of the resource "file descriptor":
 //    . { ha::scoped_resource<int> fd( [&filename]() { return ::open(filename.c_str(), O_RDONLY); }, ::close); assert(fd != -1); std::vector<char> buff(1024); ssize_t rc = ::read(fd, &buff[0], 1024); } 


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

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


That is, in the general case, we have a template class that is instantiated by the type of the resource, and its constructor takes two std::functions : initializer_t finalizer_t .

Parameters for the initializer, which are part of the pattern specifiers, follow between the initializer and the finalizer.

The destructor simply calls the finalizer for the captured resource.

For raw access to a resource, there is a resource type operator.
 { ha::scoped_resource <resource_t, param1_t, ...> 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 the parameters of the constructor, but in the constructor itself. This, for example, allows you to implement a "normal" transfer of the initializer, which allows you to capture the resource in a lazy-style, before the first call to the operator resource_t() . It also allows you to create named initializers, thereby reusing them.
  2. You can explicitly pass any number of parameters for the initializer. Here, perhaps, there is another second useful mechanism - std::initializer_list .
  3. If clause 2. for some reason is not applicable, it is possible to pass lambda as an initializer, which will close all initializer parameters to itself.
  4. Deinitializator has a single parameter - the type of resource, but if necessary, it can also be a lambda, closing on itself additional parameters of deinitialization.
  5. This is much easier to implement than std::shared_ptr(T* ptr, deleter d) .


Disadvantages?
Sometimes it is still more effective to write a full-fledged resource vraper.

Need more examples? I have them:

Creating AVFormatContext context:
 ha::scoped_resource<ffmpeg::AVFormatContext*> formatctx (ffmpeg::avformat_alloc_context, ffmpeg::avformat_free_context); 


This is analogous to the following:
 std::shared_ptr<ffmpeg::AVFormatContext> formatctx = std::shared_ptr<ffmpeg::AVFormatContext> (ffmpeg::avformat_alloc_context(), ffmpeg::avformat_free_context); 


Or here's another, here is used composite deinitsializator:
 ha::scoped_resource<ffmpeg::AVCodecContext*> codecctx( ffmpeg::avcodec_alloc_context, [](ffmpeg::AVCodecContext* c) { ffmpeg::avcodec_close(c), ffmpeg::av_free(c); }); 


And this example is interesting because there is a seizure of a resource that does not need to be released:
 ha::scoped_resource<ffmpeg::AVCodec*, ffmpeg::AVCodecID> codec( ffmpeg::avcodec_find_decoder, codecctx->codec_id, [](__attribute__((unused)) ffmpeg::AVCodec* c) { }); 


And finally the simplest oneliner:
 ha::scoped_resource<ffmpeg::AVFrame*> frame(ffmpeg::avcodec_alloc_frame, ffmpeg::av_free); 


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

But is it really all about naked plain-C resources? And where are the examples with suitable C ++?
But:
 ha::mutex mutex; ha::scoped_resource<ha::mutex*, ha::mutex*> 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 implementation of the scoped_resource class is so simple and elegant that it even reminded me of the idea of Y-combinator 'a.
That is, it is possible to easily implement something like this by simply starting with the constructor declaration scoped_resource::scoped_resource(initializer_t, finalizer_t); and then build up the variadic-part for the parameters.

Well, but where is the implementation, anyway?
 template <typename T, typename... A> class scoped_resource { public: typedef std::function<T (A...)> initializer_t; typedef std::function<void(T)> 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 <typename Y> Y get() const { return static_cast<Y>(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

Source: https://habr.com/ru/post/172817/


All Articles