3
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$

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.

\$\endgroup\$
  • \$\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 call std::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 Feb 25 '17 at 23:01
  • \$\begingroup\$ You mean "destructor", not "constructor"; but yes, if your f() can throw then you ought to have that noexcept(false). But then you also might want to wrap everything in if (!std::current_exception()) or something so that you don't throw during stack-unwinding-due-to-a-throw; that's also a cause for calling std::terminate(). My preferred "solution" is just to make sure f() never throws anything. \$\endgroup\$ – Quuxplusone Feb 26 '17 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 of std::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 like exit_failure. \$\endgroup\$ – Incomputable Feb 26 '17 at 17:47
  • \$\begingroup\$ noexcept is already "deduced" in the sense that it defaults to true for dtors and false for everything else. People have proposed the syntax noexcept(auto) for... something... but it's currently unclear what the "something" should be. \$\endgroup\$ – Quuxplusone Feb 26 '17 at 17:54
  • \$\begingroup\$ @Incomputable: You can already have f() "temporarily" throw an exception as long as f() catches that exception too. The dangerous situation is where f() 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 Feb 26 '17 at 17:57
0
\$\begingroup\$

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();
    }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.