Professional CMake:

A Practical Guide

Learn to use CMake effectively with practical advice from a CMake co-maintainer. You can also have the author work directly with your team!

OnLeavingScope: The sequel

In a previous article, the OnLeavingScope class was presented as a technique for robustly and concisely handling scenarios involving multi-step setup, run and cleanup stages. It focused on ease of learning and wide applicability rather than performance, so while the implementation was complete, it was not necessarily optimal. This article picks up where the previous article left off and deals with some of the more advanced aspects to provide some improvements.

Since this article covers more advanced topics, the reader should have some familiarity with the difference between rvalues and lvalues, along with an understanding of move semantics. I highly recommend Thomas Becker’s excellent discussion on these topics, as well as Scott Meyers’ recent publication Effective Modern C++ if you want to get up to speed on these areas.

Quick recap

For reference, the basic OnLeavingScope implementation which we will build on is repeated here:

#include <functional>

class OnLeavingScope
{
public:
    // Use std::function so we can support
    // any function-like object
    using Func = std::function<void()>;

   // Prevent copying
    OnLeavingScope(const OnLeavingScope&) = delete;
    OnLeavingScope& operator=(const OnLeavingScope&) = delete;

    OnLeavingScope(const Func& f) :m_func(f) {}
    ~OnLeavingScope(){m_func();}

private:
    Func m_func;
};

Lvalues, rvalues, copies and moves

The basic OnLeavingScope implementation does not distinguish between lvalue and rvalue function objects given to its constructor. In all cases, it just treats the function object as a const lvalue, which will usually result in the object being copied. If the object passed in is an rvalue, however, the lack of a constructor which accepts an rvalue function object means we miss an opportunity to move the object into m_func instead of copying it. If the function object is non-trivial to copy but cheap to move, we miss out on a potential performance improvement.

In order to demonstrate some interesting differences, we will make use of the following function object rather than using lambdas. The Printer class logs every object construction along with information about how each object is created:

class Printer
{
public:
    Printer(const std::string& msg) :m_msg(msg)
    {
        std::cout << "Created" << std::endl;
    }

    Printer(const Printer& p) :m_msg(p.m_msg)
    {
        std::cout << "Copied" << std::endl;
    }

    Printer(Printer&& p) :m_msg(std::move(p.m_msg))
    {
        std::cout << "Moved" << std::endl;
    }

    void operator()() const
    {
        std::cout << m_msg << std::endl;
    }

private:
    std::string m_msg;
};

Now let’s consider the following short test function:

void tester()
{
    Printer p("lvalue");
    OnLeavingScope olsLValue(p);

    std::cout << "----" << std::endl;
    OnLeavingScope olsRValue(Printer("rvalue"));

    std::cout << "----" << std::endl;
}

With our basic OnLeavingScope implementation, calling tester leads to the following output with one particular compiler (clang in this case):

Created
Copied
Moved
Copied
----
Created
Moved
Copied
----
rvalue
lvalue

The number of copy and move operations may be a surprise at first. We can get a clue to what is going on if we replace the definition of Func with Printer like so:

class OnLeavingScope
{
public:
    using Func = Printer;

    ...
};

With this modification, the output changes to:

Created
Copied
----
Created
Copied
----
rvalue
lvalue

So just by our use of std::function to hold the function object to be invoked, we have somehow accumulated extra copy/move operations. We will revisit this later, but for now, we want to focus on how to improve efficiency by avoiding unnecessary copies at the OnLeavingScope level. Let us see what happens if we add a constructor to OnLeavingScope which accepts a function object by rvalue reference:

class OnLeavingScope
{
public:
    using Func = std::function<void()>;

    OnLeavingScope(const Func& f) : m_func(f) {}
    OnLeavingScope(Func&& f) : m_func(std::move(f)) {}

    ...
};

The output of invoking tester for the two different definitions of Func now looks like this:

// Using Func = std::function
Created
Copied
Moved
----
Created
Moved
----
rvalue
lvalue

// Using Func = Printer
Created
Copied
----
Created
Moved
----
rvalue
lvalue

By adding the additional constructor to OnLeavingScope, we have avoided one copy for both the lvalue and rvalue cases when using std::function. When using Func = Printer directly, we have a move instead of a copy for the rvalue case. The results may vary from compiler to compiler depending on what optimizations it performs, but it should be clear that there are potential performance gains to be made by adding a constructor which accepts function objects by rvalue reference.

An alternative way to store the function object

