8
\$\begingroup\$

The idea of a defer statement or a ScopeGuard has, for a while, been something I have wanted in C++, but up until recently I had always assumed that C++ did not have it. However, that's when I found std::experimental::scope_exit but it had a problem, which was that it had not been implemented in any of the compilers I use. As a result, I decided to implement it.

scope.hh

#ifndef SCOPE_SCOPE_HH
#define SCOPE_SCOPE_HH

#include <utility>
#include <new>
#include <concepts>

namespace turtle
{

    template<typename EF> requires std::invocable<EF> && requires(EF x) {{ x() } -> std::same_as<void>; }
    struct scope_exit
    {
        constexpr scope_exit operator=(const scope_exit &) = delete;

        constexpr scope_exit operator=(scope_exit &&) = delete;

        template<typename Fn, typename =
        std::enable_if_t<!std::is_same_v<std::remove_cvref_t<Fn>, scope_exit>, int>,
                typename = std::enable_if_t<std::is_constructible_v<EF, Fn>, int>>
        constexpr explicit scope_exit(Fn &&fn) noexcept(std::is_nothrow_constructible_v<EF, Fn> ||
                                                        std::is_nothrow_constructible_v<EF, Fn &>)
        {
            set_functor(std::forward<Fn>(fn));
        }

        template<typename = std::disjunction<std::enable_if_t<std::is_nothrow_move_constructible_v<EF>, int>,
                std::enable_if_t<std::is_nothrow_move_constructible_v<EF>, int>>>
        constexpr scope_exit(scope_exit &&other) noexcept(std::is_nothrow_move_constructible_v<EF> ||
                                                          std::is_nothrow_copy_constructible_v<EF>)
        {
            set_functor_move(std::move(other));
        }

        constexpr scope_exit(const scope_exit &) = delete;

        constexpr ~scope_exit() noexcept
        {
            /* if active, call functor and then destroy it */
            if (!m_released) {
                m_functor();
                m_released = true;
            }
            m_functor.~EF();
        }

        constexpr void release() noexcept
        {
            m_released = true;
        }

        constexpr const auto& exit_function() noexcept
        {
            return m_functor;
        }

    private:
        union
        {
            EF m_functor;
            char m_functor_bytes[sizeof(EF)] = {};
        };
        bool m_released{false};

        template<typename Fn>
        constexpr void set_functor(Fn &&fn) noexcept(std::is_nothrow_constructible_v<EF, Fn> ||
                                                     std::is_nothrow_constructible_v<EF, Fn &>)
        {
            if constexpr(!std::is_lvalue_reference_v<Fn> && std::is_nothrow_constructible_v<EF, Fn>) {
                ::new(&m_functor) EF(std::forward<Fn>(fn));
            } else {
                try {
                    ::new(&m_functor) EF(fn);
                } catch (...) {
                    m_released = true;
                    fn();
                    throw;
                }
            }
        }

        constexpr void set_functor_move(scope_exit &&other) noexcept(std::is_nothrow_move_constructible_v<EF> ||
                                                                     std::is_nothrow_copy_constructible_v<EF>)
        {
            /* only preform construction if other is active */
            if (!other.m_released) {
                if constexpr(std::is_nothrow_move_constructible_v<EF>) {
                    ::new(&m_functor) EF(std::forward<EF>(other.m_functor));
                } else {
                    try {
                        ::new(&m_functor) EF(other.m_functor);
                    } catch (...) {
                        m_released = true;
                        other.m_functor();
                        other.release();
                        throw;
                    }
                }
                other.release();
            }
        }
    };
    template<typename EF>
    scope_exit(EF) -> scope_exit<EF>;
}

namespace turtle
{
    template<typename EF> requires std::invocable<EF> && requires(EF x) {{ x() } -> std::same_as<void>; }
    struct scope_fail
    {
        constexpr scope_fail operator=(const scope_fail &) = delete;

