5
\$\begingroup\$

I want to use the scope_exit class but my compiler/standard library (clang++-16 with libc++) don't support it. Until they do I wanted an implementation. I found a few on the Internet but they weren't quite what I wanted. So I made an attempt at implementing it.

I used requires clauses on the constructor overloads to implement the forwarding behavior described on the cppreference.com pages.

I came up with a brief suite of test objects (see test_scope_exit.cpp below) but it's paltry; what are more test cases where this should work? What are some examples of when it should not work so I can check if the compiler gives good diagnostic messages?

How could the code be improved for clarity, idiosyncrasy, etc?

scope_exit.hpp:

#ifndef SCOPE_EXIT
#define SCOPE_EXIT

#include <type_traits>
#include <utility>

template <typename EF> class Scope_Exit {
private:
  template <typename Fn>
  static inline constexpr bool noexcept_ctor =
      std::is_nothrow_constructible_v<EF, Fn> ||
      std::is_nothrow_constructible_v<EF, Fn &>;

  static inline constexpr bool noexcept_move =
      std::is_nothrow_move_constructible_v<EF> ||
      std::is_nothrow_copy_constructible_v<EF>;

public:
  template <typename Fn>
    requires(!std::is_lvalue_reference_v<Fn> &&
             std::is_nothrow_constructible_v<EF, Fn>)
  explicit Scope_Exit(Fn &&fn) noexcept(noexcept_ctor<Fn>)
      : exitfun(std::forward<Fn>(fn)) {}

  template <typename Fn>
  explicit Scope_Exit(Fn &&fn) noexcept(noexcept_ctor<Fn>) : exitfun(fn) {}

  Scope_Exit(Scope_Exit &&other) noexcept(noexcept_move)
    requires std::is_nothrow_move_constructible_v<EF>
      : active(other.active), exitfun(std::forward<EF>(other.exitfun)) {
    other.release();
  }

  Scope_Exit(Scope_Exit &&other) noexcept(noexcept_move)
      : active(other.active), exitfun(other.exitfun) {
    other.release();
  }

  Scope_Exit(const Scope_Exit &) = delete;
  Scope_Exit &operator=(Scope_Exit &&) = delete;
  Scope_Exit &operator=(const Scope_Exit &) = delete;

  ~Scope_Exit() noexcept {
    if (active)
      exitfun();
  }

  void release() noexcept { active = false; }

private:
  bool active = true;
  EF exitfun;
};

template <class EF> Scope_Exit(EF) -> Scope_Exit<EF>;

#endif // SCOPE_EXIT

test_scope_exit.cpp:

#include "../src/scope_exit.hpp"
#include <iostream>
#include <utility>
#include <functional>

void foo() {
  std::cout << "foo()" << std::endl;
}

void bar(int x) {
  std::cout << "bar() => " << x << std::endl;
}

class Baz {
public:
  Baz() = default;
  Baz(const Baz &other) { std::cout << "copied Baz" << std::endl; }
  void operator()() { std::cout << "Baz::operator()" << std::endl; }
};

int main() {
  Scope_Exit se1(foo);
  Scope_Exit se2(&foo);
  void (&foo_ref)() = foo;
  Scope_Exit se3(foo_ref);
  Scope_Exit se4([]{ std::cout << "lambda" << std::endl; });
  Scope_Exit se5(std::bind(bar, 2));
  int y = 5;
  Scope_Exit se6([&]{ std::cout << "capturing lambda => " << y << std::endl; });
  Baz baz;
  Scope_Exit se7(baz);
  Baz &baz_ref = baz;
  Scope_Exit se8(baz_ref);
  Scope_Exit<Baz &> se9(baz_ref); // explicitly specifying a reference type prevents copy
  return 0;
}

result:

copied Baz
copied Baz
Baz::operator()
Baz::operator()
Baz::operator()
capturing lambda => 5
bar() => 2
lambda
foo()
foo()
foo()
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Name it scope_exit

If you want to have a drop-in replacement for std::experimental::scope_exit, give it the same name. You could put it in your own namespace if you want. This allows you to write code like:

using mynamespace::scope_exit;
...
scope_exit se(somefunc);

And once scope_exit becomes available in the implementation of the standard library you are using, you can simply replace the first line by:

using std::experimental::scope_exit;

And no other code needs to be changed.

Can exit functions throw?

According to cppreference:

The behavior is undefined if calling fn() throws an exception or results in undefined behavior, even if fn has not been called.

And std::experimental::exit_scope's destructor is unconditionally marked noexcept. However, I think that's a bit unsafe: it still allows you to pass a throwing function, and if it throws you cannot catch it. I think we can do better, but there are two options:

  1. Restrict EF to functions that are noexcept. You can do this by creating a suitable concept.
  2. Make the destructor noexcept only if EF is.

Test cases

I came up with a brief suite of test objects (see test_scope_exit.cpp below) but it's paltry; what are more test cases where this should work?

  • You tested the copy constructor and various kinds of functions. However, you did not test that the exit functions are called in the reverse order that the exit scope were constructed.
  • You only test normal scope exit, but std::experimental::exit_scope also calls the exit function when the scope exits abnormally, for example if an exception is thrown that is not caught inside the scope. You should test that as well.
  • You never call release() in your test cases.
  • You never test move constructing from an inactive scope.

What are some examples of when it should not work so I can check if the compiler gives good diagnostic messages?

  • Copy-constructing and assignment should not work. You can check this programmatically (for example, static_assert(!std::is_copy_constructible<…>)).
  • Anything you check for using requires should be verified in the test suite. Of course, checking for a compile failure is harder.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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