By employing std::function to hold the function object in OnLeavingScope, we gained the ability to pass any function-like object to be executed in OnLeavingScope‘s destructor. We can give it concrete function objects (like in our Printer example above), a lambda function or even a pointer to an ordinary free function. As the previous section highlighted, however, there can be costs associated with using std::function.

We’ve already seen how we can incur extra copy or move operations, but we didn’t explore why. It turns out that std::function is potentially quite involved. Depending on the type of object given to it, it may create a copy on the heap or it might store a copy directly in a member variable without dynamic allocation. So in addition to the extra move/copy operations, there is potentially a dynamic memory allocation and deallocation being performed too. This is all quietly hidden away from clients of OnLeavingScope and the potential performance cost may come as a surprise.

A further downside is a consequence of std::function‘s flexibility to use static or dynamic storage depending on the type of object given to it: the size of a std::function instance can be non-trivial. For example, the compiler used to produce the above output reports sizeof(std::function) as 48 bytes. This may seem unexpectedly high if we are passing a very simple object such as a pointer to a free function.

Templatizing on the function type

With these drawbacks, it is reasonable to consider if there is a better alternative to std::function. In particular, since the type of function object being held by an OnLeavingScope instance will never change over its lifetime, we don’t need the run-time flexibility std::function provides. Rather, we would ideally encode the type of function object into OnLeavingScope, which we can achieve by making OnLeavingScope a template class and moving Func to become a template parameter:

// This has problems, discussed later
template<typename Func>
class OnLeavingScope
{
public:
    // Prevent copying
    OnLeavingScope(const OnLeavingScope&) = delete;
    OnLeavingScope& operator=(const OnLeavingScope&) = delete;

    OnLeavingScope(const Func& f) :m_func(f) {}
    OnLeavingScope(Func&& f) :m_func(std::move(f)) {}

     ~OnLeavingScope(){m_func();}

private:
    Func m_func;
};

Now a copy of the function object will always be stored directly in m_func without dynamic memory allocation (unless the function object itself uses it in its move or copy constructor). In addition, we know exactly what is being stored, precisely one copy of the function object with no hidden extras.

Deducing the function type

But now we have the problem of how to provide the template parameter. Indeed, if our function object is a lambda, it’s even worse – it is impossible to explicitly provide a lambda’s type, since only the compiler knows that type. The only way to provide the template parameter in that case is to let the compiler somehow deduce it for us, which we can achieve by using a factory function to create the OnLeavingScope object:

// Not quite correct, but close
template<typename Func>
OnLeavingScope<Func> onLeavingScope(Func&& f)
{
    return OnLeavingScope<Func>(std::forward<Func>(f));
}

Conceptually, the above expresses what we want, but it doesn’t quite do what we might naively expect. It is worth pointing out that because onLeavingScope is a function template, its Func&& f parameter isn’t necessarily a rvalue, but could be either a lvalue or rvalue depending on the argument passed to it. Scott Meyers refers to these as Universal references, a concept which he explains in depth in Effective Modern C++, so I won’t go into it here. The main implication we need to be aware of is that the type deduced for Func isn’t always going to be T, but will be T& for lvalues and T for rvalues (where T is the actual type of the function object and perhaps what we were naively expecting).

The fact that for lvalues we get T& instead of just T creates a problem for the OnLeavingScope constructors. If we substitute Func = T&, our constructor taking an lvalue function object becomes:

OnLeavingScope(const T&& f)

Now the problem is clear, it has become a move constructor taking a rvalue and hence we have no constructor which takes a lvalue function object any more. The compiler will also likely complain about multiple overloads instantiating to the same signature since we already have a constructor defined which takes a rvalue function object (clang issues such an error message, for example).

To address this problem, we need to strip off any reference from the type we use as the OnLeavingScope template parameter. The wonderful people who gave us C++11 foresaw situations like this and provided std::remove_reference to do exactly that, but it turns out to be more useful to use another function, std::decay, instead. While both successfully strip off the reference part, std::decay also performs function-to-pointer conversion, allowing us to pass free functions directly without having to take their address (STL algorithms typically have this feature).

// Correct, but has consequences
template<typename Func>
OnLeavingScope<typename std::decay<Func>::type>
onLeavingScope(Func&& f)
{
    return OnLeavingScope<typename std::decay<Func>::type>(std::forward<Func>(f));
}

This could be made a little more readable with C++14, but that’s going beyond the scope of what we want to focus on here. See Item 3 of Scott Meyers’ Effective Modern C++ or another suitable reference for further details. As it now stands, this factory function will pass through to OnLeavingScope precisely the function object type we want.

Addressing movability