        constexpr scope_fail operator=(scope_fail &&) = delete;

        template<typename Fn, typename =
        std::enable_if_t<!std::is_same_v<std::remove_cvref_t<Fn>, scope_fail>, int>,
                typename = std::enable_if_t<std::is_constructible_v<EF, Fn>, int>>
        constexpr explicit scope_fail(Fn &&fn) noexcept(std::is_nothrow_constructible_v<EF, Fn> ||
                                                        std::is_nothrow_constructible_v<EF, Fn &>)
                                                        : m_uncaught_exceptions(std::uncaught_exceptions())
        {
            set_functor(std::forward<Fn>(fn));
        }

        template<typename = std::disjunction<std::enable_if_t<std::is_nothrow_move_constructible_v<EF>, int>,
                std::enable_if_t<std::is_nothrow_move_constructible_v<EF>, int>>>
        constexpr scope_fail(scope_fail &&other) noexcept(std::is_nothrow_move_constructible_v<EF> ||
                                                          std::is_nothrow_copy_constructible_v<EF>)
                                                          : m_uncaught_exceptions(other.m_uncaught_exceptions)
        {
            set_functor_move(std::move(other));
        }

        constexpr scope_fail(const scope_fail &) = delete;

        constexpr ~scope_fail() noexcept
        {
            /* if active and an exception happened, call functor. */
            if (!m_released && std::uncaught_exceptions() > m_uncaught_exceptions) {
                m_functor();
            }
            /* destroy functor */
            m_functor.~EF();
        }

        constexpr void release() noexcept
        {
            m_released = true;
        }

        constexpr const auto& exit_function() noexcept
        {
            return m_functor;
        }

    private:
        union
        {
            EF m_functor;
            char m_functor_bytes[sizeof(EF)] = {};
        };
        bool m_released{false};
        int m_uncaught_exceptions{0};
        template<typename Fn>
        constexpr void set_functor(Fn &&fn) noexcept(std::is_nothrow_constructible_v<EF, Fn> ||
                                                     std::is_nothrow_constructible_v<EF, Fn &>)
        {
            if constexpr(!std::is_lvalue_reference_v<Fn> && std::is_nothrow_constructible_v<EF, Fn>) {
                ::new(&m_functor) EF(std::forward<Fn>(fn));
            } else {
                try {
                    ::new(&m_functor) EF(fn);
                } catch (...) {
                    m_released = true;
                    fn();
                    throw;
                }
            }
        }

        constexpr void set_functor_move(scope_fail &&other) noexcept(std::is_nothrow_move_constructible_v<EF> ||
                                                                     std::is_nothrow_copy_constructible_v<EF>)
        {
            /* only preform construction if other is active */
            if (!other.m_released) {
                if constexpr(std::is_nothrow_move_constructible_v<EF>) {
                    ::new(&m_functor) EF(std::forward<EF>(other.m_functor));
                } else {
                    try {
                        ::new(&m_functor) EF(other.m_functor);
                    } catch (...) {
                        m_released = true;
                        other.m_functor();
                        other.release();
                        throw;
                    }
                }
                other.release();
            }
        }
    };
    template<typename EF>
    scope_fail(EF) -> scope_fail<EF>;
}

namespace turtle
{
    template<typename EF> requires std::invocable<EF> && requires(EF x) {{ x() } -> std::same_as<void>; }
    struct scope_success
    {
        constexpr scope_success operator=(const scope_success &) = delete;

        constexpr scope_success operator=(scope_success &&) = delete;

        template<typename Fn, typename =
        std::enable_if_t<!std::is_same_v<std::remove_cvref_t<Fn>, scope_success>, int>,
                typename = std::enable_if_t<std::is_constructible_v<EF, Fn>, int>>
        constexpr explicit scope_success(Fn &&fn) noexcept(std::is_nothrow_constructible_v<EF, Fn> ||
                                                        std::is_nothrow_constructible_v<EF, Fn &>)
                : m_uncaught_exceptions(std::uncaught_exceptions())
        {
            set_functor(std::forward<Fn>(fn));
        }

