diff --git a/dlib/scope.h b/dlib/scope.h new file mode 100644 index 000000000..95ea97f04 --- /dev/null +++ b/dlib/scope.h @@ -0,0 +1,89 @@ +// Copyright (C) 2023 Davis E. King (davis@dlib.net) +// License: Boost Software License See LICENSE.txt for the full license. +#ifndef DLIB_SCOPE_H_ +#define DLIB_SCOPE_H_ + +#include +#include +#include + +namespace dlib +{ + +// ---------------------------------------------------------------------------------------- + + template + 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::value) + : f_{std::move(other.f_)}, active_{std::exchange(other.active_, false)} + {} + + template< + class F, + std::enable_if_t, scope_exit>::value, bool> = true + > + explicit scope_exit(F&& f) noexcept(std::is_nothrow_constructible::value) + : f_{std::forward(f)}, active_{true} + {} + + ~scope_exit() noexcept + { + if (active_) + f_(); + } + + void release() noexcept { active_ = false; } + }; + + template + auto make_scope_exit(Fn&& f) + /*! + ensures: + - This is factory function that wraps the callback in a scope_exit object. + !*/ + + { + return scope_exit>(std::forward(f)); + } + +#ifdef __cpp_deduction_guides + template + scope_exit(Fn) -> scope_exit; +#endif + +// ---------------------------------------------------------------------------------------- + + using scope_exit_erased = scope_exit>; + /*! + 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_ \ No newline at end of file diff --git a/dlib/test/CMakeLists.txt b/dlib/test/CMakeLists.txt index 9fadd237b..998d9d01e 100644 --- a/dlib/test/CMakeLists.txt +++ b/dlib/test/CMakeLists.txt @@ -162,6 +162,7 @@ add_executable(${target_name} main.cpp tester.cpp te.cpp ffmpeg.cpp optional.cpp + scope.cpp ) get_filename_component(DLIB_FFMPEG_DATA ${CMAKE_SOURCE_DIR}/ffmpeg_data/details.cfg REALPATH) diff --git a/dlib/test/scope.cpp b/dlib/test/scope.cpp new file mode 100644 index 000000000..d69123919 --- /dev/null +++ b/dlib/test/scope.cpp @@ -0,0 +1,171 @@ +// Copyright (C) 2023 Davis E. King (davis@dlib.net) +// License: Boost Software License See LICENSE.txt for the full license. + +#include +#include +#include +#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::value, "bad"); + static_assert(!std::is_copy_assignable::value, "bad"); + static_assert(!std::is_move_assignable::value, "bad"); + static_assert(std::is_move_constructible::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::value, "bad"); + static_assert(!std::is_copy_assignable::value, "bad"); + static_assert(!std::is_move_assignable::value, "bad"); + static_assert(std::is_move_constructible::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 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; + +// --------------------------------------------------------------------------------------------------- + +} \ No newline at end of file