Surely we’ve now got this right? Sadly, no, but don’t dispair, there is a light at the end of this tunnel, but there’s still a plot twist or two before we get there. Consider how we would now use this factory function:

auto cleanup = onLeavingScope([]{ ... });

This still concisely expresses what we want this line to do. You can even read it like regular English and it accurately self-describes the code. But you may rightly have a niggling feeling that something is amiss here since we explicitly deleted OnLeavingScope‘s copy constructor and copy assignment operator, but the factory function seems to require at least the former. Furthermore, we also want to ensure that we only invoke the function object once and only at the point where our cleanup object goes out of scope. On the surface, the use of a factory function to create the cleanup object seems problematic, and while this code does indeed fail to compile, it is not because of the deleted copy constructor or copy assignment operator. The real problem here is that OnLeavingScope does not have a move constructor.

With no move constructor, the factory function requires a copy constructor to be able to pass out the OnLeavingScope object, but we have rightfully deleted the copy constructor to prevent the function object being invoked more than once. By adding a move constructor, we allow the OnLeavingScope object created inside the factory function to be moved into the cleanup local variable. So as long as the move constructor ensures the moved-from object no longer tries to call its function object in its destructor, the expected behaviour is preserved.

What this means in practice is that our move constructor needs to somehow mark the moved-from object as “don’t invoke the function object”. We can’t just nullify the m_func data member, since that might not make sense for the particular function type (and indeed generally won’t make sense except for free functions). The cleanest approach is to add a boolean member which indicates whether or not to invoke m_func in the destructor.

Taking all of this into account, we (finally!) arrive at our fully working, caveat-free implementation (direct download link provided at the end of this article):

template<typename Func>
class OnLeavingScope
{
public:
    // Prevent copying
    OnLeavingScope(const OnLeavingScope&) = delete;
    OnLeavingScope& operator=(const OnLeavingScope&) = delete;

    // Allow moving
    OnLeavingScope(OnLeavingScope&& other) :
        m_func(std::move(other.m_func)),
        m_active(other.m_active)
    {
        other.m_active = false;
    }

    // Accept lvalue function objects
    OnLeavingScope(const Func& f) :
        m_func(f),
        m_active(true)
    {
    }

    // Accept rvalue function objects
    OnLeavingScope(Func&& f) :
        m_func(std::move(f)),
        m_active(true)
    {
    }

    // Only invoke function object if it
    // hasn't been moved elsewhere
    ~OnLeavingScope()
    {
        if (m_active)
            m_func();
    }

private:
    Func m_func;
    bool m_active;
};

template<typename Func>
OnLeavingScope<typename std::decay<Func>::type>
onLeavingScope(Func&& f)
{
    return OnLeavingScope<typename std::decay<Func>::type>(std::forward<Func>(f));
}

We would use this as in the following examples:

// Note non-void return type to demonstrate
// we are not restricted to void, unlike
// the std::function approach which is.
bool freeFunc()
{
    std::cout << "free function" << std::endl;
    return true;
}

void tester()
{
    auto p = Printer("lvalue");
    auto olsLValue = onLeavingScope(p);
    std::cout << "Printer lvalue size = "
              << sizeof(olsLValue) << std::endl;

    std::cout << "----" << std::endl;
    auto olsRValue = onLeavingScope(Printer("rvalue"));
    std::cout << "Printer rvalue size = "
              << sizeof(olsRValue) << std::endl;

    std::cout << "----" << std::endl;
    auto olsLambda = onLeavingScope(
    []{ std::cout << "lambda" << std::endl; });
    std::cout << "Lambda size = "
              << sizeof(olsLamba) << std::endl;

    std::cout << "----" << std::endl;
    auto olsFreeFunc = onLeavingScope(&freeFunc);
    std::cout << "Free function size = "
              << sizeof(olsFreeFunc) << std::endl;

    std::cout << "----" << std::endl;
}

which produces the following output with one particular compiler (clang on Mac OSX):

Created
Copied
Printer lvalue size = 32
----
Created
Moved
Printer rvalue size = 32
----
Lambda size = 2
----
Free function size = 16
----
free function
lambda
rvalue
lvalue

A final observation on move

We had to go through quite a bit of work to ensure the move functionality works robustly. Ironically though, the compiler will most likely optimize away the move constructor completely so it never gets invoked. This makes it seem like we don’t need to implement it and consequently don’t need the m_active member variable, but the compiler will flag an error without it and if we just declare it without any definition, the linker will fail instead. This is the case even though the move constructor may never be called! The reader is referred to this reference for a more detailed discussion of this topic. Rather than relying on the optimization from a particular compiler, the material presented here was chosen to be robust and technically correct in all situations.