        template<typename = std::disjunction<std::enable_if_t<std::is_nothrow_move_constructible_v<EF>, int>,
                std::enable_if_t<std::is_nothrow_move_constructible_v<EF>, int>>>
        constexpr scope_success(scope_success &&other) noexcept(std::is_nothrow_move_constructible_v<EF> ||
                                                          std::is_nothrow_copy_constructible_v<EF>)
                : m_uncaught_exceptions(other.m_uncaught_exceptions)
        {
            set_functor_move(std::move(other));
        }

        constexpr scope_success(const scope_success &) = delete;

        constexpr ~scope_success() noexcept(noexcept(std::declval<EF&>()()))
        {
            /* if active and an exception did not happen, call functor. */
            if (!m_released && std::uncaught_exceptions() <= m_uncaught_exceptions) {
                m_functor();
            }
            /* destroy functor */
            m_functor.~EF();
        }

        constexpr void release() noexcept
        {
            m_released = true;
        }

        constexpr const auto& exit_function() noexcept
        {
            return m_functor;
        }

    private:
        union
        {
            EF m_functor;
            char m_functor_bytes[sizeof(EF)] = {};
        };
        bool m_released{false};
        int m_uncaught_exceptions{0};
        template<typename Fn>
        constexpr void set_functor(Fn &&fn) noexcept(std::is_nothrow_constructible_v<EF, Fn> ||
                                                     std::is_nothrow_constructible_v<EF, Fn &>)
        {
            if constexpr(!std::is_lvalue_reference_v<Fn> && std::is_nothrow_constructible_v<EF, Fn>) {
                ::new(&m_functor) EF(std::forward<Fn>(fn));
            } else {
                try {
                    ::new(&m_functor) EF(fn);
                } catch (...) {
                    m_released = true;
                    fn();
                    throw;
                }
            }
        }

        constexpr void set_functor_move(scope_success &&other) noexcept(std::is_nothrow_move_constructible_v<EF> ||
                                                                     std::is_nothrow_copy_constructible_v<EF>)
        {
            /* only preform construction if other is active */
            if (!other.m_released) {
                if constexpr(std::is_nothrow_move_constructible_v<EF>) {
                    ::new(&m_functor) EF(std::forward<EF>(other.m_functor));
                } else {
                    try {
                        ::new(&m_functor) EF(other.m_functor);
                    } catch (...) {
                        m_released = true;
                        other.m_functor();
                        other.release();
                        throw;
                    }
                }
                other.release();
            }
        }
    };
    template<typename EF>
    scope_success(EF) -> scope_success<EF>;
}

#endif //SCOPE_SCOPE_HH

here is an example of it:

main.cc

#include <iostream>
#include <ctime>
#include <SDL.h>
#include "scope.hh"


int main(int, char*[])
{
    // Reseed rand
    std::srand(std::time(0));

    SDL_Init(SDL_INIT_VIDEO);              // Initialize SDL2

    // Create an application window with the following settings:
    SDL_Window* window = SDL_CreateWindow(
            "An SDL2 window",                  // window title
            SDL_WINDOWPOS_UNDEFINED,           // initial x position
            SDL_WINDOWPOS_UNDEFINED,           // initial y position
            640,                               // width, in pixels
            480,                               // height, in pixels
            SDL_WINDOW_OPENGL
    );

    // Check that the window was successfully created
    if (window == nullptr)
    {
        // In the case that the window could not be made...
        std::cerr << "Could not create window: " << SDL_GetError() << "\n";
        return 1;
    }

    // Clean up when exiting the scope
    turtle::scope_exit cleanup{[&]{
        SDL_DestroyWindow(window);
        SDL_Quit();
    }};

    // An exception could be thrown
    if(std::rand() % 4 == 0)
    {
        throw;
    }

    // The window is open: could enter program loop here

    SDL_Delay(3000);  // Pause execution for 3000 milliseconds, for example

    return 0;
}

\$\endgroup\$
  • 1
    \$\begingroup\$ I don't understand. What's this scope exit? \$\endgroup\$ – ALX23z Jul 22 '20 at 12:58
  • 2
    \$\begingroup\$ @ALX23z: it's a way to register a function that is called when the scope ends. For example: { scope_exit foo([]{std::cout << "scope exit\n";}); if(rand()&1) throw std::runtime_error("onoes!"); std::cout << "hello\n"; } Here, the text scope exit will always be printed, if there was no uncaught exception it will be done after printing hello. The above code also has variants that only trigger if there was an exception or if there were none. @user222866: it would help if you added an example of how you intend your code to be used. \$\endgroup\$ – G. Sliepen Jul 22 '20 at 22:26
  • 1
    \$\begingroup\$ I added an example of usage \$\endgroup\$ – nullptr Jul 23 '20 at 12:10
3
\$\begingroup\$

Bugs

It isn't necessary to require x() to return exactly void — in fact, any return type would do, since we can discard the result. So, the requirements on the structs should be simplified to template <std::invocable EF>.

This code is problematic:

template<typename = std::disjunction<std::enable_if_t<std::is_nothrow_move_constructible_v<EF>, int>,
        std::enable_if_t<std::is_nothrow_move_constructible_v<EF>, int>>>
  • std::disjunction doesn't work the way you think it does;

  • one of the moves should be copy;

  • SFINAE with std::enable_if_t needs to depend on a template parameter of the function template.

So it should be

template <typename EF2 = EF, typename = std::enable_if_t<
       std::is_nothrow_move_constructible_v<EF2>
    || std::is_nothrow_copy_constructible_v<EF2>
>>

or just use requires.

You'll need to special-case references. unions containing references are ill-formed IIRC.

Don't mark things as constexpr unless the spec says so if your objective is conformance — the standard prohibits implementations from adding constexpr.

Non-bugs

As I said before, you can use requires instead of enable_if in many cases.

You don't need a union-simulated std::optional to store the exit function, because all scope guards (including inactive ones) keep their exit function alive. (per comment) For the move constructor, use std::move_if_noexcept for the noexcept dispatch behavior; for example:

scope_exit(scope_exit&& other)
    noexcept(std::is_nothrow_move_constructible_v<EF> ||
             std::is_nothrow_copy_constructible_v<EF>)
    requires std::is_nothrow_move_constructible_v<EF> ||
             std::is_copy_constructible_v<EF>
    : m_functor(std::move_if_noexcept(other.m_functor))
{
    other.release();
}
\$\endgroup\$
  • \$\begingroup\$ Using std::optional instead of that broken hack with the union is a good suggestion… but a better one would be to ask why all that is necessary at all. It looks like the only point where the object doesn’t have an active m_functor is in the moments right after construction, which means all that song-and-dance with the unions is just to emulate ordinary construction. \$\endgroup\$ – indi Jul 24 '20 at 22:49
  • \$\begingroup\$ In other words, why couldn’t the move constuctor just be basically constexpr scope_exit(scope_exit&& other) : m_functor{std::move(other.m_functor)} { other.m_released = true; }, and the converting constructor template <typename Fn> constexpr explicit scope_exit(Fn&& fn) : m_functor(std::forward<Fn>(fn)) {} (plus the noexcept specifiers, omitted for brevity)? \$\endgroup\$ – indi Jul 24 '20 at 22:49
  • \$\begingroup\$ @indi The dispatch behavior is mandated by the spec. As for optional, yes, you're right, seems that inactive scope_exits keep their function. I'll edit the answer. \$\endgroup\$ – L. F. Jul 25 '20 at 5:04

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.