Mix RAII & lambdas for deferred execution

Posted: August 27, 2012 in Programming Recipes
Tags: , , ,

Sometimes we want some code to be executed after (or just before) the end of a function, also if an exeception is thrown. Suppose to deal with a C API that requires an initialization call and a termination call, something like that:


void use_C_api()

{

   Init_Lib();

   code_that_could_throw();

   Terminate_Lib();

}

The first thing a C++ programmer could think about is to encapsulate the library usage in a wrapper and use RAII (Resource Acquisition is Initialization). For example:

class LibWrapper
{
public:
 LibWrapper()
 {
    Init_Lib();
 }

 ~LibWrapper()
 {
    Terminate_Lib();
 }

 Call_code_that_could_throw()
 {
    code_that_could_throw();
 }
};

...

void use_C_api()
{
   LibWrapper wrapper; // Init here
   wrapper.Call_code_that_could_throw();
} // Terminate here (also if an exception occurs)

In C++  the only code that can be guaranteed to be executed after an exception is thrown are the destructors of objects residing on the stack, so this is our case.

Ok cool, but what if we just want to execute some code in a function? Do we have to write classes? What if we need something like Java’s finally or Go’s defer? Sometimes this kind of constructs suffice.

RAII is perfect for our purpose, but we need to create on-the-fly a sort of “finalizer”, passing it some code. Thanks to C++11 we can use lambdas to be enough generic. So, this is my idea:

template <typename F>
struct finalizer
{
template<typename T>
finalizer(T&& f) : m_f(std::forward<T>(f)) {}

~finalizer()
{
   m_f();
}
private:
   F m_f;
};

template <typename F>
finalizer<F> defer(F&& f)
{
   return finalizer<F>(std::forward<F>(f));
}

...

auto atEnd = defer( []{cout << "I'm printed out at the end of the scope!" << endl;} );

The code is trivial: the finalizer uses a template to accept any kind of “executable object” (lambdas, functions, functors, etc). I created a simple factory method to avoid expliciting the template parameter (the compiler will do the work for us).

But wait…I spot at least a possible problem in this mechanism, do you?

We’re relying on our compiler, aren’t we? What happens if our compiler doesn’t use RVO (Return Value Optimization)? We’ll be in a proper jam! We get a copy and then two destructions that lead us to a couple of calls to our at-end-code!

Is it clear? If not, try to change the factory this way and you’ll get the same effect:


template <typename F>
finalizer<F> defer_ops(F&& f)
{
   auto f1 = finalizer<F>(std::forward<F>(f));
   return f1;
}

Now our code is called twice (first when the copied object is destroyed, second at the end of the calling code’s scope). A simple way to “fix” this problem is to add a move constructor and a bool flag:

template <typename F>
struct finalizer
{
template<typename T>
finalizer(T&& f) : m_f(std::forward<T>(f)), m_isMoved(false) {}
finalizer(finalizer&& other) : m_f(std::move(other.m_f)), m_isMoved(other.m_isMoved)
{
   other.m_isMoved = true; // sort of "resource stealing" here
}
~finalizer()
{
  if (!m_isMoved)
  {
    m_f();
  }
}
private:
 F m_f;
 bool m_isMoved;
};

I don’t like this fix but it’s not possible to copy-assign a lambda-expression to another one (e.g. a no-op one – the idea would be to “reset” the moved finalizer’s m_f to an “empty” lambda). In fact the standard says “The closure type associated with a lambda-expression has a deleted copy assignment operator“.

If you’re unlucky and you compiler does not support C++11 move semantics then change your compiler! Obviously I’m joking. Let’s make another (even uglier) extra:

template <typename F>
struct finalizer
{
  // Please change it to a ctor taking a universal reference as soon as possible (when C++11 is available)
  finalizer(F f) : m_f(f), m_isMoved(false) {}

  // Please change it to a move ctor as soon as possible (when C++11 is available)
  finalizer(const finalizer& other) : m_f(other.m_f), m_isMoved(false)
  {
     other.m_isMoved = true; // sort of "resource stealing" here
  }
  ~finalizer()
  {
    if (!m_isMoved)
    {
      m_f();
    }
  }
  private:
   F m_f;
   // Please remove this mutable as soon as possible (when C++11 is available)
   mutable bool m_isMoved;
};

The key here is to replace the move constructor with the copy constructor and to add the mutable qualifier to our m_isMoved flag. Why? To be standard (the standard copy constructor receives a const&) and clear (it’s a design choice, though it is nasty).

To sum up: we started from a sort of elegant and simple code and we sank into a worse patched one. This regression may be caused by compilers that poorly support modern C++. In five minutes we wrote a simple code that worked and that solved our problem. More time was spent to fix and to meet compiler’s deficiencies.

RVO is a stupid example because I think that almost all C++ compilers support it, but sometimes you maybe need to disable it for some reason. Your code has to be prepared and must keep on working. Then be aware that design decisions are often made not only because of “core-business” but also because of your tools and facilities that are also part of your priorities just because they support your production. The more bad decisions are made because of infrastructure, the more you’re probably in trouble!

So, don’t forget to keep the barrel clean!

[Edit] Thanks to tr3w for letting me know some typos [/Edit]

Advertisements
Comments
  1. tr3w says:

    You said the lambda expression has deleted copy constructor, but if it’s true, then even your first version is not compiling. (Looks like under g++ 4.7.1 you can copy the lambdas…)

    You can test it out with the following functor class:
    struct functor
    {
    functor() = default;
    functor(const functor&) = delete;
    functor(functor&&) = default;

    void operator() ()
    {
    std::cout << "I'm printed out at the end of the scope!" << std::endl;
    }
    };

    To make it work you need to change the constructor from
    finalizer(F&& f) : m_f(f) {}
    to:
    finalizer(F&& f) : m_f(std::forward(f)) {}

    Another little thing with the second version: if you move the finalizer class twice, you get the same “double destructor” problem, so you need to change the move constructor from
    finalizer(finalizer&& other) : m_f(other.m_f), m_isMoved(false)
    to
    finalizer(finalizer&& other) : m_f(other.m_f), m_isMoved(other.m_isMoved)

    And also a little typo:
    In the changed factory function you use “f” for two things, so it won’t compile.

    • Marco Arena says:

      Hi! You’re quite right, lambdas are perfectly copyable and I wrote a stupid thing. Thanks, I’ll fix.
      About the second point, yes, right again, but it’s just nonsense moving a finalizer twice (at least in simple situations I showed). Thanks again, I’ll also fix it!
      Third point…what a shame!
      You won 3-0, nothing to add 🙂

      • tr3w says:

        Yes, you are right, here it doesn’t make sense to move it twice (I think it never does… I wonder what does the standard say), and it’s not likely to happen anyway, but better safe than sorry, especially if the fix is that simple. 🙂

  2. […] idea of golang-style defer is explained here and […]

  3. […] idea of golang-style defer is explained here and […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s