Comparison with ScopeGuard

A few words on how this relates to Andrei Alexandrescu’s ScopeGuard11 (an updated version of the original ScopeGuard) may be helpful. The final version of OnLeavingScope presented here is essentially the same as ScopeGuard11, except ScopeGuard11 includes an additional dismiss() function to set m_active to false. The factory function presented here is also similar to the one from Andrei, but with an important difference. Andrei’s factory function only handles rvalue function objects, whereas the factory function in this article uses std::remove_reference to allow it to accept both rvalue and lvalue function objects. It should be noted that this deficiency in ScopeGuard11 appears to have been addressed in the code made publicly available on github under an Apache license as part of Facebook’s folly library. Andrei’s presentation covering ScopeGuard11 is available on msdn.

In this article and the previous one on the same topic, I have used the name OnLeavingScope rather than ScopeGuard. I believe this leads to more naturally readable code. Andrei’s presentation adds a further macro and some operator scaffolding for invoking the factory function, but using the name OnLeavingScope seems to make that unnecessary and we can avoid the added complexity.

Also note that similar material is currently part of a draft proposal to the C++ standards committee, referencing the same work by Andrei Alexandrescu that led to the folly implementation. That proposal, written by Peter Sommerlad and Andrew L. Sandoval, currently favors the name scope_exit for the class and make_scope_exit for the factory function. It also uses std::remove_reference rather than std::decay in the factory function.

Summary

So let’s recap what has been achieved by the final implementation:

  • Creating an OnLeavingScope object via its associated onLeavingScope factory function yields robust, concise, self-documenting code.
  • Dynamic memory allocation is not required to store the function object.
  • The amount of storage needed for a particular function object type is minimized to just that needed to hold a copy of the function object, plus an additional bool to track move operations.
  • We can handle any kind of function object that takes no parameters. Unlike the std::function approach, the return type of the function object can be anything.

The final version of OnLeavingScope and its factory function can be downloaded from here.

8 thoughts on “OnLeavingScope: The sequel”

  1. Excellent article. I’m pretty sure I’ll be using your OnLeavingScope class in my future projects.

    I think the flow could be helped by an almost obsolete construct – #define. For example:

    #define onLeaving(name, method) auto name = onLeavingScope([]{method;})

    which would allow for

    onLeaving(cleanup, device.close());

    looks better than

    auto cleanup = onLeavingScope([]{device.cleanup();});

    I believe…

    Reply
    • The folly library and Andrei himself provided such a #define macro, but I see a number of drawbacks:

      – It results in what looks like a regular function call, so a casual reader could incorrectly assume that the method part (device.close() in your case) is executed immediately. When you have to use the lambda syntax, it is clear you are defining something rather than executing that code right away.

      – The #define macro assumes no lambda captures, but quite often you want to have access to variables that are in scope, which means you would want to capture them. The #define has to make assumptions about what captures you want to allow.

      – Using a #define forces you to add extra parentheses around anything in your method part which needs to use a comma. Specifically, if you need to refer to a template which has more than one template parameter and you need to explicitly list those template arguments, or even if you just want to call a function which has more than one argument, you have to surround the whole thing in parentheses so that the preprocessor handles it correctly.

      I personally don’t think the #define improves things all that much. Without it, the code is very clear in what it is doing and is still relatively concise, whereas the #define approach comes with more caveats than I’d typically want to have. That said, I understand that for simple cases, some may find the #define attractive. If it works for you, then more power to you!

      Reply
  2. Thanks for your article!

    Apart from the know how of copying or moving closure or functor, don’t you think it is enough for OnLeavingScope to keep the reference instead of copying or moving?

    I hardly can imagine a situation where OnLeavingScope instance can outlive the closure or functor.

    Reply
    • It needs to take a copy to be able to support accepting rvalues in the OnLeavingScope constructor. If it only used a reference, that reference would be referring to a destroyed object for common cases like:


      auto olsRValue = onLeavingScope(Printer("rvalue"));
      auto olsLambda = onLeavingScope([]{ std::cout << "lambda" << std::endl; });

      Reply
      • Thank you for the reply.
        Originally I thought above cases should fall into the cases where temporary objects’ lifetime is extended as https://en.cppreference.com/w/cpp/language/lifetime says

        “The lifetime of a temporary object may be extended by binding to a const lvalue reference or to an rvalue reference (since C++11)”

        But it looks like move-constructing from rvalue is not such a case and I am wrong.

        Thank you!

        Reply

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.