diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index b130621ab..09c63251d 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -29,47 +29,45 @@ jobs: sudo apt-get update sudo apt-get install ${{ matrix.compiler.pkg }} -y - name: Compile tests - working-directory: out env: CXX: ${{ matrix.compiler.exe }} run: | - cmake -DSIMPLE_UTILITY_BUILD_TESTS=ON -S .. - make -j4 + cmake -DSIMPLE_UTILITY_BUILD_TESTS=ON -B out -S . + cmake --build out -j4 - name: Run tests - working-directory: out/tests env: CTEST_OUTPUT_ON_FAILURE: 1 - run: ctest --timeout 30 -C Debug -j4 + run: ctest --test-dir out/tests --timeout 30 -C Debug -j4 windows: - runs-on: windows-latest + runs-on: windows-2022 timeout-minutes: 10 strategy: fail-fast: false matrix: - toolset: [v142, clang-cl] + toolset: [v142, v143, clang-cl] include: - toolset: v142 toolset_option: -T"v142" + - toolset: v143 + toolset_option: -T"v143" - toolset: clang-cl toolset_option: -T"ClangCl" steps: - uses: actions/checkout@v2 - name: Compile tests - working-directory: out run: | - cmake -DSIMPLE_UTILITY_BUILD_TESTS=ON ${{ matrix.toolset_option }} .. - cmake --build . -j 4 + cmake -DSIMPLE_UTILITY_BUILD_TESTS=ON ${{ matrix.toolset_option }} -B out -S . + cmake --build out -j4 - name: Run tests - working-directory: out/tests env: CTEST_OUTPUT_ON_FAILURE: 1 - run: ctest --timeout 30 -C Debug -j4 + run: ctest --test-dir out/tests --timeout 30 -C Debug -j4 # macos: -# runs-on: macOS-latest +# runs-on: macos-latest # timeout-minutes: 10 # # strategy: @@ -78,12 +76,11 @@ jobs: # steps: # - uses: actions/checkout@v2 # - name: Compile tests -# working-directory: out # run: | -# cmake -DSIMPLE_UTILITY_BUILD_TESTS=ON .. -# make -j4 +# cmake -DSIMPLE_UTILITY_BUILD_TESTS=ON -B out -S . +# cmake --build out -j4 # - name: Run tests # working-directory: out/tests # env: # CTEST_OUTPUT_ON_FAILURE: 1 -# run: ctest --timeout 30 -C Debug -j4 \ No newline at end of file +# run: ctest --test-dir out/tests --timeout 30 -C Debug -j4 diff --git a/.gitignore b/.gitignore index f739b1b11..feb1f8a73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vs/ -Folder.DotSettings* \ No newline at end of file +Folder.DotSettings* +CMakeSettings.json \ No newline at end of file diff --git a/CMakeSettings.json b/CMakeSettings.json deleted file mode 100644 index 09df61dc8..000000000 --- a/CMakeSettings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "configurations": [ - { - "name": "x64-Debug", - "generator": "Ninja", - "configurationType": "Debug", - "inheritEnvironments": [ "msvc_x64_x64" ], - "buildRoot": "${projectDir}\\out\\build\\${name}", - "installRoot": "${projectDir}\\out\\install\\${name}", - "cmakeCommandArgs": "", - "buildCommandArgs": "", - "ctestCommandArgs": "", - "variables": [ - { - "name": "SIMPLE_UTILITY_BUILD_TESTS", - "value": "True", - "type": "BOOL" - } - ] - } - ] -} \ No newline at end of file diff --git a/Doxyfile b/Doxyfile index f4eb4cebb..fd032823e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -989,7 +989,7 @@ EXCLUDE_PATTERNS = # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories use the pattern */test/* -EXCLUDE_SYMBOLS = +EXCLUDE_SYMBOLS = sl::detail # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include diff --git a/include/Simple-Utility/concepts/stl_counterparts.hpp b/include/Simple-Utility/concepts/stl_counterparts.hpp new file mode 100644 index 000000000..80f362336 --- /dev/null +++ b/include/Simple-Utility/concepts/stl_counterparts.hpp @@ -0,0 +1,59 @@ +// Copyright Dominic Koepke 2019 - 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SL_STL_SYMMETRICAL_HPP +#define SL_STL_SYMMETRICAL_HPP + +#pragma once + +#include + +// ReSharper disable CppClangTidyClangDiagnosticDocumentation + +namespace sl::concepts +{ + /** + * \addtogroup concepts + * @{ + */ + + /** + * \defgroup stl_counterpart_concepts stl counterparts + * \brief This group offers counterpart concepts for existing stl concepts. + * @{ + */ + + /** + * \brief Checks whether the left-hand-side type is unequal to the right-hand-side type. + * \details This is the inverted counterpart of ``std::same_as`` concept. + * \tparam TLhs The source type handed over to the target + * \tparam TRhs The target type to check + */ + template + concept not_same_as = !std::same_as; + + /** + * \brief Checks whether the target type is constructible from the source type. + * \details This is the symmetrical counterpart of ``std::constructible_from`` concept with a single constructor argument. + * \tparam TSource The source type handed over to the constructor of the target + * \tparam TTarget The target type to check + */ + template + concept constructs = std::constructible_from; + + /** + * \brief Checks whether the target type is assignable from the source type. + * \details This is the symmetrical counterpart of ``std::assignable_from`` concept. + * \tparam TSource The source type handed over to the target + * \tparam TTarget The target type to check + */ + template + concept assignable_to = std::assignable_from; + + /** @} */ + /** @} */ +} + +#endif diff --git a/include/Simple-Utility/unique_handle.hpp b/include/Simple-Utility/unique_handle.hpp new file mode 100644 index 000000000..8059f5719 --- /dev/null +++ b/include/Simple-Utility/unique_handle.hpp @@ -0,0 +1,507 @@ +// Copyright Dominic Koepke 2019 - 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SL_UNIQUE_HANDLE_HPP +#define SL_UNIQUE_HANDLE_HPP + +#pragma once + +#include +#include +#include + +#include "Simple-Utility/concepts/stl_counterparts.hpp" + +// some of the std::optional interface hasn't been declared constexpr before +#if __cpp_lib_optional >= 202106 +#define SL_UNIQUE_HANDLE_FULL_CONSTEXPR constexpr +#else +#define SL_UNIQUE_HANDLE_FULL_CONSTEXPR +#endif + +namespace sl +{ + /** + * \defgroup unique_handle_module unique_handle + * @{ + * + * \details The class \ref unique_handle is in fact a wrapper around a ``std::optional``, thus has at least the overhead of that. Additionally it adds two + * important aspects: + * -# it resets its internal value after it got moved. + * -# it invokes its delete action every time when the internal value switches its state from initialized to uninitialized. + * + * The latter happens when the value is in an initialized state and the \ref unique_handle gets + * - moved, + * - destructed or + * - assigned + * + * This is very similar to the behaviour of a ``std::unique_ptr``, hence the name. + * This behaviour is useful in cases when one has an identifier to a resource, which isn't stored on the heap (thus a ``std::unique_ptr`` is not + * a good option), and this identifier should have the responsibility as an owner over that resource, but the resource itself is not bound to the + * lifetime of that identifier. This might sound quite abstract, thus let us visit a simple example. + * + * Here some entities are stored in a simple ``std::list``. Imagine this entities are accessible from many places in your program, thus something + * like this can easily happen. + * + * \code{.cpp} + * std::list entities{}; + * + * { + * entities.emplace_front(); + * auto entity_id = entities.begin(); + * + * // do some actions with entity and other stuff + * + * // entity should now be erased. Not actually c++-ig, is it? + * entities.erase(entity_id); + * } + * \endcode + * + * This is clearly no ``memory leak`` but if one forgets to erase the entity, it exists until the list is cleared. + * With \ref unique_handle one can do this. + * + * \code{.cpp} + * struct list_delete_action + * { + * std::list* list{}; // pointer here, because a delete action must be move and copyable + * + * void operator ()(const std::list::iterator& itr) { list->erase(itr); } + * }; + * std::list entities{}; + * + * { + * entities.emplace_front(); + * sl::unique_handle entity_id{ entities.begin(), list_delete_action{ &entities } }; + * + * // do some actions with entity and other stuff + * + * // no cleanup necessary + * } + * \endcode + * + * Of course, at a first glance this seems quite more verbose, but in the long term nobody has to care about that entity anymore. This is what ``RAII`` is about. + * Note that ``unique_handles`` also can be stored as a member, then they really begin to shine, because if one would like to bind that entity to the lifetime of + * an other object that would of course lead to custom move constructor, assignment operator and destructor and explicitly deleted copy. With a + * \ref unique_handle none of this is necessary (and this is in fact the main reason why I decided to implement this). + * + * \note As using lambdas as delete action is usually fine, using capturing lambdas will fail to compile because they are non-copy-assignable. Use a old-school + * invokable struct as in the example instead. + * + * \see https://en.cppreference.com/w/cpp/utility/optional + * \see https://en.cppreference.com/w/cpp/memory/unique_ptr + * \see https://en.cppreference.com/w/cpp/language/lambda + */ + + /** + * \brief helper type for indicating unique_handles with uninitialized state + */ + // ReSharper disable once IdentifierTypo + struct nullhandle_t + { + }; + + /** + * \brief an object of type nullhandle_t + */ + // ReSharper disable once IdentifierTypo + constexpr nullhandle_t nullhandle{}; + + /** + * \brief exception type which indicates checked access to an uninitialized value + */ + using bad_handle_access = std::bad_optional_access; + + /** + * \brief default delete action for unique_handle with an empty operator () + */ + struct default_delete_action + { + constexpr void operator ()(auto&) const noexcept + { + } + }; + + /** @} */ + + namespace detail + { + template + struct value_validator + { + static_assert(std::movable, "The value type must be movable."); + static_assert(concepts::not_same_as, nullhandle_t>, "The value type must not be sl::nullhandle_t."); + + using type = T; + }; + + template + struct delete_action_validator + { + static_assert(std::copyable, "The delete action object must be copyable (capturing lambdas are not)."); + static_assert(std::invocable, "The delete action object must be invokable by T&."); + + using type = TDeleteAction; + }; + + template + using type_t = typename T::type; + } + + /** + * \addtogroup unique_handle_module + * @{ + */ + + /** + * \brief This type models some kind of ``std::optional`` behaviour but resets itself on move operations. + * \details For more details and information about related components go to \ref unique_handle_module "unique_handle module page". + * \tparam T The type of the stored value + * \tparam TDeleteAction Type of the used delete action + */ + template TDeleteAction = default_delete_action> + requires concepts::not_same_as + && std::copyable + class unique_handle + { + public: + /** + * \brief Type of the stored value + */ + using value_type = T; + /** + * \brief Type of the used delete action + */ + using delete_action_type = TDeleteAction; + + /** + * \brief Default constructor. The value will be in an uninitialized stated and the delete action gets default constructed. + */ + constexpr unique_handle() noexcept = default; + + /** + * \brief Destruct. Does invoke the delete action if the value is in an initialized state. + */ + constexpr ~unique_handle() noexcept + { + invoke_delete_action_if_necessary(); + } + + /** + * \brief Move constructor, which relocates the ownership of the value to the target and resets the source. Delete actions will be copied. + * \param other The source object which will lose ownership, if it has any. + */ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR unique_handle + ( + unique_handle&& other + ) noexcept(noexcept(std::is_nothrow_move_constructible_v + && std::is_nothrow_copy_constructible_v)) + : m_Value{ std::exchange(other.m_Value, std::nullopt) }, + m_DeleteAction{ other.m_DeleteAction } + { + } + + /** + * \brief Move assignment, which relocates the ownership of the value to the target and resets the source. Delete actions will be copied. + * \param other The source object which will lose ownership, if it has any. + * \return A reference to the target object + */ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR unique_handle& operator = + ( + unique_handle&& other + ) noexcept(noexcept(std::is_nothrow_move_constructible_v + && std::is_nothrow_move_assignable_v + && std::is_nothrow_copy_constructible_v + && std::is_nothrow_copy_assignable_v)) + { + if (this != &other) + { + invoke_delete_action_if_necessary(); + + m_Value = std::exchange(other.m_Value, std::nullopt); + m_DeleteAction = other.m_DeleteAction; + } + return *this; + } + + /** + * \brief Swaps the target and the source in a more efficient way. + * \param other The source object + */ + constexpr void swap + ( + unique_handle& other + ) noexcept(noexcept(std::is_nothrow_move_constructible_v + && std::is_nothrow_swappable_v + && std::is_nothrow_move_constructible_v + && std::is_nothrow_swappable_v)) + { + using std::swap; + + swap(m_Value, other.m_Value); + swap(m_DeleteAction, other.m_DeleteAction); + } + + /** + * \brief Explicitly deleted copy constructor. + */ + unique_handle(const unique_handle&) = delete; + /** + * \brief Explicitly deleted copy assignment. + * \return nothing + */ + unique_handle& operator =(const unique_handle&) = delete; + + /** + * \brief Explicitly does not initialize the value. This overload is mainly used for convenience when returning + * a \ref nullhandle. + * \param deleteAction The provided delete action object + */ + constexpr unique_handle(nullhandle_t, const delete_action_type& deleteAction = delete_action_type()) noexcept + : m_Value{ std::nullopt }, + m_DeleteAction{ deleteAction } + { + } + + /** + * \brief Explicitly resets the value and invokes the delete action if value was initialized. + */ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR unique_handle& operator =(nullhandle_t) noexcept + { + invoke_delete_action_if_necessary(); + + m_Value = std::nullopt; + return *this; + } + + /** + * \brief Constructor overload for explicitly providing a delete action. + * \param deleteAction The provided delete action object + */ + constexpr unique_handle(const delete_action_type& deleteAction) noexcept + : m_Value{ std::nullopt }, + m_DeleteAction{ deleteAction } + { + } + + /** + * \brief Constructor overload for initializing the value. + * \tparam T2 Type of the provided value. Must be convertible to ``T``. + * \param value Used object to initialize the value + * \param deleteAction The provided delete action object + */ + template T2> + requires concepts::not_same_as, unique_handle> + && concepts::not_same_as, nullhandle_t> + explicit (!std::convertible_to) + constexpr unique_handle(T2&& value, const delete_action_type& deleteAction = delete_action_type{}) + : m_Value{ std::forward(value) }, + m_DeleteAction{ deleteAction } + { + } + + /** + * \brief Assignment operator overload for assigning the value. + * \tparam T2 Type of the provided value. Must be convertible to ``T``. + * \param value Used object to assign the value + * \return A reference to this + */ + template T2> + requires concepts::not_same_as, unique_handle> + && concepts::not_same_as, nullhandle_t> + && concepts::constructs, T> + constexpr unique_handle& operator =(T2&& value) + { + invoke_delete_action_if_necessary(); + + m_Value = std::forward(value); + return *this; + } + + /** + * \brief Constructor overload for directly initializing the value with a set of arguments. + * \tparam TArgs Type of the provided arguments. + * \param args Used arguments to initialize the value + */ + template + requires std::constructible_from + constexpr explicit unique_handle(std::in_place_t, TArgs&&... args) + : m_Value{ std::in_place, std::forward(args)... } + { + } + + /** + * \brief Constructor overload for directly initializing the value with a set of arguments and also initializing + * the delete action. + * \tparam TArgs Type of the provided arguments. + * \param deleteAction The provided delete action object + * \param args Used arguments to initialize the value + */ + template + requires std::constructible_from + constexpr explicit unique_handle(std::in_place_t, const delete_action_type& deleteAction, TArgs&&... args) + : m_Value{ std::in_place, std::forward(args)... }, + m_DeleteAction{ deleteAction } + { + } + + /** + * \brief Constructor overload for directly initializing the value with a set of arguments. + * \tparam TArgs Type of the provided arguments. + * \param args Used arguments to initialize the value + */ + template + requires std::constructible_from + SL_UNIQUE_HANDLE_FULL_CONSTEXPR void emplace(TArgs&&... args) + { + m_Value.emplace(std::forward(args)...); + } + + /** + * \brief Resets the value and invokes the delete action if value was initialized. + */ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR void reset() noexcept + { + invoke_delete_action_if_necessary(); + + m_Value.reset(); + } + + /** + * \brief Immutable access to the value. No checks will be performed. + * \exception Throws bad_handle_access if value is uninitialized. + * \return A const reference to the value + */ + [[nodiscard]] + constexpr const T& raw() const { return m_Value.value(); } + + /** + * \brief Immutable access to the value. No checks will be performed. + * \return A const reference to the value + */ + [[nodiscard]] + constexpr const T& operator *() const noexcept { return *m_Value; } + + /** + * \brief Immutable access to the value. No checks will be performed. + * \return A const pointer to the value + */ + [[nodiscard]] + constexpr const T* operator ->() const noexcept { return &*m_Value; } + + /** + * \brief Checks whether the value is initialized. + * \return True if value is initialized + */ + [[nodiscard]] + constexpr explicit operator bool() const noexcept { return m_Value.has_value(); } + + /** + * \brief Checks whether the value is initialized. + * \return True if value is initialized + */ + [[nodiscard]] + constexpr bool is_valid() const noexcept { return m_Value.has_value(); } + + /** + * \brief Immutable access to the delete action. + * \return A const reference to the delete action + */ + [[nodiscard]] + constexpr const delete_action_type& delete_action() const noexcept { return m_DeleteAction; } + + private: + std::optional m_Value{}; + + [[no_unique_address]] + TDeleteAction m_DeleteAction{}; + + constexpr void invoke_delete_action_if_necessary() noexcept + { + if (m_Value) + std::invoke(m_DeleteAction, *m_Value); + } + }; + + /** + * \brief Deduction guide for \ref unique_handle class + * \tparam T Value type + * \tparam TDeleteAction Delete action type + */ + template + unique_handle + ( + T, + TDeleteAction + ) -> unique_handle>, detail::type_t>>; + + /** + * \brief Deduction guide for \ref unique_handle class + * \tparam T Value type + */ + template + unique_handle(T) -> unique_handle; + + /** + * \brief Three-way-comparison operator overload between two \ref unique_handle "unique_handles". + * \tparam T Value type of the handles + * \tparam TDeleteAction Delete action type of the handles + * \param lhs left-hand-side of the operation + * \param rhs right-hand-side of the operation + * \return If both handles have initialized values, both values will be compared. Otherwise they will be compared in accordance + * to the result of ``is_valid()``. + */ + template + [[nodiscard]] + constexpr std::compare_three_way_result_t operator <=> + ( + const unique_handle& lhs, + const unique_handle& rhs + ) + { + if (lhs && rhs) + { + return *lhs <=> *rhs; + } + return lhs.is_valid() <=> rhs.is_valid(); + } + + /** + * \brief Three-way-comparison operator overload for comparison between a \ref unique_handle and a value. + * \tparam T Value type of the handles + * \tparam TDeleteAction Delete action type of the handles + * \tparam T2 Type of right-hand-side. Must be three-way-comparable to ``T`` + * \param lhs left-hand-side of the operation + * \param rhs right-hand-side of the operation + * \return If the handle's value is initialized, both values will be compared. Otherwise handle is less. + */ + template T2> + [[nodiscard]] + constexpr std::compare_three_way_result_t operator <=>(const unique_handle& lhs, const T2& rhs) + { + if (lhs) + { + return *lhs <=> rhs; + } + return std::compare_three_way_result_t::less; + } + + /** + * \brief Three-way-comparison operator overload for comparison of \ref unique_handle and \ref nullhandle_t + * \tparam T Value type of the handles + * \tparam TDeleteAction Delete action type of the handles + * \param lhs left-hand-side of the operation + * \return Returns ``std::strong_ordering::equal`` if handle's value is uninitialized, otherwise ``std::strong_ordering::greater``. + */ + template + [[nodiscard]] + constexpr std::strong_ordering operator <=>(const unique_handle& lhs, nullhandle_t) noexcept + { + return lhs.is_valid() <=> false; + } + + /** @} */ +} + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 038c71d07..d95662e74 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,8 @@ add_executable( "main.cpp" "concepts/shift_operators.cpp" "concepts/logical_arithmetic_operators.cpp" + "unique_handle.cpp" + "concepts/stl_counterparts.cpp" ) target_link_libraries( diff --git a/tests/concepts/stl_counterparts.cpp b/tests/concepts/stl_counterparts.cpp new file mode 100644 index 000000000..f7f091c1f --- /dev/null +++ b/tests/concepts/stl_counterparts.cpp @@ -0,0 +1,76 @@ +// Copyright Dominic Koepke 2019 - 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include + +#include "../helper.hpp" + +#include "Simple-Utility/concepts/stl_counterparts.hpp" + +using namespace sl::concepts; + +namespace +{ + struct target_t + { + target_t() = default; + + target_t(int) + { + } + + target_t& operator =(int) + { + return *this; + } + }; +} + +#pragma warning(disable: 26444) +TEMPLATE_TEST_CASE_SIG +( + "constructs should behave as the symmetrical counterpart of std::constructible_from with one constructor argument", + "[concepts][stl_ext]", + ((class TSource, class TTarget, bool VExpected), TSource, TTarget, VExpected), + (int, int, true), + (int, target_t, true), + (target_t, int, false) +) +#pragma warning(default: 26444) +{ + REQUIRE(constructs == VExpected); +} + +#pragma warning(disable: 26444) +TEMPLATE_TEST_CASE_SIG +( + "assignable_to should behave as the symmetrical counterpart of std::assignable_from.", + "[concepts][stl_ext]", + ((class TSource, class TTarget, bool VExpected), TSource, TTarget, VExpected), + (int, int&, true), + (int, target_t&, true), + (int, target_t, false), + (target_t, int&, false) +) +#pragma warning(default: 26444) +{ + REQUIRE(assignable_to == VExpected); +} + +#pragma warning(disable: 26444) +TEMPLATE_TEST_CASE_SIG +( + "not_same_as should behave as the inverted counterpart of std::same_as.", + "[concepts][stl_ext]", + ((class TSource, class TTarget, bool VExpected), TSource, TTarget, VExpected), + (int, int, false), + (int, target_t, true), + (target_t&, target_t, true), + (const target_t&, const target_t&, false) +) +#pragma warning(default: 26444) +{ + REQUIRE(not_same_as == VExpected); +} diff --git a/tests/unique_handle.cpp b/tests/unique_handle.cpp new file mode 100644 index 000000000..da4c28a0c --- /dev/null +++ b/tests/unique_handle.cpp @@ -0,0 +1,492 @@ +// Copyright Dominic Koepke 2019 - 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include + +#include "helper.hpp" + +#include "Simple-Utility/unique_handle.hpp" + +using namespace sl; + +namespace +{ + struct value_t + { + int a{}; + int z{}; + + constexpr value_t(int a) + : a{ a } + { + } + + constexpr value_t(int a, int z) + : a{ a }, + z{ z } + { + } + + constexpr value_t(value_t&&) noexcept = default; + constexpr value_t& operator =(value_t&&) noexcept = default; + }; + + struct delete_action_mock + { + int* invoke_counter{}; + + constexpr void operator ()(auto&) noexcept + { + if (invoke_counter) + ++(*invoke_counter); + } + }; + + using test_handle = unique_handle; +} + +TEST_CASE("unique_handle should be default constructible.", "[unique_handle]") +{ + STATIC_REQUIRE(std::default_initializable); +} + +TEST_CASE("unique_handle should neither be copy constructible nor assignable.", "[unique_handle]") +{ + STATIC_REQUIRE(!std::copy_constructible); + STATIC_REQUIRE(!std::assignable_from); +} + +TEST_CASE("default constructed unique_handle should not contain a value.", "[unique_handle]") +{ + constexpr test_handle handle{}; + + STATIC_REQUIRE(!handle.is_valid()); + STATIC_REQUIRE(!handle); +} + +TEST_CASE("unique_handle should be explicitly null constructible by nullhandle.", "[unique_handle]") +{ + constexpr test_handle handle{ nullhandle }; + + STATIC_REQUIRE(!handle.is_valid()); + STATIC_REQUIRE(!handle); +} + +TEST_CASE("unique_handle should be empty constructible by nullhandle and deleteAction.", "[unique_handle]") +{ + constexpr bool result = [] + { + int testValue = 1337; + const test_handle handle{ nullhandle, delete_action_mock{ &testValue } }; + + return !handle && *handle.delete_action().invoke_counter == 1337; + }(); + + REQUIRE(result); +} + +TEST_CASE("unique_handle should be assignable by nullhandle.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const test_handle handle = [] + { + // ReSharper disable once CppInitializedValueIsAlwaysRewritten + test_handle temp{}; + temp = nullhandle; + return temp; + }(); + + REQUIRE(!handle.is_valid()); + REQUIRE(!handle); +} + +TEST_CASE("unique_handle should be empty constructible by deleteAction.", "[unique_handle]") +{ + constexpr bool result = [] + { + int testValue = 1337; + const test_handle handle{ delete_action_mock{ &testValue } }; + + return !handle && *handle.delete_action().invoke_counter == 1337; + }(); + + REQUIRE(result); +} + +TEST_CASE("unique_handle should be constructible by value.", "[unique_handle]") +{ + constexpr test_handle handle{ 42 }; + + STATIC_REQUIRE(handle.is_valid()); + STATIC_REQUIRE(handle); +} + +TEST_CASE("unique_handle should be constructible by value and deleteAction.", "[unique_handle]") +{ + constexpr bool result = [] + { + int testValue = 1337; + const test_handle handle{ 42, delete_action_mock{ &testValue } }; + + return *handle == 42 && *handle.delete_action().invoke_counter == 1337; + }(); + + REQUIRE(result); +} + +TEST_CASE("unique_handle should be assignable by value.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const test_handle handle = [] + { + // ReSharper disable once CppInitializedValueIsAlwaysRewritten + test_handle temp{}; + temp = 42; + return temp; + }(); + + REQUIRE(handle.is_valid()); + REQUIRE(handle); +} + +TEST_CASE("unique_handle should automatically deduct its template arguments when constructed by value and deleteAction.", "[unique_handle]") +{ + constexpr unique_handle handle{ 42, delete_action_mock{} }; + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); +} + +TEST_CASE("unique_handle should automatically deduct its template arguments when constructed by value.", "[unique_handle]") +{ + constexpr unique_handle handle{ 42 }; + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); +} + +TEST_CASE("unique_handle should be explicitly in-place construct value when std::in_place token is used.", "[unique_handle]") +{ + constexpr unique_handle handle{ std::in_place, 42, 1337 }; + + STATIC_REQUIRE(handle->a == 42); + STATIC_REQUIRE(handle->z == 1337); +} + +TEST_CASE +( + "unique_handle should be explicitly in-place construct value and deleteAction when std::in_place token is used.", + "[unique_handle]" +) +{ + constexpr bool result = [] + { + int testValue = 1337; + const unique_handle handle{ std::in_place, delete_action_mock{ &testValue }, 42, -1 }; + + return handle->a == 42 + && handle->z == -1 + && *handle.delete_action().invoke_counter == 1337; + }(); + + REQUIRE(result); +} + +TEST_CASE("unique_handle::emplace constructs value in place.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + unique_handle handle{}; + handle.emplace(1337, 42); + return handle->a == 1337 && handle->z == 42; + }(); + + REQUIRE(result); +} + +#pragma warning(disable: 26444) +TEMPLATE_TEST_CASE_SIG +( + "delete action on assignment must only be invoked if unique_handle holds a value.", + "[unique_handle]", + ((class TInit, class TAssign, bool VExpected), TInit, TAssign, VExpected), + (int, int, true), + (int, nullhandle_t, true), + (nullhandle_t, int, false), + (nullhandle_t, nullhandle_t, false) +) +#pragma warning(disable: 26444) +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + int counter{}; + test_handle temp{ TInit{}, delete_action_mock{ .invoke_counter = &counter } }; + temp = TAssign{}; + return counter == 1; + }(); + + REQUIRE(result == VExpected); +} + +TEST_CASE("unique_handle should be move constructible and invalidate the source.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + test_handle source{ 42 }; + const test_handle target{ std::move(source) }; + return !source.is_valid() && target.is_valid(); + }(); + + REQUIRE(result); +} + +TEST_CASE("unique_handle should be move constructible and receive the value of other.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + // ReSharper disable once CppInitializedValueIsAlwaysRewritten + test_handle source{ 42 }; + const test_handle target{ std::move(source) }; + return target.raw() == 42; + }(); + + REQUIRE(result); +} + +TEST_CASE("unique_handle should be move assignable and invalidate the source.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + test_handle source{ 42 }; + // ReSharper disable once CppInitializedValueIsAlwaysRewritten + test_handle target{ nullhandle }; + target = std::move(source); + return !source.is_valid() && target.is_valid(); + }(); + + REQUIRE(result); +} + +TEST_CASE("unique_handle should be move assignable and receive the value of other.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + test_handle source{ 1337 }; + // ReSharper disable once CppInitializedValueIsAlwaysRewritten + test_handle target{ nullhandle }; + target = std::move(source); + return target.raw() == 1337; + }(); + + REQUIRE(result); +} + +TEST_CASE("moving unique_handle with itself should change nothing.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + test_handle handle{ 1337 }; + handle = std::move(handle); + return handle.is_valid() && *handle == 1337; + }(); + + REQUIRE(result); +} + +TEST_CASE("swapping unique_handle with itself should change nothing.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + test_handle handle{ 1337 }; + handle.swap(handle); + return handle.is_valid() && *handle == 1337; + }(); + + REQUIRE(result); +} + +TEST_CASE("unique_handle should be swapable.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + test_handle lhs{ 1337 }; + test_handle rhs{ 42 }; + lhs.swap(rhs); + return *lhs == 42 && *rhs == 1337; + }(); + + REQUIRE(result); +} + +TEST_CASE("unique_handle::raw should expose a const reference of its value.", "[unique_handle]") +{ + constexpr bool result = [] + { + test_handle handle{ 1337 }; + const int& ref{ handle.raw() }; + return ref == 1337; + }(); + + STATIC_REQUIRE(result); +} + +TEST_CASE("unique_handle::raw should throw bad_handle_access if no value is hold.", "[unique_handle]") +{ + constexpr test_handle handle{}; + + REQUIRE_THROWS_AS(handle.raw(), bad_handle_access); +} + +TEST_CASE("unique_handle's operator * should expose a const reference of its value.", "[unique_handle]") +{ + constexpr bool result = [] + { + test_handle handle{ 42 }; + const int& ref{ *handle }; + return ref == 42; + }(); + + REQUIRE(result); +} + +TEMPLATE_TEST_CASE +( + "unique_handle's operator -> overload should expose a pointer to its value.", + "[unique_handle]", + test_handle, + const test_handle +) +{ + constexpr bool result = [] + { + TestType handle{ 1337 }; + auto* ptr = handle.operator ->(); + return *ptr == 1337; + }(); + + STATIC_REQUIRE(result); +} + +TEST_CASE("unique_handle::reset should reset to a nullhandle.", "[unique_handle]") +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const test_handle handle = [] + { + test_handle temp{ 42 }; + temp.reset(); + return temp; + }(); + + REQUIRE(!handle); +} + +TEST_CASE("resetting a handle without value should do nothing.", "[unique_handle]") +{ + REQUIRE_NOTHROW + ( + [] + { + test_handle temp{ nullhandle }; + temp.reset(); + }() + ); +} + +#pragma warning(disable: 26444) +TEMPLATE_TEST_CASE_SIG +( + "delete action on reset must only be invoked if unique_handle holds a value.", + "[unique_handle]", + ((class TInit, bool VExpected), TInit, VExpected), + (int, true), + (nullhandle_t, false) +) +#pragma warning(disable: 26444) +{ + SL_UNIQUE_HANDLE_FULL_CONSTEXPR + const bool result = [] + { + int counter{}; + test_handle temp{ TInit{}, delete_action_mock{ .invoke_counter = &counter } }; + temp.reset(); + return counter == 1; + }(); + + REQUIRE(result == VExpected); +} + +TEST_CASE("unique_handle::delete_action should return a reference to the used deleter action object.", "[unique_handle]") +{ + constexpr bool result = [] + { + test_handle temp{ 42 }; + + return &temp.delete_action() == &std::as_const(temp).delete_action(); + }(); + + STATIC_REQUIRE(result); +} + +#pragma warning(disable: 26444) +TEMPLATE_TEST_CASE_SIG +( + "delete action on destruction must only be invoked if unique_handle holds a value.", + "[unique_handle]", + ((class TInit, bool VExpected), TInit, VExpected), + (int, true), + (nullhandle_t, false) +) +#pragma warning(disable: 26444) +{ + constexpr bool result = [] + { + int counter{}; + { + const test_handle temp{ TInit{}, delete_action_mock{ .invoke_counter = &counter } }; + } + return counter == 1; + }(); + + REQUIRE(result == VExpected); +} + +TEST_CASE("unique_handle should be three-way-comparable with unqiue_handle of same type.", "[unique_handle]") +{ + STATIC_REQUIRE((test_handle{} <=> test_handle{}) == std::strong_ordering::equal); + STATIC_REQUIRE((test_handle{} <=> test_handle{ 42 }) == std::strong_ordering::less); + STATIC_REQUIRE((test_handle{42 } <=> test_handle{}) == std::strong_ordering::greater); + STATIC_REQUIRE((test_handle{42 } <=> test_handle{1337}) == std::strong_ordering::less); +} + +TEST_CASE("unique_handle should be three-way-comparable with nullhandle.", "[unique_handle]") +{ + STATIC_REQUIRE((nullhandle <=> test_handle{ 42 }) == std::strong_ordering::less); + STATIC_REQUIRE((nullhandle <=> test_handle{}) == std::strong_ordering::equal); + STATIC_REQUIRE((test_handle{ 42 } <=> nullhandle) == std::strong_ordering::greater); + STATIC_REQUIRE((test_handle{} <=> nullhandle) == std::strong_ordering::equal); +} + +TEST_CASE("unique_handle should be three-way-comparable with value type.", "[unique_handle]") +{ + STATIC_REQUIRE((42 <=> test_handle{ 1337 }) == std::strong_ordering::less); + STATIC_REQUIRE((1337 <=> test_handle{ 42 }) == std::strong_ordering::greater); + STATIC_REQUIRE((test_handle{ 1337 } <=> 42 ) == std::strong_ordering::greater); + STATIC_REQUIRE((test_handle{ 42 } <=> 1337) == std::strong_ordering::less); + + STATIC_REQUIRE((1337 <=> test_handle{}) == std::strong_ordering::greater); + STATIC_REQUIRE((test_handle{} <=> 1337) == std::strong_ordering::less); + + STATIC_REQUIRE((42 <=> test_handle{ 42 }) == std::strong_ordering::equal); + STATIC_REQUIRE((test_handle{ 42 } <=> 42) == std::strong_ordering::equal); +}