From 459fbdcf4862fd29dd111860bbf64fb9551a8504 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sat, 10 Jan 2026 18:50:18 +0100 Subject: [PATCH 1/9] Add Windows CI build too --- .github/workflows/cmake.yml | 68 +++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 4 ++- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/cmake.yml diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 0000000..ef7638f --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,68 @@ +--- +name: CMake + +on: + push: + branches: ["develop"] + pull_request: + branches: ["develop"] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + schedule: + # run at 15:30 on day-of-month 7. + - cron: '30 15 7 * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: release + CTEST_OUTPUT_ON_FAILURE: 1 + +jobs: + build: + strategy: + fail-fast: false + + matrix: + os: [windows] + + runs-on: ${{ matrix.os }}-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup build environment + uses: lukka/get-cmake@latest + with: + cmakeVersion: "~4.2.1" + ninjaVersion: "^1.13.0" + + - name: Setup MSVC + if: startsWith(matrix.os, 'windows') + uses: TheMrMilchmann/setup-msvc-dev@v3 + with: + arch: x64 + + - name: Setup Cpp + if: matrix.os != 'windows' + uses: aminya/setup-cpp@v1 + with: + compiler: llvm + + - name: Configure CMake + run: cmake --preset msvc-${{env.BUILD_TYPE}} --log-level=VERBOSE + + - name: Build + # Build your program with the given configuration + run: cmake --build --preset msvc-${{env.BUILD_TYPE}} + + # - name: Install + # # Install the project artefacts to CMAKE_INSTALL_PREFIX + # run: cmake --install ${{github.workspace}}/build/${{env.BUILD_TYPE}} --config ${{env.BUILD_TYPE}} + + - name: Test + working-directory: ${{github.workspace}}/build/${{env.BUILD_TYPE}} + # Execute tests defined by the CMake configuration, but needs to find_package(Boost)! + run: ctest -C msvc-${{env.BUILD_TYPE}} diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b36f29..ce92578 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,11 +11,13 @@ project( # gersemi: off -# Modules opt in only on compilers that support g++-15 and clang-20+ +# Modules opt in only on compilers that support it: msvc, g++-15 and clang-20+ if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 20) set(CMAKE_CXX_SCAN_FOR_MODULES 1) elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15) set(CMAKE_CXX_SCAN_FOR_MODULES 1) +elseif(MSVC) + set(CMAKE_CXX_SCAN_FOR_MODULES 1) else() set(CMAKE_CXX_SCAN_FOR_MODULES 0) endif() From f3c5c4af29d897375b1d00c7c60fee19ce05d66f Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sat, 10 Jan 2026 19:19:15 +0100 Subject: [PATCH 2/9] Use portable C++20 check and portable C++ feature checks --- include/beman/scope/scope.hpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/include/beman/scope/scope.hpp b/include/beman/scope/scope.hpp index 7d33b2c..e768259 100644 --- a/include/beman/scope/scope.hpp +++ b/include/beman/scope/scope.hpp @@ -10,12 +10,30 @@ #include // clang-format off -#if __cplusplus < 202002L +#include + +#if defined(__cpp_concepts) && __cpp_concepts >= 201907L + // C++20 concepts supported +#elif __cplusplus < 202002L #error "C++20 or later is required" #endif -// clang-format on -#include //todo unconditional for unique_resource +// detect standard header first, then experimental, otherwise use local implementation +#if defined(__has_include) +# if __has_include() +# include +# define BEMAN_SCOPE_USE_STD +# elif __has_include() +# include +# define BEMAN_SCOPE_USE_STD_EXPERIMENTAL +# else +// no std scope header — fall through to local implementation below +# endif +#elif defined(__cpp_lib_scope) && __cpp_lib_scope >= 2023xxxxL +# include +# define BEMAN_SCOPE_USE_STD +#endif +// clang-format on #ifdef BEMAN_SCOPE_USE_STD_EXPERIMENTAL From 9ad29b70fba901b194f1760ff91d51b01c7cd7f0 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sat, 10 Jan 2026 19:35:01 +0100 Subject: [PATCH 3/9] Use C++23 on Windows --- CMakePresets.json | 2 ++ include/beman/scope/scope.hpp | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index 483e1a3..655058f 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -101,6 +101,7 @@ "_debug-base" ], "cacheVariables": { + "CMAKE_CXX_STANDARD": "23", "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" } }, @@ -112,6 +113,7 @@ "_release-base" ], "cacheVariables": { + "CMAKE_CXX_STANDARD": "23", "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" } } diff --git a/include/beman/scope/scope.hpp b/include/beman/scope/scope.hpp index e768259..d18f154 100644 --- a/include/beman/scope/scope.hpp +++ b/include/beman/scope/scope.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -67,7 +68,7 @@ namespace beman::scope { // todo temporary template -using unique_resource = std::experimental::unique_resource; +using unique_resource = std::unique_resource; // todo temporary template > From 01a4dcf6d65803bb3da33a6718da441bfdeaf3d8 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sat, 10 Jan 2026 20:23:31 +0100 Subject: [PATCH 4/9] Quickfix to compile with C++23 --- include/beman/scope/scope.hpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/include/beman/scope/scope.hpp b/include/beman/scope/scope.hpp index d18f154..df45bb9 100644 --- a/include/beman/scope/scope.hpp +++ b/include/beman/scope/scope.hpp @@ -50,15 +50,15 @@ template using scope_success = std::experimental::scope_success; // todo temporary -// template -// using unique_resource = std::experimental::unique_resource; +template +using unique_resource = std::experimental::fundamentals_v3::unique_resource; -// template > -// unique_resource, std::decay_t> -// make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept(noexcept( -// std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)))) { -// return std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)); -//} +template > +unique_resource, std::decay_t> +make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept(noexcept( + std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)))) { + return std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)); +} } // namespace beman::scope @@ -71,11 +71,11 @@ template using unique_resource = std::unique_resource; // todo temporary -template > -unique_resource, std::decay_t > -make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept(noexcept( - std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)))) { - return std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)); +template > +unique_resource, std::decay_t> +make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept( + noexcept(std::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)))) { + return std::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)); } //================================================================================================== From 6f7560373938ac2dbe303c48a1cce99287e75dd9 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 11 Jan 2026 21:59:08 +0100 Subject: [PATCH 5/9] Add simple BEMAN_SCOPE_USE_FALLBACK code This compiles with clang++-21 on OSX and should build on Windows too. --- include/beman/scope/scope.hpp | 17 +- include/beman/scope/scope_impl.hpp | 281 +++++++++++++++++++++++++++++ 2 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 include/beman/scope/scope_impl.hpp diff --git a/include/beman/scope/scope.hpp b/include/beman/scope/scope.hpp index df45bb9..9b45233 100644 --- a/include/beman/scope/scope.hpp +++ b/include/beman/scope/scope.hpp @@ -33,6 +33,8 @@ #elif defined(__cpp_lib_scope) && __cpp_lib_scope >= 2023xxxxL # include # define BEMAN_SCOPE_USE_STD +#else +# warning "Missing feature __cpp_lib_scope" #endif // clang-format on @@ -62,20 +64,20 @@ make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept(noexcept( } // namespace beman::scope -#else // ! BEMAN_SCOPE__USE_STD_EXPERIMENTAL +#elif defined(BEMAN_SCOPE_USE_STD) namespace beman::scope { // todo temporary template -using unique_resource = std::unique_resource; +using unique_resource = std::experimental::unique_resource; // todo temporary template > unique_resource, std::decay_t> -make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept( - noexcept(std::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)))) { - return std::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)); +make_unique_resource_checked(R&& r, const S& invalid, D&& d) noexcept(noexcept( + std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)))) { + return std::experimental::make_unique_resource_checked(std::forward(r), std::forward(invalid), std::forward(d)); } //================================================================================================== @@ -440,6 +442,9 @@ using scope_fail = scope_guard + +#if defined(__cpp_concepts) && __cpp_concepts >= 201907L + // C++20 concepts supported +#elif __cplusplus < 202002L +# error "C++20 or later is required" +#endif + +// detect standard header first, then experimental, otherwise use local implementation +#ifdef __has_include +# if __has_include() +# include +# define BEMAN_SCOPE_USE_STD +# elif __has_include() +# include +# define BEMAN_SCOPE_USE_STD_EXPERIMENTAL +# else +# define BEMAN_SCOPE_USE_FALLBACK +# endif +#else +# define BEMAN_SCOPE_USE_FALLBACK +#endif + +#ifdef BEMAN_SCOPE_USE_STD +# if !defined(__cpp_lib_scope_exit) +# error "Standard present but __cpp_lib_scope_exit not defined" +# endif +#endif +// clang-format on + +#ifdef BEMAN_SCOPE_USE_FALLBACK +#include +#include +#include + +namespace beman::scope { + +// TODO(CK): make a std::experimental::scope_exit::scope_exit conform +// implementation +template +class [[nodiscard]] scope_exit { + F f; + bool active = true; + + public: + constexpr explicit scope_exit(F func) noexcept(std::is_nothrow_move_constructible_v) : f(std::move(func)) {} + + // Move constructor + constexpr scope_exit(scope_exit&& other) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(other.f)), active(other.active) { + other.active = false; + } + + // Deleted copy + auto operator=(const scope_exit&) -> scope_exit& = delete; + scope_exit(const scope_exit&) = delete; + + // Move assignment + constexpr auto operator=(scope_exit&& other) noexcept(std::is_nothrow_move_assignable_v) -> scope_exit& { + if (this != &other) { + f = std::move(other.f); + active = other.active; + other.active = false; + } + return *this; + } + + // Destructor: call only if scope is exiting normally + ~scope_exit() noexcept(noexcept(f())) { + if (active) { + f(); + } + } + + // Release to prevent execution + constexpr auto release() -> void { active = false; } +}; + +// Factory helper +template +static auto make_scope_exit(F f) -> scope_exit { + return scope_exit(std::move(f)); +} + +// TODO(CK): make a std::experimental::scope_fail::scope_fail conform +// implementation +template +class [[nodiscard]] scope_fail { + F f; + bool active = true; + int exception_count{}; + + public: + // Constructor: capture current uncaught exceptions + constexpr explicit scope_fail(F func) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(func)), exception_count(std::uncaught_exceptions()) {} + + // Move constructor + constexpr scope_fail(scope_fail&& other) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(other.f)), active(other.active), exception_count(other.exception_count) { + other.active = false; + } + + // Deleted copy + scope_fail(const scope_fail&) = delete; + auto operator=(const scope_fail&) -> scope_fail& = delete; + + // Move assignment + constexpr auto operator=(scope_fail&& other) noexcept(std::is_nothrow_move_assignable_v) -> scope_fail& { + if (this != &other) { + f = std::move(other.f); + active = other.active; + exception_count = other.exception_count; + other.active = false; + } + return *this; + } + + // Destructor: call if scope is exiting due to an exception + ~scope_fail() noexcept(noexcept(f())) { + if (active && std::uncaught_exceptions() > exception_count) { + f(); + } + } + + // Release to prevent execution + constexpr auto release() -> void { active = false; } +}; + +// Factory helper +template +constexpr auto make_scope_fail(F&& f) -> scope_fail> { + return scope_fail>(std::forward(f)); +} + +// TODO(CK): make a std::experimental::scope_success::scope_success conform +// implementation +template +class [[nodiscard]] scope_success { + F f; + bool active = true; + int exception_count{}; + + public: + // Constructor: capture current uncaught exceptions + constexpr explicit scope_success(F func) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(func)), exception_count(std::uncaught_exceptions()) {} + + // Move constructor + constexpr scope_success(scope_success&& other) noexcept(std::is_nothrow_move_constructible_v) + : f(std::move(other.f)), active(other.active), exception_count(other.exception_count) { + other.active = false; + } + + // Deleted copy + scope_success(const scope_success&) = delete; + auto operator=(const scope_success&) -> scope_success& = delete; + + // Move assignment + constexpr auto operator=(scope_success&& other) noexcept(std::is_nothrow_move_assignable_v) -> scope_success& { + if (this != &other) { + f = std::move(other.f); + active = other.active; + exception_count = other.exception_count; + other.active = false; + } + return *this; + } + + // Destructor: call only if scope is exiting normally + ~scope_success() noexcept(noexcept(f())) { + if (active && std::uncaught_exceptions() == exception_count) { + f(); + } + } + + // Release to prevent execution + constexpr auto release() -> void { active = false; } +}; + +// Factory helper +template +constexpr auto make_scope_success(F&& f) -> scope_success> { + return scope_success>(std::forward(f)); +} + +template +class [[nodiscard]] unique_resource { + Resource resource; + Deleter deleter; + bool active = true; + + public: + // Constructor + constexpr unique_resource(Resource r, Deleter d) noexcept(std::is_nothrow_move_constructible_v) + : resource(std::move(r)), deleter(std::move(d)) {} + + // Move constructor + constexpr unique_resource(unique_resource&& other) noexcept(std::is_nothrow_move_constructible_v) + : resource(std::move(other.resource)), deleter(std::move(other.deleter)), active(other.active) { + other.active = false; + } + + // Move assignment + constexpr auto operator=(unique_resource&& other) noexcept(std::is_nothrow_move_assignable_v) + -> unique_resource& { + if (this != &other) { + reset(std::move(other.resource)); + deleter = std::move(other.deleter); + active = other.active; + other.active = false; + } + return *this; + } + + // Deleted copy operations + unique_resource(const unique_resource&) = delete; + auto operator=(const unique_resource&) -> unique_resource& = delete; + + // Destructor + ~unique_resource() noexcept(noexcept(deleter(resource))) { + if (active) { + deleter(resource); + } + } + + // Release ownership + constexpr void release() noexcept(noexcept(deleter(resource))) { active = false; } + + // Reset resource + constexpr void reset() noexcept(noexcept(deleter(resource))) { + if (active) { + deleter(resource); + } + active = false; + } + constexpr void reset(Resource new_resource) noexcept(noexcept(deleter(resource))) { + if (active) { + deleter(resource); + } + resource = std::move(new_resource); + active = true; + } + + // Accessors + constexpr auto get() const -> const Resource& { return resource; } + constexpr auto get() -> Resource& { return resource; } + + // operator* — only for non-void pointer resources + constexpr auto operator*() const noexcept -> std::add_lvalue_reference_t> + requires(std::is_pointer_v && !std::is_void_v>) + { + return *resource; + } + + // operator-> — only for pointer resources + constexpr auto operator->() const noexcept -> Resource + requires std::is_pointer_v + { + return resource; + } + + // Check if active + constexpr explicit operator bool() const noexcept { return active; } +}; + +// Deduction guide +template +unique_resource(Resource&&, Deleter&&) -> unique_resource, std::decay_t>; + +} // namespace beman::scope + +#endif // BEMAN_SCOPE_USE_FALLBACK + +#endif // SCOPE_IMPL_HPP From d83e3f443e82980021bfd336f7e606dca61baa06 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 11 Jan 2026 22:23:05 +0100 Subject: [PATCH 6/9] Fix Windows CI test directory --- .github/workflows/cmake.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index ef7638f..09db9ed 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -58,11 +58,10 @@ jobs: # Build your program with the given configuration run: cmake --build --preset msvc-${{env.BUILD_TYPE}} + - name: Test + # Execute tests defined by the CMake configuration + run: ctest --preset msvc-${{env.BUILD_TYPE}} + # - name: Install # # Install the project artefacts to CMAKE_INSTALL_PREFIX - # run: cmake --install ${{github.workspace}}/build/${{env.BUILD_TYPE}} --config ${{env.BUILD_TYPE}} - - - name: Test - working-directory: ${{github.workspace}}/build/${{env.BUILD_TYPE}} - # Execute tests defined by the CMake configuration, but needs to find_package(Boost)! - run: ctest -C msvc-${{env.BUILD_TYPE}} + # run: cmake --build --preset msvc-${{env.BUILD_TYPE}} --target install From 2f5ab941ea93d03326d85a6461a7fd205bfccc74 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 13 Jan 2026 21:05:04 +0100 Subject: [PATCH 7/9] Refactory unique_resource code to be portable Add more test for unique_resource --- CMakePresets.json | 2 + examples/CMakeLists.txt | 1 + gcovr.cfg | 21 +++ include/beman/scope/scope_impl.hpp | 61 +++++-- makefile | 65 ++++++++ tests/CMakeLists.txt | 8 +- tests/unique_resource_2.test.cpp | 254 +++++++++++++++++++++++++++++ 7 files changed, 396 insertions(+), 16 deletions(-) create mode 100644 gcovr.cfg create mode 100644 makefile create mode 100644 tests/unique_resource_2.test.cpp diff --git a/CMakePresets.json b/CMakePresets.json index 655058f..91bb5e6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -79,6 +79,7 @@ "_debug-base" ], "cacheVariables": { + "CMAKE_CXX_STANDARD": "23", "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" } }, @@ -90,6 +91,7 @@ "_release-base" ], "cacheVariables": { + "CMAKE_CXX_STANDARD": "23", "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" } }, diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 23ffaf7..0f1d6d3 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -13,4 +13,5 @@ foreach(example ${ALL_EXAMPLES}) add_executable(${example}) target_sources(${example} PRIVATE ${example}.cpp) target_link_libraries(${example} PRIVATE beman::scope) + add_test(NAME ${example} COMMAND ${example}) endforeach() diff --git a/gcovr.cfg b/gcovr.cfg new file mode 100644 index 0000000..52add0f --- /dev/null +++ b/gcovr.cfg @@ -0,0 +1,21 @@ +root = . +search-path = build + +filter = examples/* +# filter = src/* +filter = include/* + +exclude-directories = stagedir +exclude-directories = build/*/*/_deps +exclude-directories = tests +exclude-directories = conan + +gcov-ignore-parse-errors = all +print-summary = yes + +html-details = build/coverage/index.html + +cobertura-pretty = yes +cobertura = build/cobertura.xml + +#TBD delete-gcov-files = yes diff --git a/include/beman/scope/scope_impl.hpp b/include/beman/scope/scope_impl.hpp index c03286a..e0cb393 100644 --- a/include/beman/scope/scope_impl.hpp +++ b/include/beman/scope/scope_impl.hpp @@ -17,9 +17,11 @@ # if __has_include() # include # define BEMAN_SCOPE_USE_STD +// XXX #warning "Set BEMAN_SCOPE_USE_STD" # elif __has_include() # include # define BEMAN_SCOPE_USE_STD_EXPERIMENTAL +// XXX #warning "Set BEMAN_SCOPE_USE_STD_EXPERIMENTAL" # else # define BEMAN_SCOPE_USE_FALLBACK # endif @@ -83,8 +85,9 @@ class [[nodiscard]] scope_exit { }; // Factory helper +// NOLINTNEXTLINE(misc-use-anonymous-namespace) template -static auto make_scope_exit(F f) -> scope_exit { +auto make_scope_exit(F f) -> scope_exit { return scope_exit(std::move(f)); } @@ -134,6 +137,7 @@ class [[nodiscard]] scope_fail { }; // Factory helper +// NOLINTNEXTLINE(misc-use-anonymous-namespace) template constexpr auto make_scope_fail(F&& f) -> scope_fail> { return scope_fail>(std::forward(f)); @@ -185,6 +189,7 @@ class [[nodiscard]] scope_success { }; // Factory helper +// NOLINTNEXTLINE(misc-use-anonymous-namespace) template constexpr auto make_scope_success(F&& f) -> scope_success> { return scope_success>(std::forward(f)); @@ -203,8 +208,8 @@ class [[nodiscard]] unique_resource { // Move constructor constexpr unique_resource(unique_resource&& other) noexcept(std::is_nothrow_move_constructible_v) - : resource(std::move(other.resource)), deleter(std::move(other.deleter)), active(other.active) { - other.active = false; + : resource(std::move(other.resource)), deleter(std::move(other.deleter)) { + active = std::exchange(other.active, false); } // Move assignment @@ -212,9 +217,8 @@ class [[nodiscard]] unique_resource { -> unique_resource& { if (this != &other) { reset(std::move(other.resource)); - deleter = std::move(other.deleter); - active = other.active; - other.active = false; + deleter = std::move(other.deleter); + active = std::exchange(other.active, false); } return *this; } @@ -224,22 +228,20 @@ class [[nodiscard]] unique_resource { auto operator=(const unique_resource&) -> unique_resource& = delete; // Destructor - ~unique_resource() noexcept(noexcept(deleter(resource))) { - if (active) { - deleter(resource); - } - } + ~unique_resource() noexcept(noexcept(deleter(resource))) { reset(); } // Release ownership - constexpr void release() noexcept(noexcept(deleter(resource))) { active = false; } + constexpr void release() noexcept { active = false; } // Reset resource constexpr void reset() noexcept(noexcept(deleter(resource))) { if (active) { + active = false; deleter(resource); } - active = false; } + + // Reset the resource and call deleter if engaged constexpr void reset(Resource new_resource) noexcept(noexcept(deleter(resource))) { if (active) { deleter(resource); @@ -259,14 +261,17 @@ class [[nodiscard]] unique_resource { return *resource; } - // operator-> — only for pointer resources + // Optional pointer convenience constexpr auto operator->() const noexcept -> Resource requires std::is_pointer_v { return resource; } - // Check if active + // TODO(CK): missing usecase? + constexpr auto get_deleter() const noexcept -> Deleter; + + // NOTE: check if active; not required from LWG? constexpr explicit operator bool() const noexcept { return active; } }; @@ -274,8 +279,34 @@ class [[nodiscard]] unique_resource { template unique_resource(Resource&&, Deleter&&) -> unique_resource, std::decay_t>; +// Factory: conditionally engaged +// NOLINTNEXTLINE(misc-use-anonymous-namespace) +template +constexpr auto make_unique_resource_checked(R&& r, const Invalid& invalid, D&& d) { + using resource_type = std::decay_t; + using deleter_type = std::decay_t; + + if (r == invalid) { + // Disengaged resource + unique_resource ur(resource_type{}, std::forward(d)); + ur.release(); // disengage immediately + return ur; + } + + return unique_resource(std::forward(r), std::forward(d)); +} + } // namespace beman::scope +#elifdef BEMAN_SCOPE_USE_STD_EXPERIMENTAL + +namespace beman::scope { +using std::experimental::scope_exit; +using std::experimental::scope_fail; +using std::experimental::scope_success; +using std::experimental::unique_resource; +} // namespace beman::scope + // #endif // BEMAN_SCOPE_USE_FALLBACK #endif // SCOPE_IMPL_HPP diff --git a/makefile b/makefile new file mode 100644 index 0000000..22b4fc4 --- /dev/null +++ b/makefile @@ -0,0 +1,65 @@ +# Standard stuff + +.SUFFIXES: + +MAKEFLAGS+= --no-builtin-rules # Disable the built-in implicit rules. +# MAKEFLAGS+= --warn-undefined-variables # Warn when an undefined variable is referenced. +# MAKEFLAGS+= --include-dir=$(CURDIR)/conan # Search DIRECTORY for included makefiles (*.mk). + +export hostSystemName=$(shell uname) + +ifeq (${hostSystemName},Darwin) + export LLVM_PREFIX:=$(shell brew --prefix llvm) + export LLVM_DIR:=$(shell realpath ${LLVM_PREFIX}) + export PATH:=${LLVM_DIR}/bin:${PATH} + + export CMAKE_CXX_STDLIB_MODULES_JSON=${LLVM_DIR}/lib/c++/libc++.modules.json + export CXX=clang++ + export LDFLAGS=-L$(LLVM_DIR)/lib/c++ -lc++abi # XXX -lc++ -lc++experimental + # FIXME: export GCOV="llvm-cov gcov" + + ### TODO: to test g++-15: + export GCC_PREFIX:=$(shell brew --prefix gcc) + export GCC_DIR:=$(shell realpath ${GCC_PREFIX}) + + # export CMAKE_CXX_STDLIB_MODULES_JSON=${GCC_DIR}/lib/gcc/current/libstdc++.modules.json + # export CXX:=g++-15 + # export CXXFLAGS:=-stdlib=libstdc++ + # export GCOV="gcov" +else ifeq (${hostSystemName},Linux) + export LLVM_DIR=/usr/lib/llvm-20 + export PATH:=${LLVM_DIR}/bin:${PATH} + export CXX=clang++-20 +endif + +.PHONY: all install coverage clean distclean + +all: build/compile_commands.json + ln -sf $< . + ninja -C build + +build/compile_commands.json: CMakeLists.txt makefile + cmake -S . -B build -G Ninja --log-level=DEBUG -D CMAKE_BUILD_TYPE=Release \ + -D CMAKE_EXPERIMENTAL_CXX_IMPORT_STD="d0edc3af-4c50-42ea-a356-e2862fe7a444" \ + -D CMAKE_CXX_STDLIB_MODULES_JSON=${CMAKE_CXX_STDLIB_MODULES_JSON} \ + -D CMAKE_CXX_STANDARD=23 -D CMAKE_CXX_EXTENSIONS=YES -D CMAKE_CXX_STANDARD_REQUIRED=YES \ + -D CMAKE_CXX_FLAGS='-fno-inline --coverage' \ + -D CMAKE_CXX_MODULE_STD=NO \ + -D CMAKE_INSTALL_MESSAGE=LAZY # XXX -D CMAKE_SKIP_INSTALL_RULES=YES # --fresh + +install: build/cmake_install.cmake + cmake --install build + +distclean: clean + rm -rf build + find . -name '*~' -delete + +build/coverage: test + mkdir -p $@ + +coverage: build/coverage + gcovr # XXX -v + +# Anything we don't know how to build will use this rule. +% :: + ninja -C build $(@) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a7cbcfc..6acb735 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,7 +11,13 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(Catch2) -set(ALL_TESTNAMES scope_success scope_exit scope_fail unique_resource) +set(ALL_TESTNAMES + scope_success + scope_exit + scope_fail + unique_resource + unique_resource_2 +) # module tests will only compile with gcc15 or clang20 and above if(CMAKE_CXX_SCAN_FOR_MODULES) diff --git a/tests/unique_resource_2.test.cpp b/tests/unique_resource_2.test.cpp new file mode 100644 index 0000000..a564286 --- /dev/null +++ b/tests/unique_resource_2.test.cpp @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include "beman/scope/scope.hpp" + +#include +#include + +namespace { + +struct Counter { + int value = 0; +}; + +struct CountingDeleter { + // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes) + Counter* counter{nullptr}; + + void operator()(int& /*unused*/) const noexcept { ++counter->value; } +}; + +} // namespace + +TEST_CASE("Construct file unique_resource", "[unique_resource]") { + bool open_file_good = false; + bool close_file_good = false; + + { + auto file = beman::scope::unique_resource(fopen("example.txt", "w"), // Acquire the FILE* + [&close_file_good](FILE* f) -> void { + if (f) { + (void)fclose(f); // Release (cleanup) the resource + close_file_good = true; + } + }); + + if (!file) { + throw std::runtime_error("file didn't open"); + } + open_file_good = true; + } + + REQUIRE(open_file_good == true); + REQUIRE(close_file_good == true); +} + +TEST_CASE("unique_resource basic construction and engagement", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + { + beman::scope::unique_resource r(42, CountingDeleter{&c}); + + REQUIRE(static_cast(r)); + REQUIRE(r.get() == 42); + REQUIRE(c.value == 0); + } + + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource release disengages without deleting", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + { + beman::scope::unique_resource r(7, CountingDeleter{&c}); + + r.release(); + + REQUIRE_FALSE(r); + } + + REQUIRE(c.value == 0); +} + +TEST_CASE("unique_resource reset() destroys current resource", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + beman::scope::unique_resource r(1, CountingDeleter{&c}); + + r.reset(); + REQUIRE_FALSE(r); + REQUIRE(c.value == 1); + } + + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource reset(new_resource) replaces resource", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + beman::scope::unique_resource r(1, CountingDeleter{&c}); + + r.reset(2); + + REQUIRE(r); + REQUIRE(r.get() == 2); + REQUIRE(c.value == 1); + } + + REQUIRE(c.value == 2); +} + +TEST_CASE("unique_resource move constructor transfers ownership", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + beman::scope::unique_resource r1(10, CountingDeleter{&c}); + beman::scope::unique_resource r2(std::move(r1)); + + REQUIRE_FALSE(r1); + REQUIRE(r2); + REQUIRE(r2.get() == 10); + + r2.reset(); + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource move assignment destroys target before transfer", "[unique_resource]") { + Counter c1{}; // NOLINT(misc-const-correctness) + Counter c2{}; // NOLINT(misc-const-correctness) + + beman::scope::unique_resource r1(1, CountingDeleter{&c1}); + beman::scope::unique_resource r2(2, CountingDeleter{&c2}); + + r2 = std::move(r1); + + REQUIRE_FALSE(r1); + REQUIRE(r2); + REQUIRE(r2.get() == 1); + + REQUIRE(c2.value == 1); // old r2 destroyed + REQUIRE(c1.value == 0); + + r2.reset(); + REQUIRE(c1.value == 1); +} + +TEST_CASE("unique_resource destructor is idempotent after release", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + beman::scope::unique_resource r(99, CountingDeleter{&c}); + + r.release(); + } + + REQUIRE(c.value == 0); +} + +TEST_CASE("make_unique_resource_checked disengages on invalid", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + auto r = beman::scope::make_unique_resource_checked(-1, -1, CountingDeleter{&c}); + + REQUIRE_FALSE(r); + } + + REQUIRE(c.value == 0); +} + +TEST_CASE("make_unique_resource_checked engages on valid", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + auto r = beman::scope::make_unique_resource_checked(3, -1, CountingDeleter{&c}); + + REQUIRE(r); + } + + REQUIRE(c.value == 1); +} + +TEST_CASE("Open a nonexisting file with make_unique_resource_checked", "[unique_resource]") { + bool open_file_good = false; + bool close_file_good = false; + + { + auto file = + beman::scope::make_unique_resource_checked(fopen("nonexisting.txt", "r"), // Acquire the FILE* + nullptr, + [&close_file_good](FILE* f) -> void { + if (f) { + (void)fclose(f); // Release (cleanup) the resource + close_file_good = true; + } + }); + + if (file.get() != nullptr) { + open_file_good = true; + } + } + + REQUIRE(open_file_good == false); + REQUIRE(close_file_good == false); +} + +TEST_CASE("unique_resource supports deduction guide", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + beman::scope::unique_resource r(123, CountingDeleter{&c}); + + static_assert(std::is_same_v >); + + r.reset(); + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource does not double-delete after move", "[unique_resource]") { + Counter c{}; // NOLINT(misc-const-correctness) + + { + beman::scope::unique_resource r1(1, CountingDeleter{&c}); + + { + auto r2 = std::move(r1); + } + + REQUIRE(c.value == 1); + } + + REQUIRE(c.value == 1); +} + +TEST_CASE("unique_resource operator* returns reference to resource", "[unique_resource]") { + int value = 42; + + // Define the deleter type explicitly (function pointer) + using DeleterType = void (*)(int*); + + // Empty deleter + auto empty_deleter = [](int*) {}; + + // Create unique_resource instance (modifiable) + beman::scope::unique_resource r(&value, empty_deleter); + + // operator* should return a reference + int& ref = *r; + + // Check that the reference refers to the original value + REQUIRE(&ref == &value); + REQUIRE(ref == 42); + + // Modify the value through the reference + ref = 100; + REQUIRE(value == 100); + + // Create a const unique_resource instance + const beman::scope::unique_resource r2(&value, empty_deleter); + + // operator* should return const reference + const int& cref = *r2; + REQUIRE(cref == 100); + + // Modifying through cref would fail to compile (correct) +} From 4a9894da3f221e5bfd0b661c4eff1ac77de02bb2 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Tue, 13 Jan 2026 21:36:32 +0100 Subject: [PATCH 8/9] Prevent use of explicit operator bool() const noexcept --- tests/unique_resource_2.test.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/unique_resource_2.test.cpp b/tests/unique_resource_2.test.cpp index a564286..da1d136 100644 --- a/tests/unique_resource_2.test.cpp +++ b/tests/unique_resource_2.test.cpp @@ -33,7 +33,7 @@ TEST_CASE("Construct file unique_resource", "[unique_resource]") { } }); - if (!file) { + if (file.get() == nullptr) { throw std::runtime_error("file didn't open"); } open_file_good = true; @@ -48,7 +48,7 @@ TEST_CASE("unique_resource basic construction and engagement", "[unique_resource { beman::scope::unique_resource r(42, CountingDeleter{&c}); - REQUIRE(static_cast(r)); + // XXX REQUIRE(static_cast(r)); REQUIRE(r.get() == 42); REQUIRE(c.value == 0); } @@ -63,7 +63,7 @@ TEST_CASE("unique_resource release disengages without deleting", "[unique_resour r.release(); - REQUIRE_FALSE(r); + // XXX REQUIRE_FALSE(r); } REQUIRE(c.value == 0); @@ -76,7 +76,7 @@ TEST_CASE("unique_resource reset() destroys current resource", "[unique_resource beman::scope::unique_resource r(1, CountingDeleter{&c}); r.reset(); - REQUIRE_FALSE(r); + // XXX REQUIRE_FALSE(r); REQUIRE(c.value == 1); } @@ -91,7 +91,7 @@ TEST_CASE("unique_resource reset(new_resource) replaces resource", "[unique_reso r.reset(2); - REQUIRE(r); + // XXX REQUIRE(r); REQUIRE(r.get() == 2); REQUIRE(c.value == 1); } @@ -105,8 +105,8 @@ TEST_CASE("unique_resource move constructor transfers ownership", "[unique_resou beman::scope::unique_resource r1(10, CountingDeleter{&c}); beman::scope::unique_resource r2(std::move(r1)); - REQUIRE_FALSE(r1); - REQUIRE(r2); + // XXX REQUIRE_FALSE(r1); + // XXX REQUIRE(r2); REQUIRE(r2.get() == 10); r2.reset(); @@ -122,8 +122,8 @@ TEST_CASE("unique_resource move assignment destroys target before transfer", "[u r2 = std::move(r1); - REQUIRE_FALSE(r1); - REQUIRE(r2); + // XXX REQUIRE_FALSE(r1); + // XXX REQUIRE(r2); REQUIRE(r2.get() == 1); REQUIRE(c2.value == 1); // old r2 destroyed @@ -144,14 +144,14 @@ TEST_CASE("unique_resource destructor is idempotent after release", "[unique_res REQUIRE(c.value == 0); } - +#ifdef BEMAN_SCOPE_USE_FALLBACK TEST_CASE("make_unique_resource_checked disengages on invalid", "[unique_resource]") { Counter c{}; // NOLINT(misc-const-correctness) { auto r = beman::scope::make_unique_resource_checked(-1, -1, CountingDeleter{&c}); - REQUIRE_FALSE(r); + // XXX REQUIRE_FALSE(r); } REQUIRE(c.value == 0); @@ -163,7 +163,7 @@ TEST_CASE("make_unique_resource_checked engages on valid", "[unique_resource]") { auto r = beman::scope::make_unique_resource_checked(3, -1, CountingDeleter{&c}); - REQUIRE(r); + // XXX REQUIRE(r); } REQUIRE(c.value == 1); @@ -192,6 +192,7 @@ TEST_CASE("Open a nonexisting file with make_unique_resource_checked", "[unique_ REQUIRE(open_file_good == false); REQUIRE(close_file_good == false); } +#endif TEST_CASE("unique_resource supports deduction guide", "[unique_resource]") { Counter c{}; // NOLINT(misc-const-correctness) From 7d92b09e87061f169b51a4ab21699c4d43398d50 Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Wed, 14 Jan 2026 06:37:23 +0100 Subject: [PATCH 9/9] Test appleclang on CI too --- .github/workflows/cmake.yml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 09db9ed..de7a8ab 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -27,7 +27,18 @@ jobs: fail-fast: false matrix: - os: [windows] + os: [windows, macos] + include: + - { os: macos, uname: appleclang } + # - { os: ubuntu, uname: gcc } + - { os: ubuntu, uname: llvm } + - { os: windows, uname: msvc } + + # TODO(CK): + # type: [shared, static] + # include: + # - { type: shared, shared: YES } + # - { type: static, shared: NO } runs-on: ${{ matrix.os }}-latest @@ -49,19 +60,20 @@ jobs: if: matrix.os != 'windows' uses: aminya/setup-cpp@v1 with: + # compiler: ${{matrix.uname}} compiler: llvm - name: Configure CMake - run: cmake --preset msvc-${{env.BUILD_TYPE}} --log-level=VERBOSE + run: cmake --preset ${{matrix.uname}}-${{env.BUILD_TYPE}} --log-level=VERBOSE - name: Build # Build your program with the given configuration - run: cmake --build --preset msvc-${{env.BUILD_TYPE}} + run: cmake --build --preset ${{matrix.uname}}-${{env.BUILD_TYPE}} - name: Test # Execute tests defined by the CMake configuration - run: ctest --preset msvc-${{env.BUILD_TYPE}} + run: ctest --preset ${{matrix.uname}}-${{env.BUILD_TYPE}} # - name: Install # # Install the project artefacts to CMAKE_INSTALL_PREFIX - # run: cmake --build --preset msvc-${{env.BUILD_TYPE}} --target install + # run: cmake --build --preset ${{matrix.uname}}-${{env.BUILD_TYPE}} --target install