This is actually not something new, but I think many people wanted something useful and not incredibly complicated. So, here it is:
#pragma once
#include <utility>
#define concat_impl(x, y) x##y
#define concat(x, y) concat_impl(x, y)
#ifdef __COUNTER__
#define ANONYMOUS_VARIABLE(NAME) concat(NAME, __COUNTER__)
#else
#define ANONYMOUS_VARIABLE(NAME) concat(NAME, __LINE__)
#endif
namespace detail
{
template <typename Func>
class ScopeGuardOnExit
{
Func f;
public:
ScopeGuardOnExit(Func&& f) :
f(std::forward<Func>(f))
{}
~ScopeGuardOnExit()
{
f();
}
};
struct dummy {};
template <typename Func>
ScopeGuardOnExit<Func> operator+(dummy, Func&& f)
{
return std::forward<Func>(f);
}
}
#define scope_exit \
auto ANONYMOUS_VARIABLE(SCOPE_EXIT_WHATEVER) = ::detail::dummy() + [&]()
Usage:
#include "scope_exit.hpp"
#include <iostream>
int f()
{
scope_exit {std::cout << "exiting f()\n";};
return 0;
}
int main()
{
f();
scope_exit {std::cout << "It's working!\n";};
}
Do note that the lambda takes (pulls into abyss) everything by reference. On top of that, it is possible to register multiple function to execute at exit. Is it possible to make it more user friendly? The best would be to try to write it using templates.
By being more user friendly I mean eliminating that SCOPE_EXIT_WHATEVERN
variable from the variable list for the current scope, because people will get surprised when they will see it in an IDE. Also, forgetting semicolon ;
will lead to pretty confusing error messages.
2 Answers 2
Between operator+
and the constructor of ScopeGuardOnExit
, you twice move-construct an object of the lambda type. In Auto() I deliberately avoided any move-constructions in favor of just storing a reference to the original lambda; if I recall correctly, that was because I had found that GCC had a hard time optimizing away those move-constructions (even though the lambda object doesn't contain any data members except references, which are trivially moveable and thus ought to be easy to optimize away).
However, I've just now tried to construct a test case where scope_exit
's assembly output differs (at -O2
or higher) from Auto
's, and failed to come up with any actual differences; so I think your move-construction-based version is safe in practice.
-
\$\begingroup\$ Hi! It might have been a long time, but when writing documentation I found that I should declare constructor as
noexcept(false)
, since if code inside of the lambda will throw, it will callstd::terminate()
(after C++11). I just thought that you're using something similar in your code base, so this is a potential danger. \$\endgroup\$Incomputable– Incomputable2017年02月25日 23:01:01 +00:00Commented Feb 25, 2017 at 23:01 -
\$\begingroup\$ You mean "destructor", not "constructor"; but yes, if your
f()
can throw then you ought to have thatnoexcept(false)
. But then you also might want to wrap everything inif (!std::current_exception())
or something so that you don't throw during stack-unwinding-due-to-a-throw; that's also a cause for callingstd::terminate()
. My preferred "solution" is just to make suref()
never throws anything. \$\endgroup\$Quuxplusone– Quuxplusone2017年02月26日 17:44:37 +00:00Commented Feb 26, 2017 at 17:44 -
\$\begingroup\$ may be it is possible to somehow deduce the
noexcept
specifier? I believe it became part of the type system lately. Also, it may be possible to "hide" the result ofstd::current_exceptions()
before performing+
, and then inside of the destructor check if the current number of exceptions exceed the previous recorded. I think that would transform it into something likeexit_failure
. \$\endgroup\$Incomputable– Incomputable2017年02月26日 17:47:19 +00:00Commented Feb 26, 2017 at 17:47 -
\$\begingroup\$
noexcept
is already "deduced" in the sense that it defaults totrue
for dtors andfalse
for everything else. People have proposed the syntaxnoexcept(auto)
for... something... but it's currently unclear what the "something" should be. \$\endgroup\$Quuxplusone– Quuxplusone2017年02月26日 17:54:30 +00:00Commented Feb 26, 2017 at 17:54 -
\$\begingroup\$ @Incomputable: You can already have
f()
"temporarily" throw an exception as long asf()
catches that exception too. The dangerous situation is wheref()
actually allows an exception to propagate out so that there are two exceptions trying to ascend the stack at once. That's a hard error because there's simply no way to assign a meaning to such a situation. So to the extent "hide the result of..." is possible, it's already the case. \$\endgroup\$Quuxplusone– Quuxplusone2017年02月26日 17:57:49 +00:00Commented Feb 26, 2017 at 17:57
Destructor has very dangerous behavior if anything inside of the code block may throw. Since C++11, every destructor is implicitly noexcept
, thus any exception thrown will immediately call std::terminate()
without any chance to recover.
The fix is simple, just modify destructor declaration:
~ScopeGuardOnExit() noexcept(false)
{
f();
}