forked from davisking/dlib
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[scope] Adding scope_exit (davisking#2875)
* initial commit * is this enough? * is this enough docs? I'm not great at writing docs --------- Co-authored-by: pf <pf@me> Co-authored-by: pf <pf@pf>
- Loading branch information
1 parent
afede57
commit 3624bf9
Showing
3 changed files
with
261 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// Copyright (C) 2023 Davis E. King ([email protected]) | ||
// License: Boost Software License See LICENSE.txt for the full license. | ||
#ifndef DLIB_SCOPE_H_ | ||
#define DLIB_SCOPE_H_ | ||
|
||
#include <utility> | ||
#include <functional> | ||
#include <type_traits> | ||
|
||
namespace dlib | ||
{ | ||
|
||
// ---------------------------------------------------------------------------------------- | ||
|
||
template<class Fn> | ||
class scope_exit | ||
{ | ||
/*! | ||
WHAT THIS OBJECT REPRESENTS | ||
This is a standard's compliant backport of std::experimental::scope_exit that works with C++14. | ||
Therefore, refer to https://en.cppreference.com/w/cpp/experimental/scope_exit for docs on the | ||
interface of scope_exit. | ||
!*/ | ||
|
||
private: | ||
Fn f_; | ||
bool active_{true}; | ||
|
||
public: | ||
constexpr scope_exit() = delete; | ||
constexpr scope_exit(const scope_exit &) = delete; | ||
constexpr scope_exit &operator=(const scope_exit &) = delete; | ||
constexpr scope_exit &operator=(scope_exit &&) = delete; | ||
|
||
constexpr scope_exit(scope_exit &&other) noexcept(std::is_nothrow_move_constructible<Fn>::value) | ||
: f_{std::move(other.f_)}, active_{std::exchange(other.active_, false)} | ||
{} | ||
|
||
template< | ||
class F, | ||
std::enable_if_t<!std::is_same<std::decay_t<F>, scope_exit>::value, bool> = true | ||
> | ||
explicit scope_exit(F&& f) noexcept(std::is_nothrow_constructible<Fn,F>::value) | ||
: f_{std::forward<F>(f)}, active_{true} | ||
{} | ||
|
||
~scope_exit() noexcept | ||
{ | ||
if (active_) | ||
f_(); | ||
} | ||
|
||
void release() noexcept { active_ = false; } | ||
}; | ||
|
||
template<class Fn> | ||
auto make_scope_exit(Fn&& f) | ||
/*! | ||
ensures: | ||
- This is factory function that wraps the callback in a scope_exit object. | ||
!*/ | ||
|
||
{ | ||
return scope_exit<std::decay_t<Fn>>(std::forward<Fn>(f)); | ||
} | ||
|
||
#ifdef __cpp_deduction_guides | ||
template<class Fn> | ||
scope_exit(Fn) -> scope_exit<Fn>; | ||
#endif | ||
|
||
// ---------------------------------------------------------------------------------------- | ||
|
||
using scope_exit_erased = scope_exit<std::function<void()>>; | ||
/*! | ||
WHAT THIS OBJECT REPRESENTS | ||
This is a type erased version of scope_exit. I.e. there is no template parameter. | ||
Use this object if you wish to hide the exact function signature, for example | ||
if splitting a declaration and definition across a header file and cpp file. | ||
This does come at a slight performance penalty since it may incur a heap allocation | ||
and due to a pointer indirection, the compiler may not inline your callback. | ||
!*/ | ||
|
||
// ---------------------------------------------------------------------------------------- | ||
|
||
} | ||
|
||
#endif //DLIB_SCOPE_H_ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
// Copyright (C) 2023 Davis E. King ([email protected]) | ||
// License: Boost Software License See LICENSE.txt for the full license. | ||
|
||
#include <cstdlib> | ||
#include <cstring> | ||
#include <dlib/scope.h> | ||
#include "tester.h" | ||
|
||
namespace | ||
{ | ||
using namespace test; | ||
using namespace dlib; | ||
|
||
logger dlog("test.scope"); | ||
|
||
// --------------------------------------------------------------------------------------------------- | ||
|
||
void test_scope_exit() | ||
{ | ||
int counter{0}; | ||
|
||
{ | ||
auto s1 = make_scope_exit([&]{++counter;}); | ||
static_assert(!std::is_copy_constructible<decltype(s1)>::value, "bad"); | ||
static_assert(!std::is_copy_assignable<decltype(s1)>::value, "bad"); | ||
static_assert(!std::is_move_assignable<decltype(s1)>::value, "bad"); | ||
static_assert(std::is_move_constructible<decltype(s1)>::value, "bad"); | ||
auto s2 = std::move(s1); | ||
auto s3 = std::move(s2); | ||
auto s4 = std::move(s3); | ||
} | ||
|
||
DLIB_TEST(counter == 1); | ||
|
||
const auto fn_inner = [&] | ||
{ | ||
auto s = make_scope_exit([&]{++counter;}); | ||
return s; | ||
}; | ||
|
||
const auto fn_outer = [&] | ||
{ | ||
auto s = fn_inner(); | ||
return s; | ||
}; | ||
|
||
{ | ||
auto s = fn_outer(); | ||
} | ||
|
||
DLIB_TEST(counter == 2); | ||
|
||
#ifdef __cpp_deduction_guides | ||
|
||
{ | ||
scope_exit s{[&]{++counter;}}; | ||
} | ||
|
||
DLIB_TEST(counter == 3); | ||
#endif | ||
|
||
} | ||
|
||
// --------------------------------------------------------------------------------------------------- | ||
|
||
|
||
void test_scope_exit_erased() | ||
{ | ||
int counter{0}; | ||
|
||
{ | ||
scope_exit_erased s1([&]{++counter;}); | ||
static_assert(!std::is_copy_constructible<decltype(s1)>::value, "bad"); | ||
static_assert(!std::is_copy_assignable<decltype(s1)>::value, "bad"); | ||
static_assert(!std::is_move_assignable<decltype(s1)>::value, "bad"); | ||
static_assert(std::is_move_constructible<decltype(s1)>::value, "bad"); | ||
auto s2 = std::move(s1); | ||
auto s3 = std::move(s2); | ||
auto s4 = std::move(s3); | ||
} | ||
|
||
DLIB_TEST(counter == 1); | ||
|
||
const auto fn_inner = [&] | ||
{ | ||
scope_exit_erased s([&]{++counter;}); | ||
return s; | ||
}; | ||
|
||
const auto fn_outer = [&] | ||
{ | ||
auto s = fn_inner(); | ||
return s; | ||
}; | ||
|
||
{ | ||
auto s = fn_outer(); | ||
} | ||
|
||
DLIB_TEST(counter == 2); | ||
} | ||
|
||
struct results_with_delayed_C_library_resource_management | ||
{ | ||
int ndata{0}; | ||
char* data{nullptr}; | ||
scope_exit_erased s; | ||
}; | ||
|
||
void test_composition() | ||
{ | ||
int counter{0}; | ||
|
||
const auto fn = [&] | ||
{ | ||
// Pretend you're in a cpp file using a C library which isn't exposed to the API via the header. | ||
// You want to return some results, but those results are only valid so long as something returned by the C library is still alive | ||
// You want to delay releasing any resources allocated by the C library until after you've returned your results and the caller is done using them. | ||
// You could return a std::unique_ptr<results> object with a custom deleter which deletes that resource but because all types in std::unique_ptr | ||
// must be complete types, you would have to pollute the header. You can use std::shared_ptr with a custom deleter, defined at runtime, | ||
// but this is less efficient. | ||
// You can use a scope_exit_erased object to wrap the resouce management function from the C library and delay the call further up the stack, | ||
// all behind a type erased callback. | ||
|
||
// pretend malloc() is a fancy function from some exotic C library. | ||
// pretend free() is another fancy function which you don't want users to have to manually call, and you want to delay calling it until after the results are used | ||
// pretend cstdlib is a fancy header you don't want to expose in your own header file. | ||
char* data = (char*)std::malloc(100); | ||
std::memset(data, 0, 100); | ||
std::snprintf(data, 100, "hello there!"); | ||
scope_exit_erased s{[=, &counter] {free(data); ++counter;}}; | ||
|
||
results_with_delayed_C_library_resource_management results{100, data, std::move(s)}; | ||
|
||
return results; | ||
}; | ||
|
||
{ | ||
// Oh, look at me. I'm using these results, blissfully unaware that some super complicated function in a C library will get called when i'm done using results. | ||
const auto results = fn(); | ||
DLIB_TEST(results.ndata == 100); | ||
DLIB_TEST(std::strcmp(results.data, "hello there!") == 0); | ||
DLIB_TEST(counter == 0); | ||
} | ||
|
||
DLIB_TEST(counter == 1); | ||
} | ||
|
||
// --------------------------------------------------------------------------------------------------- | ||
|
||
class scope_tester : public tester | ||
{ | ||
public: | ||
scope_tester ( | ||
) : | ||
tester ("test_scope", | ||
"Runs tests on the scope_exit and related objects") | ||
{} | ||
|
||
void perform_test ( | ||
) | ||
{ | ||
test_scope_exit(); | ||
test_scope_exit_erased(); | ||
test_composition(); | ||
} | ||
} a; | ||
|
||
// --------------------------------------------------------------------------------------------------- | ||
|
||
} |