From 6c5a909903400438b7dcc6908a2e9f786dc7584d Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Wed, 12 Mar 2025 03:22:13 +0200 Subject: [PATCH] feat: add `unique_resource` --- .github/workflows/ci-vcpkg.yml | 5 - README.md | 73 +++- examples/.clang-tidy | 4 + examples/CMakeLists.txt | 3 + examples/unique_resource.cpp | 37 ++ src/unique_resource.h | 639 +++++++++++++++++++++++++++++++++ test/.clang-tidy | 1 + test/CMakeLists.txt | 1 + test/unique_resource.cpp | 467 ++++++++++++++++++++++++ 9 files changed, 1224 insertions(+), 6 deletions(-) create mode 100644 examples/.clang-tidy create mode 100644 examples/unique_resource.cpp create mode 100644 src/unique_resource.h create mode 100644 test/unique_resource.cpp diff --git a/.github/workflows/ci-vcpkg.yml b/.github/workflows/ci-vcpkg.yml index 0eb64bb..e87335d 100644 --- a/.github/workflows/ci-vcpkg.yml +++ b/.github/workflows/ci-vcpkg.yml @@ -47,11 +47,6 @@ jobs: - name: Set up vcpkg uses: lukka/run-vcpkg@5e0cab206a5ea620130caf672fce3e4a6b5666a1 # v11.5 - # - name: Fix for AppleClang - # run: | - # sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer - # if: runner.os == 'macOS' - - name: Build and test run: | cmake --preset debug-vcpkg -DBUILD_DOCS=OFF -DBUILD_EXAMPLES=OFF diff --git a/README.md b/README.md index a2f6b97..3c965aa 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ ## Overview -This project provides a set of scope guard utilities for managing exit actions in C++. These utilities ensure that specified actions are executed when a scope is exited, regardless of how the exit occurs. The scope guards include: +This project provides a set of utilities for managing exit actions and resource handling in C++. These utilities ensure that specified actions are executed when a scope is exited, regardless of (or depending on) how the exit occurs. The scope guards include: - `exit_action`: Executes an action when the scope is exited. - `fail_action`: Executes an action when the scope is exited due to an exception. - `success_action`: Executes an action when the scope is exited normally. +- `unique_resource`: Manages a resource through a handle and disposes of that resource upon destruction (scope exit). These utilities are useful for ensuring that resources are properly released or actions are taken when a scope is exited. @@ -19,11 +20,14 @@ These utilities are useful for ensuring that resources are properly released or - **exit_action**: Calls its exit function on destruction, when a scope is exited. - **fail_action**: Calls its exit function when a scope is exited via an exception. - **success_action**: Calls its exit function when a scope is exited normally. +- **unique_resource**: Manages a resource with a custom deleter, ensuring the resource is released when the scope is exited. ## Usage ### Example Usage +#### Scope Guards + ```cpp #include #include @@ -77,6 +81,29 @@ int main() } ``` +#### `unique_resource` + +```cpp +#include +#include + +int main() +{ + auto file = wwa::utils::make_unique_resource_checked( + std::fopen("potentially_nonexistent_file.txt", "r"), nullptr, std::fclose + ); + + if (file.get() != nullptr) { + std::puts("The file exists.\n"); + } + else { + std::puts("The file does not exist.\n"); + } + + return 0; +} +``` + ## API Documentation The documentation is available at [https://sjinks.github.io/scope-action-cpp/](https://sjinks.github.io/scope-action-cpp/). @@ -144,6 +171,50 @@ public: }; ``` +### `unique_resource` + +A universal RAII resource handle wrapper for resource handles that owns and manages a resource through a handle and disposes of that resource upon destruction. + +```cpp +template +class [[nodiscard]] unique_resource { +public: + unique_resource(); + + template + unique_resource(Res&& r, Del&& d) noexcept( + (std::is_nothrow_constructible_v || std::is_nothrow_constructible_v) && + (std::is_nothrow_constructible_v || std::is_nothrow_constructible_v) + ); + + unique_resource(unique_resource&& other) noexcept(std::is_nothrow_move_constructible_v && std::is_nothrow_move_constructible_v); + ~unique_resource() noexcept; + + unique_resource& operator=(unique_resource&& other) noexcept( + std::is_nothrow_move_assignable_v && std::is_nothrow_move_assignable_v + ) + + void release() noexcept; + void reset() noexcept; + + template + void reset(Res&& r); + + const Resource& get() const noexcept; + const Deleter& get_deleter() const noexcept; + + std::add_lvalue_reference_t> operator*() const noexcept; + Resource operator->() const noexcept +}; + +template> +unique_resource, std::decay_t> +make_unique_resource_checked(Resource&& r, const Invalid& invalid, Deleter&& d) noexcept( + std::is_nothrow_constructible_v, Resource> && + std::is_nothrow_constructible_v, Deleter> +); +``` + ## Building and Testing ### Prerequisites diff --git a/examples/.clang-tidy b/examples/.clang-tidy new file mode 100644 index 0000000..020dbf1 --- /dev/null +++ b/examples/.clang-tidy @@ -0,0 +1,4 @@ +--- +InheritParentConfig: true +Checks: > + -cppcoreguidelines-owning-memory diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 246e6cf..1a8e479 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -8,3 +8,6 @@ set_directory_properties(PROPERTIES INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/src add_executable(scope_action scope_action.cpp) target_link_libraries(scope_action PRIVATE wwa::scope_action) + +add_executable(unique_resource unique_resource.cpp) +target_link_libraries(unique_resource PRIVATE wwa::scope_action) diff --git a/examples/unique_resource.cpp b/examples/unique_resource.cpp new file mode 100644 index 0000000..6de46e4 --- /dev/null +++ b/examples/unique_resource.cpp @@ -0,0 +1,37 @@ +#include + +#ifndef _WIN32 +# include +# include +#endif + +#include "unique_resource.h" + +int main() +{ + //! [Using make_unique_resource_checked()] + auto file = wwa::utils::make_unique_resource_checked( + std::fopen("potentially_nonexistent_file.txt", "r"), nullptr, std::fclose + ); + + if (file.get() != nullptr) { + std::puts("The file exists.\n"); + } + else { + std::puts("The file does not exist.\n"); + } + //! [Using make_unique_resource_checked()] + +#ifndef _WIN32 + //! [Using unique_resource] + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock != -1) { + const wwa::utils::unique_resource sock_guard(sock, close); + // Do something with the socket + // The socket will be closed when `sock_guard` goes out of scope + } + //! [Using unique_resource] +#endif + + return 0; +} diff --git a/src/unique_resource.h b/src/unique_resource.h new file mode 100644 index 0000000..db3eeae --- /dev/null +++ b/src/unique_resource.h @@ -0,0 +1,639 @@ +#ifndef E25CE0A0_3429_4977_B6AE_73697782F7BD +#define E25CE0A0_3429_4977_B6AE_73697782F7BD + +#include +#include +#include "scope_action.h" + +namespace wwa::utils { + +/** + * @brief A universal RAII resource handle wrapper. + * + * `unique_resource` is a universal RAII wrapper for resource handles that owns and manages a resource through a handle + * and disposes of that resource when the `unique_resource` is destroyed. + * + * The resource is disposed of using the deleter of type `Deleter` when either of the following happens: + * - the managing `unique_resource` object is destroyed, + * - the managing `unique_resource` object is assigned from another resource via `operator=()` or `reset()`. + * + * Usage example: + * @snippet{trimleft} unique_resource.cpp Using unique_resource + * + * @tparam Resource Resource handle type. `Resource` shall be an object type or an lvalue reference to an object type. + * `std::remove_reference_t` shall be + * [MoveConstructible](https://en.cppreference.com/w/cpp/named_req/MoveConstructible), and if + * `std::remove_reference_t` is not + * [CopyConstructible](https://en.cppreference.com/w/cpp/named_req/CopyConstructible), + * `std::is_nothrow_move_constructible_v>` shall be `true`. + * @tparam Deleter Deleter type. `Deleter` shall be a + * [Destructible](https://en.cppreference.com/w/cpp/named_req/Destructible) and + * [MoveConstructible](https://en.cppreference.com/w/cpp/named_req/MoveConstructible) + * [FunctionObject](https://en.cppreference.com/w/cpp/named_req/FunctionObject) type, and if `Deleter` is not + * [CopyConstructible](https://en.cppreference.com/w/cpp/named_req/CopyConstructible), + * `std::is_nothrow_move_constructible_v` shall be `true`. Given an lvalue `d` of type `Deleter` and an lvalue + * `r` of type `std::remove_reference_t`, the expression `d(r)` shall be well-formed. + * + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource + * @see `make_unique_resource_checked()` + */ +template +requires(!std::is_rvalue_reference_v && !std::is_reference_v) +class [[nodiscard]] unique_resource { + /** + * @brief Dummy "scope guard". + * + * A structure that provides a no-op `release()` method. + * It is used instead of `fail_action` when the operation is known to be no-throw. + */ + struct dummy_scope_guard { + /** + * @brief Dummy method that does nothing. + */ + constexpr void release() {} + }; + + /** + * @brief Guards object construction with a scope guard. + * + * @tparam T Object type. + */ + template + struct guard { // NOLINT(cppcoreguidelines-special-member-functions) + template + requires std::is_constructible_v + guard(U&&) noexcept(std::is_nothrow_constructible_v); + + /** + * @brief Constructor. + * + * Constructs a `T` with `U` and releases the scope guard `ScopeGuard`. + * + * @tparam U Object type to construct `T` from. `std::is_constructible_v` must be `true`. + * @tparam ScopeGuard Scope guard type. + */ + template + requires(std::is_constructible_v) + // NOLINTNEXTLINE(cppcoreguidelines-missing-std-forward) + guard(U&& r, ScopeGuard&& d) noexcept(std::is_nothrow_constructible_v) : m_obj(std::forward(r)) + { + d.release(); + } + + /** @brief Default constructor */ + guard() = default; + /** @brief Move constructor */ + guard(guard&&) = default; + + /** + * @brief Move constructor. + * + * This overload participates in overload resolution only if `std::is_nothrow_move_constructible_v` is + * `false`. + * + * @param rhs Another `guard` to acquire the ownership from. + */ + guard(guard&& rhs) noexcept(std::is_nothrow_constructible_v) + requires(!std::is_nothrow_move_constructible_v) + : m_obj(rhs.m_obj) + {} + + /** @brief Assignment operator */ + guard& operator=(const guard&) = default; + /** @brief Move-assignment operator */ + guard& operator=(guard&&) = default; + + /** + * @brief Returns a reference to the stored object. + * + * @return Reference to the stored object. + */ + constexpr T& get() noexcept { return this->m_obj; } + + /** + * @brief Returns a constant reference to the stored object. + * + * @return Constant reference to the stored object. + */ + [[nodiscard]] constexpr const T& get() const noexcept { return this->m_obj; } + + private: + [[no_unique_address]] T m_obj{}; ///< Stored object. + }; + + /** + * @brief Guards construction with a scope guard for a reference type. + * + * @tparam T Object type. + */ + template + struct guard { // NOLINT(cppcoreguidelines-special-member-functions) + template + requires std::is_constructible_v, U> + guard(U&&) noexcept(std::is_nothrow_constructible_v, U>); + + /** + * @brief Constructor. + * + * Constructs a `std::reference_wrapper>` with `U` and releases the scope guard + * `ScopeGuard`. + * + * @tparam U Object type to construct `std::reference_wrapper>` from. + * @tparam ScopeGuard Scope guard type. + */ + template + guard( + U&& r, ScopeGuard&& d // NOLINT(cppcoreguidelines-missing-std-forward) + ) noexcept(std::is_nothrow_constructible_v>, U>) + : m_value(static_cast(r)) + { + d.release(); + } + + /** @cond */ + guard() = delete; + /** @endcond */ + /** @brief Copy constructor. */ + guard(const guard&) = default; + /** @brief Assignment operator. */ + guard& operator=(const guard&) = default; + + /** + * @brief Returns the stored reference. + * + * @return Stored reference. + */ + T& get() noexcept { return this->m_value.get(); } + + /** + * @brief Returns the stored reference. + * + * @return Stored reference. + */ + [[nodiscard]] T& get() const noexcept { return this->m_value.get(); } + + private: + std::reference_wrapper> m_value; ///< Stored reference. + }; + + using guarded_resource_t = guard; ///< Guarded `Resource` + using guarded_deleter_t = guard; ///< Guarded `Deleter` + + /** + * @brief Forward type. + * + * If `T` is nothrow constructible from `U`, then `U`; otherwise `U&`. + * + * @tparam T Object type to construct. + * @tparam U Object type to construct `T` from. + */ + template + requires(std::is_constructible_v && (std::is_nothrow_constructible_v || std::is_constructible_v)) + using forwarder_t = std::conditional_t, U, U&>; + + /** + * @brief A helper functon to forwards `U` as `U` or `U&`. + * + * @see @a forwarder_t + * + * @tparam T Object type to construct. + * @tparam U Object type to construct `T` from. + * @param u Object to construct `T` from. + * @return `U` or `U&`. + */ + template + static constexpr forwarder_t fwd(U& u) + { + return static_cast&&>(u); + } + + /** + * @brief Returns a scope guard for the construction of `T` from `U`. + * + * If `T` is nothrow constructible from `U`, returns a dummy scope guard; otherwise, returns a `fail_action` that + * will release the resource `r` with `d(r)` on failure. + * + * @tparam T Object type to construct. + * @tparam U Object type to construct `T` from. + * @tparam Del Deleter type. + * @tparam Res Resource type. + * @param d Deleter. + * @param r Resource. + * @return Scope guard. + */ + template + static constexpr auto make_scope_guard(Del& d, Res& r) + { + if constexpr (std::is_nothrow_constructible_v) { + return dummy_scope_guard{}; + } + else { + return fail_action{[&d, &r] { d(r); }}; + } + } + +public: + /** + * @brief Constructs a new `unique_resource`. + * + * Default constructor. Value-initializes the stored resource handle and the deleter. + * + * This overload participates in overload resolution only if both `std::is_default_constructible_v` and + * `std::is_default_constructible_v` are `true`. + * + * @note The constructed `unique_resource` does not own the resource. + * + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/unique_resource + */ + unique_resource() + requires(std::is_default_constructible_v && std::is_default_constructible_v) + = default; + + /** @cond */ + unique_resource(const unique_resource&) = delete; + /** @endcond */ + + /** + * @brief Constructs a new `unique_resource`. + * + * The stored resource handle is initialized with `std::forward(r)` if + * `std::is_nothrow_constructible_v` is `true`, otherwise `r`. If initialization of the stored + * resource handle throws an exception, calls `d(r)`. + * + * Then, the deleter is initialized with `std::forward(d)` if + * `std::is_nothrow_constructible_v` is `true`, otherwise `d`. If initialization of deleter throws an + * exception, calls `d(m_resource)`. + * + * The constructed `unique_resource` owns the resource. + * + * @tparam Res Resource type. + * @tparam Del Deleter type. + * @param r A resource handle. + * @param d A deleter to use to dispose the resource. + * + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/unique_resource + */ + template + requires requires { + typename forwarder_t; + typename forwarder_t; + } + // NOLINTNEXTLINE(cppcoreguidelines-missing-std-forward) + unique_resource(Res&& r, Del&& d) noexcept( + (std::is_nothrow_constructible_v || + std::is_nothrow_constructible_v) && + (std::is_nothrow_constructible_v || std::is_nothrow_constructible_v) + ) + : m_resource(fwd(r), make_scope_guard(d, r)), + m_deleter(fwd(d), make_scope_guard(d, m_resource.get())), m_run_on_reset(true) + {} + + /** + * @brief Constructs a new `unique_resource`. + * + * Move constructor. + * + * @li The stored resource handle is initialized from the one of `rhs` using `std::move`. + * @li Then, the deleter is initialized with the one of `rhs` using `std::move`. + * @li After construction, the constructed `unique_resource` owns its resource if and only if `rhs` owned the + * resource before the construction, and `rhs` is set to not own the resource. + * + * @param rhs Another unique_resource to acquire the ownership from. + * + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/unique_resource + */ + unique_resource(unique_resource&& rhs) noexcept + requires std::is_nothrow_move_constructible_v && std::is_nothrow_move_constructible_v + : m_resource(std::move(rhs.m_resource)), m_deleter(std::move(rhs.m_deleter)), + m_run_on_reset(std::exchange(rhs.m_run_on_reset, false)) + {} + + /** + * @brief Constructs a new `unique_resource`. + * + * Move constructor. + * + * @li The stored resource handle is initialized from the one of `rhs` using `std::move`. + * @li Then, the deleter is initialized with the one of `rhs`. If initialization of the deleter throws an exception + * and `rhs` owns the resource, calls the deleter of `rhs` with `m_resource` to dispose the resource, then calls + * `rhs.release()`. + * @li After construction, the constructed `unique_resource` owns its resource if and only if `rhs` owned the + * resource before the construction, and `rhs` is set to not own the resource. + * + * @param rhs Another unique_resource to acquire the ownership from. + * @throw * Any exception thrown during initialization of the stored resource handle or the deleter. + * + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/unique_resource + */ + unique_resource(unique_resource&& rhs) // NOLINT(performance-noexcept-move-constructor,bugprone-exception-escape) + requires(std::is_nothrow_move_constructible_v && !std::is_nothrow_move_constructible_v) + : m_resource(std::move(rhs.m_resource)), + m_deleter(fwd(rhs.m_deleter.get()), fail_action([&rhs, this] { + if (rhs.m_run_on_reset) { + rhs.m_deleter.get()(this->m_resource.get()); + rhs.release(); + } + })), + m_run_on_reset(std::exchange(rhs.m_run_on_reset, false)) + {} + + /** + * @brief Constructs a new `unique_resource`. + * + * Move constructor. + * + * @li The stored resource handle is initialized from the one of `rhs`. If initialization of the stored resource + * handle throws an exception, `rhs` is not modified. + * @li Then, the deleter is initialized with the one of `rhs`, using `std::move` if + * `std::is_nothrow_move_constructible_v` is true. + * @li After construction, the constructed `unique_resource` owns its resource if and only if `rhs` owned the + * resource before the construction, and `rhs` is set to not own the resource. + * + * @param rhs Another unique_resource to acquire the ownership from. + * @throw * Any exception thrown during initialization of the stored resource handle or the deleter. + * + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/unique_resource + */ + unique_resource(unique_resource&& rhs) // NOLINT(performance-noexcept-move-constructor,bugprone-exception-escape) + requires(!std::is_nothrow_move_constructible_v) + : unique_resource(rhs.m_resource.get(), rhs.m_deleter.get(), dummy_scope_guard{}) + { + this->m_run_on_reset = std::exchange(rhs.m_run_on_reset, false); + } + + /** + * @brief Disposes the managed resource if such is present. + * + * Disposes the resource by calling the deleter with the underlying resource handle if the `unique_resource` owns + * it, equivalent to calling `reset()`. Then destroys the stored resource handle and the deleter. + * + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/%7Eunique_resource + */ + ~unique_resource() { this->reset(); } + + /** @cond */ + unique_resource& operator=(const unique_resource&) = delete; + /** @endcond */ + + /** + * @brief Assigns a `unique_resource`. + * + * Move assignment operator. Replaces the managed resource and the deleter with `rhs`'s. + * + * @li First, calls `reset()` to dispose the currently owned resource, if any. + * @li Then assigns the stored resource handle and the deleter with `rhs`'s. `std::move` is applied to the stored + * resource handle or the deleter of `rhs` if `std::is_nothrow_move_assignable_v` or + * `std::is_nothrow_move_assignable_v` is `true` respectively. Assignment of the stored resource handle is + * executed first, unless `std::is_nothrow_move_assignable_v` is `false` and + * `std::is_nothrow_move_assignable_v` is `true`. + * @li Finally, sets `*this` to own the resource if and only if `rhs` owned it before assignment, and `rhs` not to + * own the resource. + * + * If `std::is_nothrow_move_assignable_v` is true, `Resource` shall satisfy the + * [MoveAssignable](https://en.cppreference.com/w/cpp/named_req/MoveAssignable) requirements; + * otherwise `Resource` shall satisfy the + * [CopyAssignable](https://en.cppreference.com/w/cpp/named_req/CopyAssignable) requirements. + * + * If `std::is_nothrow_move_assignable_v` is `true`, `Deleter` shall satisfy the + * [MoveAssignable](https://en.cppreference.com/w/cpp/named_req/MoveAssignable) requirements; + * otherwise `Deleter` shall satisfy the + * [CopyAssignable](https://en.cppreference.com/w/cpp/named_req/CopyAssignable) requirements. + * + * Failing to satisfy above requirements results in undefined behavior. + * + * @param rhs resource wrapper from which ownership will be transferred + * @return `*this` + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/operator%3D + */ + unique_resource& operator=(unique_resource&& rhs + ) noexcept(std::is_nothrow_move_assignable_v && std::is_nothrow_move_assignable_v) + { + this->reset(); + if constexpr (std::is_nothrow_move_assignable_v) { + if constexpr (std::is_nothrow_move_assignable_v) { + this->m_resource = std::move(rhs.m_resource); + this->m_deleter = std::move(rhs.m_deleter); + } + else { + this->m_deleter = rhs.m_deleter; + this->m_resource = std::move(rhs.m_resource); + } + } + else { + this->m_resource = rhs.m_resource; + if constexpr (std::is_nothrow_move_assignable_v) { + this->m_deleter = std::move(rhs.m_deleter); + } + else { + this->m_deleter = rhs.m_deleter; + } + } + + this->m_run_on_reset = std::exchange(rhs.m_run_on_reset, false); + return *this; + } + + /** + * @brief Releases the ownership. + * + * Releases the ownership of the managed resource if any. The destructor will not execute the deleter after the + * call, unless `reset()` is called later for managing new resource. + * + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/release + */ + void release() noexcept { this->m_run_on_reset = false; } + + /** + * @brief Disposes the managed resource. + * + * Disposes the resource by calling the deleter with the underlying resource handle if the `unique_resource` owns + * it. The unique_resource does not own the resource after the call. + * + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/reset + */ + void reset() noexcept + { + if (this->m_run_on_reset) { + this->m_run_on_reset = false; + this->m_deleter.get()(this->m_resource.get()); + } + } + + /** + * @brief Replaces the managed resource. + * + * Replaces the resource by calling `reset()` and then assigns the stored resource handle with + * `std::forward`(r) if `std::is_nothrow_assignable_v` is `true`, otherwise `std::as_const(r)`, + * where `Resource` is the type of stored resource handle. The `unique_resource` owns the resource after the call. + * If copy-assignment of the store resource handle throws an exception, calls `del(r)`, where `del` is the deleter + * object. + * + * This overload participates in overload resolution only if the selected assignment expression assigning the + * stored resource handle is well-formed. + * + * @warning The program is ill-formed if `del(r)` is ill-formed. + * @warning The behavior is undefined if `del(r)` results in undefined behavior or throws an exception. + * + * @tparam Res Resource type. + * @param r The new resource handle. + * @throw * Any exception thrown in assigning the stored resource handle. + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/reset + */ + template + void reset(Res&& r) + { + this->reset(); + if constexpr (std::is_nothrow_assignable_v) { + this->m_resource.get() = std::forward(r); + } + else { + this->m_resource.get() = std::as_const(r); // const_cast&>(r); + } + + this->m_run_on_reset = true; + } + + /** + * @brief Accesses the underlying resource handle. + * + * @return The underlying resource handle. + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/get + */ + [[nodiscard]] const Resource& get() const noexcept { return this->m_resource.get(); } + + /** + * @brief Get the deleter object. + * + * Accesses the deleter object which would be used for disposing the managed resource. + * + * @return The stored deleter. + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/get_deleter + */ + [[nodiscard]] const Deleter& get_deleter() const noexcept { return this->m_deleter.get(); } + + /** + * @brief Accesses the pointee if the resource handle is a pointer. + * + * Access the object or function pointed by the underlying resource handle which is a pointer. + * This function participates in overload resolution only if `std::is_pointer_v` is `true` + * and `std::is_void_v>` is `false`. + * + * @warning If the resource handle is not pointing to an object or a function, the behavior is undefined. + * + * @return The object or function pointed by the underlying resource handle. + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/operator* + */ + std::add_lvalue_reference_t> operator*() const noexcept + requires(std::is_pointer_v && !std::is_void_v>) + { + return *this->get(); + } + + /** + * @brief Accesses the pointee if the resource handle is a pointer. + * + * Get a copy of the underlying resource handle which is a pointer. This function participates in overload + * resolution only if `std::is_pointer_v` is `true`. The return value is typically used to access the + * pointed object. + * + * @return Copy of the underlying resource handle. + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/operator* + */ + Resource operator->() const noexcept + requires(std::is_pointer_v) + { + return this->get(); + } + +private: + [[no_unique_address]] guarded_resource_t m_resource{}; ///< Resource handle. + [[no_unique_address]] guarded_deleter_t m_deleter{}; ///< Deleter. + bool m_run_on_reset = false; ///< Whether to invoke the deleter on `reset()`/destruction. + + /** + * @brief Creates a `unique_resource`, checking invalid value. + * + * @tparam Res Resource type. + * @tparam Del Deleter type. + * @tparam Invalid Type of the value indicating the resource handle is invalid. + * @return A unique resource. + * @see wwa::utils::make_unique_resource_checked() + */ + template + friend unique_resource, std::decay_t> + make_unique_resource_checked(Res&&, const Invalid&, Del&&) +#ifndef _MSC_VER + noexcept( + std::is_nothrow_constructible_v, Res> && + std::is_nothrow_constructible_v, Del> + ) +#endif + ; + + template + unique_resource( + Res&& r, Del&& d, dummy_scope_guard dummy + ) noexcept(std::is_nothrow_constructible_v && std::is_nothrow_constructible_v) + : m_resource(std::forward(r), dummy), m_deleter(std::forward(d), dummy) + {} +}; + +/** + * @brief Deduction guide for @a unique_resource. + * + * @tparam Resource Resource type. + * @tparam Deleter Deleter type. + */ +template +unique_resource(Resource, Deleter) -> unique_resource; + +/** + * @brief Creates a `unique_resource`, checking invalid value. + * + * Creates a `unique_resource`, initializes its stored resource handle with `std::forward(r)` and its deleter + * with `std::forward(d)`. The created `unique_resource` owns the resource if and only if `bool(r == invalid)` + * is `false`. + * + * Usage example: + * @snippet{trimleft} unique_resource.cpp Using make_unique_resource_checked() + * + * @warning The program is ill-formed if the expression `r == invalid` cannot be contextually converted to `bool`, + * and the behavior is undefined if the conversion results in undefined behavior or throws an exception. + * + * @note `make_unique_resource_checked()` exists to avoid calling a deleter function with an invalid argument. + * + * @tparam Res Resource type. + * @tparam Del Deleter type. + * @tparam Invalid Type of the value indicating the resource handle is invalid. + * @param r A resource handle. + * @param invalid A value indicating the resource handle is invalid. + * @param d A deleter to use to dispose the resource + * @return A unique resource. + * @throw * Any exception thrown in initialization of the stored resource handle and the deleter. + * @see https://en.cppreference.com/w/cpp/experimental/unique_resource/make_unique_resource_checked + */ +template +unique_resource, std::decay_t> +make_unique_resource_checked(Res&& r, const Invalid& invalid, Del&& d) +#ifndef _MSC_VER + noexcept( + std::is_nothrow_constructible_v, Res> && + std::is_nothrow_constructible_v, Del> + ) +#endif +{ + if (r == invalid) { + return {std::forward(r), std::forward(d), {}}; + } + + return {std::forward(r), std::forward(d)}; +} + +/** + * @example unique_resource.cpp + * Example of using `unqiue_resource` and `make_unique_resource_checked`. + */ + +} // namespace wwa::utils + +#endif /* E25CE0A0_3429_4977_B6AE_73697782F7BD */ diff --git a/test/.clang-tidy b/test/.clang-tidy index 8c2ce80..a049898 100644 --- a/test/.clang-tidy +++ b/test/.clang-tidy @@ -3,5 +3,6 @@ InheritParentConfig: true Checks: > -readability-function-cognitive-complexity, -cppcoreguidelines-special-member-functions, + -cppcoreguidelines-owning-memory, -clang-analyzer-cplusplus.Move, -clang-analyzer-deadcode.DeadStores diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6b9551b..dd020cb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable( exit_action.cpp fail_action.cpp success_action.cpp + unique_resource.cpp ) target_link_libraries("${TEST_TARGET}" PRIVATE ${PROJECT_NAME} GTest::gtest_main) diff --git a/test/unique_resource.cpp b/test/unique_resource.cpp new file mode 100644 index 0000000..970bee5 --- /dev/null +++ b/test/unique_resource.cpp @@ -0,0 +1,467 @@ +#include + +#include +#include +#include + +#include "unique_resource.h" + +namespace { + +template +struct Resource { + Resource() = default; + Resource(const Resource&) noexcept(NE) = default; + + // NOLINTNEXTLINE(bugprone-exception-escape) + Resource(Resource&&) noexcept(NE) + { + if constexpr (Throw) { + throw std::runtime_error("Resource move constructor"); + } + } +}; + +template +struct Deleter { + Deleter(int& run) : m_run(&run) {} + Deleter(const Deleter& other) : m_run(other.m_run) + { + if (other.m_throw_on_copy) { + throw std::runtime_error("Deleter copy constructor"); + } + } + + // NOLINTNEXTLINE(bugprone-exception-escape) + Deleter(Deleter&& other) noexcept(NE) : m_run(other.m_run) + { + if constexpr (Throw) { + throw std::runtime_error("Deleter move constructor"); + } + } + + void operator()(Res&) { *this->m_run += 1; } + + void set_throw_on_copy(bool value) const { this->m_throw_on_copy = value; } + +private: + int* m_run; + mutable bool m_throw_on_copy = false; +}; + +template +struct MoveAssignableResource { + MoveAssignableResource() = default; + MoveAssignableResource(const MoveAssignableResource&) = default; + MoveAssignableResource& operator=(MoveAssignableResource&&) noexcept(NE) = default; + MoveAssignableResource& operator=(const MoveAssignableResource&) = default; +}; + +template +struct MoveAssignableDeleter { + MoveAssignableDeleter(int& run) : m_run(&run) {} + MoveAssignableDeleter(const MoveAssignableDeleter&) = default; + MoveAssignableDeleter& operator=(MoveAssignableDeleter&&) noexcept(NE) = default; + MoveAssignableDeleter& operator=(const MoveAssignableDeleter&) = default; + + void operator()(Res&) { *this->m_run += 1; } + +private: + int* m_run; +}; + +} // namespace + +TEST(UniqueResource, MakeChecked_Invalid) +{ + bool run = false; + auto close = [&run](FILE* file) { + EXPECT_EQ(std::fclose(file), 0); + run = true; + }; + + { + const char* fname = "this-file-does-not-exist.txt"; + auto file = wwa::utils::make_unique_resource_checked(std::fopen(fname, "r"), nullptr, close); + EXPECT_EQ(file.get(), nullptr); + } + + EXPECT_FALSE(run); +} + +TEST(UniqueResource, MakeChecked_Valid) +{ + bool run = false; + auto free = [&run](void* p) { + std::free(p); // NOLINT(cppcoreguidelines-no-malloc) + run = true; + }; + + { + // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) + auto ptr = wwa::utils::make_unique_resource_checked(std::malloc(1), nullptr, free); + ASSERT_NE(ptr.get(), nullptr); + } + + EXPECT_TRUE(run); +} + +TEST(UniqueResource, DefaultCtor) +{ + const wwa::utils::unique_resource ptr; + EXPECT_EQ(ptr.get(), nullptr); + EXPECT_EQ(ptr.get_deleter(), nullptr); +} + +TEST(UniqueResource, MoveCtor_NN) +{ + int run = 0; + auto free = [&run](void* p) { + std::free(p); // NOLINT(cppcoreguidelines-no-malloc) + ++run; + }; + + { + wwa::utils::unique_resource f1(nullptr, free); + const wwa::utils::unique_resource f2(std::move(f1)); + + EXPECT_NE(&f2.get_deleter(), nullptr); + } + + EXPECT_EQ(run, 1); +} + +TEST(UniqueResource, MoveCtor_NE) +{ + int deleter_run = 0; + + Resource res; + static_assert(std::is_nothrow_move_constructible_v>); + + Deleter&> del(deleter_run); + static_assert(!std::is_nothrow_move_constructible_v>>); + + { + wwa::utils::unique_resource&, Deleter&>> f1(res, del); + const wwa::utils::unique_resource&, Deleter&>> f2(std::move(f1)); + + EXPECT_EQ(deleter_run, 0); + + EXPECT_EQ(&f2.get(), &res); + } + + EXPECT_EQ(deleter_run, 1); +} + +TEST(UniqueResource, MoveCtor_EE) +{ + int deleter_run = 0; + + Resource res; + static_assert(!std::is_nothrow_move_constructible_v>); + + Deleter> del(deleter_run); + static_assert(!std::is_nothrow_move_constructible_v>>); + + { + wwa::utils::unique_resource, Deleter>> f1(res, del); + const wwa::utils::unique_resource, Deleter>> f2(std::move(f1)); + + EXPECT_EQ(deleter_run, 0); + } + + EXPECT_EQ(deleter_run, 1); +} + +TEST(UniqueResource, MoveCtor_NT) +{ + int deleter_run = 0; + + Resource res; + static_assert(std::is_nothrow_move_constructible_v>); + + Deleter, true> del(deleter_run); + static_assert(!std::is_nothrow_move_constructible_v, true>>); + + { + using ur_t = wwa::utils::unique_resource, Deleter, true>>; + ur_t f1(res, del); + f1.get_deleter().set_throw_on_copy(true); + EXPECT_THROW(const ur_t f2(std::move(f1)), std::runtime_error); + + EXPECT_EQ(deleter_run, 1); + } + + EXPECT_EQ(deleter_run, 1); +} + +TEST(UniqueResource, MoveCtor_NT2) +{ + int deleter_run = 0; + + Resource res; + static_assert(std::is_nothrow_move_constructible_v>); + + Deleter, true> del(deleter_run); + static_assert(!std::is_nothrow_move_constructible_v, true>>); + + { + using ur_t = wwa::utils::unique_resource, Deleter, true>>; + ur_t f1(res, del); + f1.release(); + f1.get_deleter().set_throw_on_copy(true); + EXPECT_THROW(const ur_t f2(std::move(f1)), std::runtime_error); + + EXPECT_EQ(deleter_run, 0); + } + + EXPECT_EQ(deleter_run, 0); +} + +TEST(UniqueResource, MoveCtor_TE) +{ + int deleter_run = 0; + + struct Resource { + Resource() noexcept = default; + Resource(Resource&&) = delete; + Resource(const Resource& other) : m_throw_on_copy(!other.m_throw_on_copy) + { + if (other.m_throw_on_copy) { + throw std::runtime_error("Resource copy constructor"); + } + } + + private: + bool m_throw_on_copy = false; + }; + + static_assert(!std::is_nothrow_move_constructible_v); + + Resource res; + Deleter del(deleter_run); + static_assert(!std::is_nothrow_move_constructible_v>); + + { + using ur_t = wwa::utils::unique_resource>; + ur_t f1(res, del); + EXPECT_THROW(const ur_t f2(std::move(f1)), std::runtime_error); + + EXPECT_EQ(deleter_run, 0); + } + + EXPECT_EQ(deleter_run, 1); +} + +TEST(UniqueResource, Assign_NN) +{ + int deleter_run = 0; + + MoveAssignableResource res1; + const MoveAssignableResource res2; + + MoveAssignableDeleter&> del1(deleter_run); + const MoveAssignableDeleter&> del2(deleter_run); + + { + using ur_t = wwa::utils::unique_resource< + MoveAssignableResource, MoveAssignableDeleter&>>; + + ur_t f1(res1, del1); + ur_t f2(res2, del2); + f2 = std::move(f1); + + EXPECT_EQ(deleter_run, 1); // f2's deleter for the original resource + } + + EXPECT_EQ(deleter_run, 2); // f2's deleter for the assigned resource +} + +TEST(UniqueResource, Assign_NE) +{ + int deleter_run = 0; + + MoveAssignableResource res1; + const MoveAssignableResource res2; + + MoveAssignableDeleter&> del1(deleter_run); + const MoveAssignableDeleter&> del2(deleter_run); + + { + using ur_t = wwa::utils::unique_resource< + MoveAssignableResource, MoveAssignableDeleter&>>; + + ur_t f1(res1, del1); + ur_t f2(res2, del2); + f2 = std::move(f1); + + EXPECT_EQ(deleter_run, 1); // f2's deleter for the original resource + } + + EXPECT_EQ(deleter_run, 2); // f2's deleter for the assigned resource +} + +TEST(UniqueResource, Assign_EN) +{ + int deleter_run = 0; + + MoveAssignableResource res1; + const MoveAssignableResource res2; + + MoveAssignableDeleter&> del1(deleter_run); + const MoveAssignableDeleter&> del2(deleter_run); + + { + using ur_t = wwa::utils::unique_resource< + MoveAssignableResource, MoveAssignableDeleter&>>; + + ur_t f1(res1, del1); + ur_t f2(res2, del2); + f2 = std::move(f1); + + EXPECT_EQ(deleter_run, 1); // f2's deleter for the original resource + } + + EXPECT_EQ(deleter_run, 2); // f2's deleter for the assigned resource +} + +TEST(UniqueResource, Assign_EE) +{ + int deleter_run = 0; + + MoveAssignableResource res1; + const MoveAssignableResource res2; + + MoveAssignableDeleter&> del1(deleter_run); + const MoveAssignableDeleter&> del2(deleter_run); + + { + using ur_t = wwa::utils::unique_resource< + MoveAssignableResource, MoveAssignableDeleter&>>; + + ur_t f1(res1, del1); + ur_t f2(res2, del2); + f2 = std::move(f1); + + EXPECT_EQ(deleter_run, 1); // f2's deleter for the original resource + } + + EXPECT_EQ(deleter_run, 2); // f2's deleter for the assigned resource +} + +TEST(UniqueResource, Assign_EE2) +{ + int deleter_run = 0; + + MoveAssignableResource res1; + MoveAssignableResource res2; + + MoveAssignableDeleter&> del1(deleter_run); + MoveAssignableDeleter&> del2(deleter_run); + + { + using ur_t = wwa::utils::unique_resource< + MoveAssignableResource&, MoveAssignableDeleter&>>; + + ur_t f1(res1, del1); + ur_t f2(res2, del2); + f2 = std::move(f1); + + EXPECT_EQ(deleter_run, 1); // f2's deleter for the original resource + } + + EXPECT_EQ(deleter_run, 2); // f2's deleter for the assigned resource +} + +TEST(UniqueResource, Release) +{ + int deleter_run = 0; + + Resource res; + Deleter&> del(deleter_run); + + { + wwa::utils::unique_resource&, Deleter&>> f(res, del); + f.release(); + + EXPECT_EQ(deleter_run, 0); + } + + EXPECT_EQ(deleter_run, 0); +} + +TEST(UniqueResource, Reset) +{ + int deleter_run = 0; + + Resource res; + Deleter&> del(deleter_run); + + { + wwa::utils::unique_resource&, Deleter&>> f(res, del); + f.reset(); + + EXPECT_EQ(deleter_run, 1); + } + + EXPECT_EQ(deleter_run, 1); +} + +TEST(UniqueResource, Reset2A) +{ + int deleter_run = 0; + + struct Resource {}; + + Resource res; + Deleter del(deleter_run); + + { + wwa::utils::unique_resource> f(res, del); + f.reset(res); + + EXPECT_EQ(deleter_run, 1); + } + + EXPECT_EQ(deleter_run, 2); +} + +TEST(UniqueResource, Reset2B) +{ + int deleter_run = 0; + + struct Resource { + Resource() = default; + Resource(const Resource&) = default; + Resource& operator=(const Resource&) noexcept(false) = default; + }; + + Resource res; + Deleter del(deleter_run); + + { + wwa::utils::unique_resource> f(res, del); + f.reset(res); + + EXPECT_EQ(deleter_run, 1); + } + + EXPECT_EQ(deleter_run, 2); +} + +TEST(UniqueResource, Accessors) +{ + constexpr int expected_value = 42; + + struct s_t { + int value; + }; + + auto deallocate = [](s_t* s) { delete s; }; + auto allocate = []() { return new s_t{expected_value}; }; + + auto obj = wwa::utils::make_unique_resource_checked(allocate(), nullptr, deallocate); + ASSERT_NE(obj.get(), nullptr); + EXPECT_EQ(obj->value, expected_value); + EXPECT_EQ((*obj).value, expected_value); +}