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 associatedonLeavingScope
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.
Minor update to the downloadable `OnLeavingScope.h` file to address a bug in the move constructor implementation.
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…
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!
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.
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; });
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!