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;
}
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 struct
s 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 move
s 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. union
s 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
.
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
Get help from others!
Recent Questions
Recent Answers
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP