TransWikia.com

C++20 ScopeGuard

Code Review Asked on November 15, 2021

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;
}

One Answer

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

Answered by L. F. on November 15, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP