diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 0000000..5b5bf9d --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,146 @@ +name: Sanitizers + +on: + workflow_dispatch: + push: + branches: + - main + - ci/** + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + test-on-msvc: + strategy: + fail-fast: false + + matrix: + build-type: [Debug, Release] + include: + - build-type: Debug + cmake-preset: conan-debug + - build-type: Release + cmake-preset: conan-release + + name: MSVC v143 - ASan - ${{ matrix.build-type }} + + runs-on: windows-2022 + + steps: + - uses: actions/checkout@v7 + + - name: Setup MSVC developer environment + uses: TheMrMilchmann/setup-msvc-dev@v4 + with: + arch: x64 + + - name: Install Conan + uses: turtlebrowser/get-conan@main + + - name: Setup Conan + run: | + conan --version + conan profile detect + + - name: Install CMake + uses: lukka/get-cmake@latest + + - name: Install dependencies + run: > + conan install . + -c tools.microsoft.msbuild:vs_version=17 + -s compiler.version=194 + -s compiler.cppstd=23 + -s build_type=${{ matrix.build-type }} + -b missing + + - name: CMake Configure + run: > + cmake --preset conan-default + -DZEUS_EXPECTED_TEST_ASAN_REQUIRED=ON + + - name: Build + run: cmake --build --preset ${{ matrix.cmake-preset }} + + - name: Test + run: ctest --preset ${{ matrix.cmake-preset }} --output-on-failure + + test-on-linux: + strategy: + fail-fast: false + + matrix: + compiler: + - name: GCC 15 + id: gcc + version: 15 + cc: gcc-15 + cxx: g++-15 + - name: Clang 22 + id: clang + version: 22 + cc: clang-22 + cxx: clang++-22 + build-type: [Debug, Release] + include: + - build-type: Debug + cmake-preset: conan-debug + - build-type: Release + cmake-preset: conan-release + + name: ${{ matrix.compiler.name }} - ASan + UBSan - ${{ matrix.build-type }} + + runs-on: ubuntu-24.04 + + env: + CC: ${{ matrix.compiler.cc }} + CXX: ${{ matrix.compiler.cxx }} + + steps: + - uses: actions/checkout@v7 + + - name: Setup GCC + if: matrix.compiler.id == 'gcc' + uses: egor-tensin/setup-gcc@v2 + with: + version: ${{ matrix.compiler.version }} + + - name: Setup Clang + if: matrix.compiler.id == 'clang' + uses: egor-tensin/setup-clang@v2 + with: + version: ${{ matrix.compiler.version }} + + - name: Install Conan + uses: turtlebrowser/get-conan@main + + - name: Setup Conan + run: | + conan --version + conan profile detect --force + + - name: Install CMake + uses: lukka/get-cmake@latest + + - name: Install dependencies + run: > + conan install . + -s compiler.cppstd=23 + -s build_type=${{ matrix.build-type }} + -b missing + + - name: CMake Configure + run: > + cmake --preset ${{ matrix.cmake-preset }} + -DZEUS_EXPECTED_TEST_ASAN_REQUIRED=ON + -DZEUS_EXPECTED_TEST_UBSAN_REQUIRED=ON + + - name: Build + run: cmake --build --preset ${{ matrix.cmake-preset }} + + - name: Test + run: ctest --preset ${{ matrix.cmake-preset }} --output-on-failure diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92bf7b9..2d4324a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: runs-on: windows-2022 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Install Conan uses: turtlebrowser/get-conan@main @@ -62,7 +62,7 @@ jobs: - name: CMake Configure run: | cmake --list-presets - cmake --preset conan-default + cmake --preset conan-default -DZEUS_EXPECTED_TEST_ASAN_AUTO=OFF -DZEUS_EXPECTED_TEST_UBSAN_AUTO=OFF - name: Build run: | @@ -99,7 +99,7 @@ jobs: runs-on: ${{ matrix.gcc.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup GCC if: matrix.gcc.version == 15 @@ -134,7 +134,7 @@ jobs: - name: CMake Configure run: | cmake --list-presets - cmake --preset ${{ matrix.cmake-preset }} + cmake --preset ${{ matrix.cmake-preset }} -DZEUS_EXPECTED_TEST_ASAN_AUTO=OFF -DZEUS_EXPECTED_TEST_UBSAN_AUTO=OFF - name: Build run: | @@ -185,7 +185,7 @@ jobs: python3-pip \ unzip - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Check GCC Version run: | @@ -214,7 +214,7 @@ jobs: - name: CMake Configure run: | cmake --list-presets - cmake --preset ${{ matrix.cmake-preset }} + cmake --preset ${{ matrix.cmake-preset }} -DZEUS_EXPECTED_TEST_ASAN_AUTO=OFF -DZEUS_EXPECTED_TEST_UBSAN_AUTO=OFF - name: Build run: | @@ -252,7 +252,7 @@ jobs: CXX: clang++-${{ matrix.clang-version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Install Clang uses: egor-tensin/setup-clang@v2 @@ -294,7 +294,7 @@ jobs: - name: CMake Configure run: | cmake --list-presets - cmake --preset ${{ matrix.cmake-preset }} + cmake --preset ${{ matrix.cmake-preset }} -DZEUS_EXPECTED_TEST_ASAN_AUTO=OFF -DZEUS_EXPECTED_TEST_UBSAN_AUTO=OFF - name: Build run: | @@ -326,7 +326,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup GCC uses: egor-tensin/setup-gcc@v1 @@ -360,7 +360,7 @@ jobs: - name: CMake Configure run: | cmake --list-presets - cmake --preset ${{ matrix.cmake-preset }} + cmake --preset ${{ matrix.cmake-preset }} -DZEUS_EXPECTED_TEST_ASAN_AUTO=OFF -DZEUS_EXPECTED_TEST_UBSAN_AUTO=OFF - name: Build run: | diff --git a/README.md b/README.md index 3547086..375c414 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![GitHub Release](https://img.shields.io/github/v/release/zeus-cpp/expected?color=green)](https://github.com/zeus-cpp/expected/releases) [![Conan Center](https://img.shields.io/conan/v/zeus_expected)](https://conan.io/center/recipes/zeus_expected) -Backporting `std::expected` to C++17. +A C++17 backport of `std::expected`, designed for API compatibility with the standard library. ## Features @@ -32,18 +32,17 @@ Enhancements: + Enhanced noexcept (covered by tests from MSVC's STL) -## Compiler supports +## Compiler support -Any compiler that supports C++17 should work. +A conforming C++17 implementation is required. Higher language standards are also supported and can provide benefits such as enhanced constexpr capabilities. -Higher language standards are also supported, which can provide benefits such as enhanced constexpr capabilities. +| Compiler | Supported versions | +| --- | --- | +| MSVC | v142 and later | +| GCC | 8 and later | +| Clang | 11 and later | -List of known compiler supported: - -+ MSVC v142 and later -+ GCC 8 and later - -Feedbacks are welcome. +See the [CI workflow](.github/workflows/tests.yml) for the compiler and language standard combinations that are continuously tested. Other conforming compilers may also work. Feedback is welcome. ## Building and testing diff --git a/include/zeus/expected.hpp b/include/zeus/expected.hpp index 39d24b5..19ba858 100644 --- a/include/zeus/expected.hpp +++ b/include/zeus/expected.hpp @@ -371,11 +371,66 @@ using enable_from_other_void_expected_t = std::enable_if_t< namespace expected_detail { -struct no_init_t +// Constructing the active alternative inside storage_base ensures its +// destructor only ever observes a fully initialized value or error. +struct construct_from_expected_t { - explicit no_init_t() = default; + explicit construct_from_expected_t() = default; }; -inline constexpr no_init_t no_init {}; +inline constexpr construct_from_expected_t construct_from_expected {}; + +template +constexpr bool expected_source_has_value(const Rhs &rhs) noexcept +{ + if constexpr (expected_detail::is_specialization_v, expected>) + { + return rhs.has_value(); + } + else + { + return rhs.m_has_val; + } +} + +template +constexpr decltype(auto) expected_source_value(Rhs &&rhs) noexcept +{ + if constexpr (expected_detail::is_specialization_v, expected>) + { + return *std::forward(rhs); + } + else + { + if constexpr (std::is_lvalue_reference_v) + { + return (rhs.m_val); + } + else + { + return std::move(rhs.m_val); + } + } +} + +template +constexpr decltype(auto) expected_source_error(Rhs &&rhs) noexcept +{ + if constexpr (expected_detail::is_specialization_v, expected>) + { + return std::forward(rhs).error(); + } + else + { + if constexpr (std::is_lvalue_reference_v) + { + return (rhs.m_unexpect); + } + else + { + return std::move(rhs.m_unexpect); + } + } +} struct construct_with_invoke_result_t { @@ -458,10 +513,23 @@ struct storage_base , m_has_val(true) { } - constexpr storage_base(no_init_t) noexcept - : m_no_init() - , m_has_val(false) + + template + constexpr storage_base(construct_from_expected_t, Rhs &&rhs) noexcept( + std::is_nothrow_constructible_v()))> && + std::is_nothrow_constructible_v()))> + ) + : m_dummy() + , m_has_val(expected_detail::expected_source_has_value(rhs)) { + if (m_has_val) + { + expected_detail::construct_at(std::addressof(m_val), expected_detail::expected_source_value(std::forward(rhs))); + } + else + { + expected_detail::construct_at(std::addressof(m_unexpect), expected_detail::expected_source_error(std::forward(rhs))); + } } template> * = nullptr> @@ -522,7 +590,7 @@ struct storage_base { T m_val; E m_unexpect; - char m_no_init; + char m_dummy; }; bool m_has_val; }; @@ -540,10 +608,23 @@ struct storage_base , m_has_val(true) { } - constexpr storage_base(no_init_t) noexcept - : m_no_init() - , m_has_val(false) + + template + constexpr storage_base(construct_from_expected_t, Rhs &&rhs) noexcept( + std::is_nothrow_constructible_v()))> && + std::is_nothrow_constructible_v()))> + ) + : m_dummy() + , m_has_val(expected_detail::expected_source_has_value(rhs)) { + if (m_has_val) + { + expected_detail::construct_at(std::addressof(m_val), expected_detail::expected_source_value(std::forward(rhs))); + } + else + { + expected_detail::construct_at(std::addressof(m_unexpect), expected_detail::expected_source_error(std::forward(rhs))); + } } template> * = nullptr> @@ -598,7 +679,9 @@ struct storage_base { } - ZEUS_EXPECTED_CONSTEXPR_DTOR ~storage_base() noexcept + // Keep the exception specification implicit; an explicit noexcept trips + // Clang's LLVM-59854 during constexpr destruction. + ZEUS_EXPECTED_CONSTEXPR_DTOR ~storage_base() { if (m_has_val) { @@ -620,7 +703,7 @@ struct storage_base { T m_val; E m_unexpect; - char m_no_init; + char m_dummy; }; bool m_has_val; }; @@ -639,10 +722,17 @@ struct storage_base { } - constexpr storage_base(no_init_t) noexcept + template + constexpr storage_base( + construct_from_expected_t, Rhs &&rhs + ) noexcept(std::is_nothrow_constructible_v()))>) : m_val() - , m_has_val(false) + , m_has_val(expected_detail::expected_source_has_value(rhs)) { + if (!m_has_val) + { + expected_detail::construct_at(std::addressof(m_unexpect), expected_detail::expected_source_error(std::forward(rhs))); + } } constexpr explicit storage_base(std::in_place_t) noexcept @@ -703,10 +793,17 @@ struct storage_base { } - constexpr storage_base(no_init_t) noexcept + template + constexpr storage_base( + construct_from_expected_t, Rhs &&rhs + ) noexcept(std::is_nothrow_constructible_v()))>) : m_val() - , m_has_val(false) + , m_has_val(expected_detail::expected_source_has_value(rhs)) { + if (!m_has_val) + { + expected_detail::construct_at(std::addressof(m_unexpect), expected_detail::expected_source_error(std::forward(rhs))); + } } constexpr explicit storage_base(std::in_place_t) noexcept @@ -738,7 +835,9 @@ struct storage_base { } - ZEUS_EXPECTED_CONSTEXPR_DTOR ~storage_base() noexcept + // Keep the exception specification implicit; an explicit noexcept trips + // Clang's LLVM-59854 during constexpr destruction. + ZEUS_EXPECTED_CONSTEXPR_DTOR ~storage_base() { if (!m_has_val) { @@ -766,27 +865,6 @@ struct operations_base : storage_base { using storage_base::storage_base; - template - constexpr void construct(Args &&...args) noexcept(std::is_nothrow_constructible_v) - { - expected_detail::construct_at(&this->m_val, std::forward(args)...); - this->m_has_val = true; - } - - template - constexpr void construct_with(Rhs &&rhs) noexcept(std::is_nothrow_constructible_v) - { - expected_detail::construct_at(&this->m_val, std::forward(rhs).get()); - this->m_has_val = true; - } - - template - constexpr void construct_error(Args &&...args) noexcept(std::is_nothrow_constructible_v) - { - expected_detail::construct_at(&this->m_unexpect, std::forward(args)...); - this->m_has_val = false; - } - constexpr T &get() &noexcept { return this->m_val; } constexpr const T &get() const &noexcept { return this->m_val; } constexpr T &&get() &&noexcept(std::is_nothrow_move_constructible_v) { return std::move(this->m_val); } @@ -805,23 +883,6 @@ struct operations_base : storage_base { using storage_base::storage_base; - constexpr void construct() noexcept { this->m_has_val = true; } - - // This function doesn't use its argument, but needs it so that code in - // levels above this can work independently of whether T is void - template - constexpr void construct_with(Rhs &&) noexcept - { - this->m_has_val = true; - } - - template - constexpr void construct_error(Args &&...args) noexcept(std::is_nothrow_constructible_v) - { - expected_detail::construct_at(&this->m_unexpect, std::forward(args)...); - this->m_has_val = false; - } - constexpr E &geterr() &noexcept { return this->m_unexpect; } constexpr const E &geterr() const &noexcept { return this->m_unexpect; } constexpr E &&geterr() &&noexcept(std::is_nothrow_move_constructible_v) { return std::move(this->m_unexpect); } @@ -867,16 +928,8 @@ struct copy_ctor_base : operations_base constexpr copy_ctor_base( const copy_ctor_base &rhs ) noexcept(is_nothrow_copy_constructible_or_void_v && std::is_nothrow_copy_constructible_v) - : operations_base(no_init) + : operations_base(construct_from_expected, rhs) { - if (rhs.m_has_val) - { - this->construct_with(rhs); - } - else - { - this->construct_error(rhs.geterr()); - } } copy_ctor_base(copy_ctor_base &&rhs) = default; @@ -923,16 +976,8 @@ struct move_ctor_base : copy_ctor_base constexpr move_ctor_base( move_ctor_base &&rhs ) noexcept(is_nothrow_move_constructible_or_void_v && std::is_nothrow_move_constructible_v) - : copy_ctor_base(no_init) + : copy_ctor_base(construct_from_expected, std::move(rhs)) { - if (rhs.m_has_val) - { - this->construct_with(std::move(rhs)); - } - else - { - this->construct_error(std::move(rhs.geterr())); - } } move_ctor_base &operator=(const move_ctor_base &rhs) = default; @@ -1286,16 +1331,9 @@ class expected constexpr expected( const expected &rhs ) noexcept(std::is_nothrow_constructible_v && std::is_nothrow_constructible_v) - : ctor_base(expected_detail::default_constructor_tag {}) + : impl_base(expected_detail::construct_from_expected, rhs) + , ctor_base(expected_detail::default_constructor_tag {}) { - if (rhs.has_value()) - { - this->construct(*rhs); - } - else - { - this->construct_error(rhs.error()); - } } // explicit const reference @@ -1309,16 +1347,9 @@ class expected constexpr explicit expected( const expected &rhs ) noexcept(std::is_nothrow_constructible_v && std::is_nothrow_constructible_v) - : ctor_base(expected_detail::default_constructor_tag {}) + : impl_base(expected_detail::construct_from_expected, rhs) + , ctor_base(expected_detail::default_constructor_tag {}) { - if (rhs.has_value()) - { - this->construct(*rhs); - } - else - { - this->construct_error(rhs.error()); - } } // implicit rvalue @@ -1330,16 +1361,9 @@ class expected expected_detail::enable_from_other_expected_t * = nullptr > constexpr expected(expected &&rhs) noexcept(std::is_nothrow_constructible_v && std::is_nothrow_constructible_v) - : ctor_base(expected_detail::default_constructor_tag {}) + : impl_base(expected_detail::construct_from_expected, std::move(rhs)) + , ctor_base(expected_detail::default_constructor_tag {}) { - if (rhs.has_value()) - { - this->construct(std::move(*rhs)); - } - else - { - this->construct_error(std::move(rhs.error())); - } } // explicit rvalue @@ -1353,16 +1377,9 @@ class expected constexpr explicit expected( expected &&rhs ) noexcept(std::is_nothrow_constructible_v && std::is_nothrow_constructible_v) - : ctor_base(expected_detail::default_constructor_tag {}) + : impl_base(expected_detail::construct_from_expected, std::move(rhs)) + , ctor_base(expected_detail::default_constructor_tag {}) { - if (rhs.has_value()) - { - this->construct(std::move(*rhs)); - } - else - { - this->construct_error(std::move(rhs.error())); - } } // template @@ -2222,16 +2239,9 @@ class expected expected_detail::enable_from_other_void_expected_t * = nullptr > constexpr expected(const expected &rhs) noexcept(std::is_nothrow_constructible_v) - : ctor_base(expected_detail::default_constructor_tag {}) + : impl_base(expected_detail::construct_from_expected, rhs) + , ctor_base(expected_detail::default_constructor_tag {}) { - if (rhs.has_value()) - { - this->construct(); - } - else - { - this->construct_error(rhs.error()); - } } template< @@ -2242,16 +2252,9 @@ class expected expected_detail::enable_from_other_void_expected_t * = nullptr > constexpr explicit expected(const expected &rhs) noexcept(std::is_nothrow_constructible_v) - : ctor_base(expected_detail::default_constructor_tag {}) + : impl_base(expected_detail::construct_from_expected, rhs) + , ctor_base(expected_detail::default_constructor_tag {}) { - if (rhs.has_value()) - { - this->construct(); - } - else - { - this->construct_error(rhs.error()); - } } template< @@ -2262,16 +2265,9 @@ class expected expected_detail::enable_from_other_void_expected_t * = nullptr > constexpr expected(expected &&rhs) noexcept(std::is_nothrow_constructible_v) - : ctor_base(expected_detail::default_constructor_tag {}) + : impl_base(expected_detail::construct_from_expected, std::move(rhs)) + , ctor_base(expected_detail::default_constructor_tag {}) { - if (rhs.has_value()) - { - this->construct(); - } - else - { - this->construct_error(std::move(rhs.error())); - } } template< @@ -2282,16 +2278,9 @@ class expected expected_detail::enable_from_other_void_expected_t * = nullptr > constexpr explicit expected(expected &&rhs) noexcept(std::is_nothrow_constructible_v) - : ctor_base(expected_detail::default_constructor_tag {}) + : impl_base(expected_detail::construct_from_expected, std::move(rhs)) + , ctor_base(expected_detail::default_constructor_tag {}) { - if (rhs.has_value()) - { - this->construct(); - } - else - { - this->construct_error(std::move(rhs.error())); - } } // constructors for unexpected diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 898c030..e87fcb3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,7 @@ +include("${CMAKE_CURRENT_LIST_DIR}/cmake/ZeusExpectedTestSanitizers.cmake") + +zeus_expected_enable_test_sanitizers() + add_subdirectory(test_expected) add_subdirectory(test_no_exceptions) add_subdirectory(third_party) diff --git a/tests/cmake/ZeusExpectedTestSanitizers.cmake b/tests/cmake/ZeusExpectedTestSanitizers.cmake new file mode 100644 index 0000000..c1cdc15 --- /dev/null +++ b/tests/cmake/ZeusExpectedTestSanitizers.cmake @@ -0,0 +1,263 @@ +include_guard(GLOBAL) + +include(CheckCXXSourceRuns) + +option(ZEUS_EXPECTED_TEST_ASAN_AUTO + "Automatically enable AddressSanitizer for tests when supported" + ON +) +option(ZEUS_EXPECTED_TEST_ASAN_REQUIRED + "Require AddressSanitizer support for tests" + OFF +) +option(ZEUS_EXPECTED_TEST_UBSAN_AUTO + "Automatically enable UndefinedBehaviorSanitizer for tests when supported" + ON +) +option(ZEUS_EXPECTED_TEST_UBSAN_REQUIRED + "Require UndefinedBehaviorSanitizer support for tests" + OFF +) +# REQUIRED takes precedence over AUTO for each sanitizer. With both options +# disabled, that sanitizer is not probed or enabled. + +function(_zeus_expected_check_asan_support RESULT_VARIABLE) + unset(_ZEUS_EXPECTED_TEST_ASAN_SUPPORTED CACHE) + if (CMAKE_CROSSCOMPILING AND NOT CMAKE_CROSSCOMPILING_EMULATOR) + set(${RESULT_VARIABLE} FALSE PARENT_SCOPE) + return() + endif () + + string(JOIN " " _required_flags + ${_zeus_expected_test_asan_compile_options} + ) + string(APPEND CMAKE_REQUIRED_FLAGS " ${_required_flags}") + list(APPEND CMAKE_REQUIRED_LINK_OPTIONS + ${_zeus_expected_test_asan_link_options} + ) + + check_cxx_source_runs([=[ + extern "C" const char* __asan_default_options() + { + return "detect_leaks=0"; + } + + int main() + { + return 0; + } + ]=] + _ZEUS_EXPECTED_TEST_ASAN_SUPPORTED + ) + set(${RESULT_VARIABLE} + "${_ZEUS_EXPECTED_TEST_ASAN_SUPPORTED}" + PARENT_SCOPE + ) +endfunction() + +function(_zeus_expected_enable_asan RESULT_VARIABLE) + set(${RESULT_VARIABLE} "" PARENT_SCOPE) + + if (NOT (ZEUS_EXPECTED_TEST_ASAN_AUTO + OR ZEUS_EXPECTED_TEST_ASAN_REQUIRED)) + message(STATUS "AddressSanitizer disabled for tests") + return() + endif () + + if (MSVC) + set(_zeus_expected_test_asan_compile_options /fsanitize=address) + set(_zeus_expected_test_asan_link_options) + else () + set(_zeus_expected_test_asan_compile_options + -fsanitize=address + -fno-omit-frame-pointer + ) + set(_zeus_expected_test_asan_link_options -fsanitize=address) + endif () + + _zeus_expected_check_asan_support( + ZEUS_EXPECTED_TEST_ASAN_SUPPORTED + ) + + if (ZEUS_EXPECTED_TEST_ASAN_SUPPORTED) + add_compile_options(${_zeus_expected_test_asan_compile_options}) + if (_zeus_expected_test_asan_link_options) + add_link_options(${_zeus_expected_test_asan_link_options}) + endif () + + if (WIN32) + set(_environment "ASAN_OPTIONS=halt_on_error=1") + else () + add_library(zeus_expected_asan_defaults OBJECT + "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/asan_default_options.cpp" + ) + link_libraries(zeus_expected_asan_defaults) + + set(_environment + "ASAN_OPTIONS=detect_leaks=1:halt_on_error=1" + ) + endif () + set(${RESULT_VARIABLE} "${_environment}" PARENT_SCOPE) + message(STATUS "AddressSanitizer enabled for tests") + elseif (ZEUS_EXPECTED_TEST_ASAN_REQUIRED) + message(FATAL_ERROR + "ZEUS_EXPECTED_TEST_ASAN_REQUIRED=ON, but the current compiler, " + "linker, or runtime environment cannot run " + "AddressSanitizer-instrumented test executables" + ) + else () + message(STATUS + "AddressSanitizer unavailable; tests will run without it" + ) + endif () +endfunction() + +function(_zeus_expected_check_ubsan_support RESULT_VARIABLE) + unset(_ZEUS_EXPECTED_TEST_UBSAN_SUPPORTED CACHE) + if (CMAKE_CROSSCOMPILING AND NOT CMAKE_CROSSCOMPILING_EMULATOR) + set(${RESULT_VARIABLE} FALSE PARENT_SCOPE) + return() + endif () + + string(JOIN " " _required_flags + ${_zeus_expected_test_ubsan_compile_options} + ) + string(APPEND CMAKE_REQUIRED_FLAGS " ${_required_flags}") + list(APPEND CMAKE_REQUIRED_LINK_OPTIONS + ${_zeus_expected_test_ubsan_link_options} + ) + + # Keep a safe division check in the executable so the linker must resolve + # the UBSan runtime, then run it to verify that the runtime is loadable. + check_cxx_source_runs([=[ + int main(int argc, char**) + { + volatile int divisor = argc; + const int result = 1 / divisor; + return result == 1 ? 0 : 1; + } + ]=] + _ZEUS_EXPECTED_TEST_UBSAN_SUPPORTED + ) + set(${RESULT_VARIABLE} + "${_ZEUS_EXPECTED_TEST_UBSAN_SUPPORTED}" + PARENT_SCOPE + ) +endfunction() + +function(_zeus_expected_enable_ubsan RESULT_VARIABLE) + set(${RESULT_VARIABLE} "" PARENT_SCOPE) + + if (NOT (ZEUS_EXPECTED_TEST_UBSAN_AUTO + OR ZEUS_EXPECTED_TEST_UBSAN_REQUIRED)) + message(STATUS "UndefinedBehaviorSanitizer disabled for tests") + return() + endif () + + if (CMAKE_CXX_COMPILER_ID MATCHES "^(AppleClang|Clang|GNU)$") + set(_zeus_expected_test_ubsan_compile_options + -fsanitize=undefined + -fno-sanitize-recover=undefined + -fno-omit-frame-pointer + ) + if (CMAKE_CXX_COMPILER_ID MATCHES "^(AppleClang|Clang)$") + list(APPEND _zeus_expected_test_ubsan_compile_options + -fno-sanitize-merge + ) + endif () + set(_zeus_expected_test_ubsan_link_options + -fsanitize=undefined + ) + + _zeus_expected_check_ubsan_support( + ZEUS_EXPECTED_TEST_UBSAN_SUPPORTED + ) + else () + set(ZEUS_EXPECTED_TEST_UBSAN_SUPPORTED FALSE) + endif () + + if (ZEUS_EXPECTED_TEST_UBSAN_SUPPORTED) + add_compile_options(${_zeus_expected_test_ubsan_compile_options}) + add_link_options(${_zeus_expected_test_ubsan_link_options}) + + set(${RESULT_VARIABLE} + "UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1" + PARENT_SCOPE + ) + message(STATUS "UndefinedBehaviorSanitizer enabled for tests") + elseif (ZEUS_EXPECTED_TEST_UBSAN_REQUIRED) + message(FATAL_ERROR + "ZEUS_EXPECTED_TEST_UBSAN_REQUIRED=ON, but the current compiler, " + "linker, or runtime environment cannot run " + "UndefinedBehaviorSanitizer-instrumented test executables" + ) + else () + message(STATUS + "UndefinedBehaviorSanitizer unavailable; tests will run without it" + ) + endif () +endfunction() + +function(zeus_expected_enable_test_sanitizers) + _zeus_expected_enable_asan(_asan_environment) + _zeus_expected_enable_ubsan(_ubsan_environment) + + set(_environment) + if (_asan_environment) + list(APPEND _environment "${_asan_environment}") + endif () + if (_ubsan_environment) + list(APPEND _environment "${_ubsan_environment}") + endif () + set(_ZEUS_EXPECTED_TEST_SANITIZER_ENVIRONMENT + "${_environment}" + PARENT_SCOPE + ) +endfunction() + +function(zeus_expected_configure_test_sanitizers TEST_NAME) + if (_ZEUS_EXPECTED_TEST_SANITIZER_ENVIRONMENT) + set_tests_properties("${TEST_NAME}" + PROPERTIES + ENVIRONMENT "${_ZEUS_EXPECTED_TEST_SANITIZER_ENVIRONMENT}" + ) + endif () +endfunction() + +function(zeus_expected_configure_catch_test_sanitizers TARGET_NAME) + if (NOT _ZEUS_EXPECTED_TEST_SANITIZER_ENVIRONMENT) + return() + endif () + + if (MSVC AND _ZEUS_EXPECTED_TEST_ASAN_SUPPORTED) + # Conan may provide Catch2 as a static library built without ASan. + # Match its MSVC STL annotation mode while keeping the test target + # itself instrumented with /fsanitize=address. + target_compile_definitions("${TARGET_NAME}" PRIVATE + _DISABLE_STRING_ANNOTATION + _DISABLE_VECTOR_ANNOTATION + ) + endif () + + # Catch2 exposes the discovered test list only while CTest processes + # TEST_INCLUDE_FILES. Set the environment there so its semicolon-separated + # entries are not flattened as Catch2 property arguments. + set(_sanitizer_script + "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}_sanitizer.cmake" + ) + string(CONCAT _sanitizer_script_content + "if(DEFINED ${TARGET_NAME}_TESTS)\n" + " set_tests_properties(\${${TARGET_NAME}_TESTS}\n" + " PROPERTIES ENVIRONMENT " + "[==[${_ZEUS_EXPECTED_TEST_SANITIZER_ENVIRONMENT}]==]\n" + " )\n" + "endif()\n" + ) + file(GENERATE + OUTPUT "${_sanitizer_script}" + CONTENT "${_sanitizer_script_content}" + ) + set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "${_sanitizer_script}" + ) +endfunction() diff --git a/tests/cmake/asan_default_options.cpp b/tests/cmake/asan_default_options.cpp new file mode 100644 index 0000000..c18f91a --- /dev/null +++ b/tests/cmake/asan_default_options.cpp @@ -0,0 +1,6 @@ +// Catch2 runs test executables during the build to discover test cases. Leak +// detection is enabled again by the ASAN_OPTIONS attached to the CTest tests. +extern "C" const char* __asan_default_options() +{ + return "detect_leaks=0"; +} diff --git a/tests/test_expected/CMakeLists.txt b/tests/test_expected/CMakeLists.txt index 3dc4efa..4270866 100644 --- a/tests/test_expected/CMakeLists.txt +++ b/tests/test_expected/CMakeLists.txt @@ -1,9 +1,13 @@ project(test_expected LANGUAGES CXX) set(SOURCES + assignment_exception_tests.cpp base_tests.cpp + converting_constructor_tests.cpp + constructor_exception_tests.cpp monadic_tests.cpp noexcept_tests.cpp + swap_exception_tests.cpp equality_tests.cpp equality_noexcept_tests.cpp p3379_tests.cpp @@ -33,6 +37,7 @@ function(add_test_expected CPP_STANDARD) target_sources(${TARGET_NAME} PRIVATE ${SOURCES}) catch_discover_tests(${TARGET_NAME}) + zeus_expected_configure_catch_test_sanitizers(${TARGET_NAME}) endfunction() add_test_expected(17) diff --git a/tests/test_expected/assignment_exception_tests.cpp b/tests/test_expected/assignment_exception_tests.cpp new file mode 100644 index 0000000..eec818a --- /dev/null +++ b/tests/test_expected/assignment_exception_tests.cpp @@ -0,0 +1,304 @@ +#include +#include + +#include + +#include + +namespace +{ + +struct ConstructionException +{ +}; + +template +class NothrowMoveState +{ +public: + explicit NothrowMoveState(int payload = 0) + : payload_(payload) + { + ++live_instances_; + } + + NothrowMoveState(const NothrowMoveState &other) noexcept + : payload_(other.payload_) + { + ++live_instances_; + } + + NothrowMoveState(NothrowMoveState &&other) noexcept + : payload_(std::exchange(other.payload_, -1)) + { + ++live_instances_; + } + + NothrowMoveState &operator=(const NothrowMoveState &other) noexcept + { + payload_ = other.payload_; + return *this; + } + + NothrowMoveState &operator=(NothrowMoveState &&other) noexcept + { + payload_ = std::exchange(other.payload_, -1); + return *this; + } + + ~NothrowMoveState() { --live_instances_; } + + int payload() const noexcept { return payload_; } + static int live_instances() noexcept { return live_instances_; } + +private: + inline static int live_instances_ = 0; + int payload_ = 0; +}; + +class ThrowOnCopy +{ +public: + explicit ThrowOnCopy(int payload = 0) + : payload_(payload) + { + } + + ThrowOnCopy(const ThrowOnCopy &) { throw ConstructionException {}; } + ThrowOnCopy(ThrowOnCopy &&) noexcept = default; + ThrowOnCopy &operator=(const ThrowOnCopy &) = default; + ThrowOnCopy &operator=(ThrowOnCopy &&) noexcept = default; + + int payload() const noexcept { return payload_; } + +private: + int payload_ = 0; +}; + +class ThrowOnMove +{ +public: + explicit ThrowOnMove(int payload = 0) + : payload_(payload) + { + } + + ThrowOnMove(const ThrowOnMove &) = delete; + ThrowOnMove &operator=(const ThrowOnMove &) = delete; + ThrowOnMove(ThrowOnMove &&) { throw ConstructionException {}; } + ThrowOnMove &operator=(ThrowOnMove &&) = default; + + int payload() const noexcept { return payload_; } + +private: + int payload_ = 0; +}; + +struct ValueSource +{ +}; + +class ThrowOnValueConversion +{ +public: + ThrowOnValueConversion(const ValueSource &) { throw ConstructionException {}; } + ThrowOnValueConversion(ThrowOnValueConversion &&) noexcept(false) {} + ThrowOnValueConversion &operator=(const ValueSource &) { return *this; } +}; + +struct ErrorSource +{ +}; + +class ThrowOnErrorConversion +{ +public: + ThrowOnErrorConversion(const ErrorSource &) { throw ConstructionException {}; } + ThrowOnErrorConversion(ThrowOnErrorConversion &&) noexcept(false) {} + ThrowOnErrorConversion &operator=(const ErrorSource &) { return *this; } +}; + +} // namespace + +TEST_CASE("copy assignment preserves an error when value construction throws", "[expected][assignment][exception-safety]") +{ + using Error = NothrowMoveState<0>; + using Expected = zeus::expected; + + STATIC_REQUIRE_FALSE(std::is_nothrow_copy_constructible_v); + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + + const int initial_live_errors = Error::live_instances(); + + { + Expected target(zeus::unexpect, 42); + const Expected source(std::in_place, 7); + + REQUIRE_THROWS_AS(target = source, ConstructionException); + REQUIRE_FALSE(target.has_value()); + REQUIRE(target.error().payload() == 42); + REQUIRE(source.has_value()); + REQUIRE(source->payload() == 7); + REQUIRE(Error::live_instances() == initial_live_errors + 1); + } + + REQUIRE(Error::live_instances() == initial_live_errors); +} + +TEST_CASE("copy assignment preserves a value when error construction throws", "[expected][assignment][exception-safety]") +{ + using Value = NothrowMoveState<1>; + using Expected = zeus::expected; + + STATIC_REQUIRE_FALSE(std::is_nothrow_copy_constructible_v); + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + + const int initial_live_values = Value::live_instances(); + + { + Expected target(std::in_place, 42); + const Expected source(zeus::unexpect, 7); + + REQUIRE_THROWS_AS(target = source, ConstructionException); + REQUIRE(target.has_value()); + REQUIRE(target->payload() == 42); + REQUIRE_FALSE(source.has_value()); + REQUIRE(source.error().payload() == 7); + REQUIRE(Value::live_instances() == initial_live_values + 1); + } + + REQUIRE(Value::live_instances() == initial_live_values); +} + +TEST_CASE("move assignment restores an error when value construction throws", "[expected][assignment][exception-safety]") +{ + using Error = NothrowMoveState<2>; + using Expected = zeus::expected; + + STATIC_REQUIRE_FALSE(std::is_nothrow_move_constructible_v); + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + + const int initial_live_errors = Error::live_instances(); + + { + Expected target(zeus::unexpect, 42); + Expected source(std::in_place, 7); + + REQUIRE_THROWS_AS(target = std::move(source), ConstructionException); + REQUIRE_FALSE(target.has_value()); + REQUIRE(target.error().payload() == 42); + REQUIRE(source.has_value()); + REQUIRE(source->payload() == 7); + REQUIRE(Error::live_instances() == initial_live_errors + 1); + } + + REQUIRE(Error::live_instances() == initial_live_errors); +} + +TEST_CASE("move assignment restores a value when error construction throws", "[expected][assignment][exception-safety]") +{ + using Value = NothrowMoveState<3>; + using Expected = zeus::expected; + + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + STATIC_REQUIRE_FALSE(std::is_nothrow_move_constructible_v); + + const int initial_live_values = Value::live_instances(); + + { + Expected target(std::in_place, 42); + Expected source(zeus::unexpect, 7); + + REQUIRE_THROWS_AS(target = std::move(source), ConstructionException); + REQUIRE(target.has_value()); + REQUIRE(target->payload() == 42); + REQUIRE_FALSE(source.has_value()); + REQUIRE(source.error().payload() == 7); + REQUIRE(Value::live_instances() == initial_live_values + 1); + } + + REQUIRE(Value::live_instances() == initial_live_values); +} + +TEST_CASE("value assignment restores an error when conversion throws", "[expected][assignment][exception-safety]") +{ + using Error = NothrowMoveState<4>; + using Expected = zeus::expected; + + STATIC_REQUIRE_FALSE(std::is_nothrow_constructible_v); + STATIC_REQUIRE_FALSE(std::is_nothrow_move_constructible_v); + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + + const int initial_live_errors = Error::live_instances(); + + { + Expected target(zeus::unexpect, 42); + const ValueSource source; + + REQUIRE_THROWS_AS(target = source, ConstructionException); + REQUIRE_FALSE(target.has_value()); + REQUIRE(target.error().payload() == 42); + REQUIRE(Error::live_instances() == initial_live_errors + 1); + } + + REQUIRE(Error::live_instances() == initial_live_errors); +} + +TEST_CASE("unexpected assignment restores a value when conversion throws", "[expected][assignment][exception-safety]") +{ + using Value = NothrowMoveState<5>; + using Expected = zeus::expected; + + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + STATIC_REQUIRE_FALSE(std::is_nothrow_constructible_v); + STATIC_REQUIRE_FALSE(std::is_nothrow_move_constructible_v); + + const int initial_live_values = Value::live_instances(); + + { + Expected target(std::in_place, 42); + const zeus::unexpected source(ErrorSource {}); + + REQUIRE_THROWS_AS(target = source, ConstructionException); + REQUIRE(target.has_value()); + REQUIRE(target->payload() == 42); + REQUIRE(Value::live_instances() == initial_live_values + 1); + } + + REQUIRE(Value::live_instances() == initial_live_values); +} + +TEST_CASE("copy assignment of a void expected retains a value when error construction throws", "[expected][assignment][exception-safety]") +{ + using Expected = zeus::expected; + + Expected target; + const Expected source(zeus::unexpect, 7); + + REQUIRE_THROWS_AS(target = source, ConstructionException); + REQUIRE(target.has_value()); + REQUIRE_FALSE(source.has_value()); + REQUIRE(source.error().payload() == 7); +} + +TEST_CASE("move assignment of a void expected retains a value when error construction throws", "[expected][assignment][exception-safety]") +{ + using Expected = zeus::expected; + + Expected target; + Expected source(zeus::unexpect, 7); + + REQUIRE_THROWS_AS(target = std::move(source), ConstructionException); + REQUIRE(target.has_value()); + REQUIRE_FALSE(source.has_value()); + REQUIRE(source.error().payload() == 7); +} + +TEST_CASE("unexpected assignment of a void expected retains a value when conversion throws", "[expected][assignment][exception-safety]") +{ + zeus::expected target; + const zeus::unexpected source(ErrorSource {}); + + REQUIRE_THROWS_AS(target = source, ConstructionException); + REQUIRE(target.has_value()); +} diff --git a/tests/test_expected/constructor_exception_tests.cpp b/tests/test_expected/constructor_exception_tests.cpp new file mode 100644 index 0000000..ae15537 --- /dev/null +++ b/tests/test_expected/constructor_exception_tests.cpp @@ -0,0 +1,424 @@ +#include + +#include + +#include + +namespace +{ + +struct ConstructorException +{ +}; + +struct SourceValue +{ +}; + +struct SourceError +{ +}; + +template +class ThrowingValueDestructorProbe +{ +public: + ThrowingValueDestructorProbe(const SourceValue &) { throw ConstructorException {}; } + + ~ThrowingValueDestructorProbe() { ++destructor_calls_; } + + static int destructor_calls() noexcept { return destructor_calls_; } + static void reset() noexcept { destructor_calls_ = 0; } + +private: + inline static int destructor_calls_ = 0; +}; + +template +class ErrorDestructorProbe +{ +public: + ErrorDestructorProbe() = default; + ErrorDestructorProbe(const SourceError &) {} + ErrorDestructorProbe(SourceError &&) {} + ErrorDestructorProbe(const ErrorDestructorProbe &) = default; + ErrorDestructorProbe(ErrorDestructorProbe &&) = default; + ErrorDestructorProbe &operator=(const ErrorDestructorProbe &) = default; + ErrorDestructorProbe &operator=(ErrorDestructorProbe &&) = default; + + ~ErrorDestructorProbe() { ++destructor_calls_; } + + static int destructor_calls() noexcept { return destructor_calls_; } + static void reset() noexcept { destructor_calls_ = 0; } + +private: + inline static int destructor_calls_ = 0; +}; + +class ImplicitThrowFromSourceValue +{ +public: + ImplicitThrowFromSourceValue(const SourceValue &) { throw ConstructorException {}; } + ImplicitThrowFromSourceValue(SourceValue &&) { throw ConstructorException {}; } +}; + +class ImplicitTargetValue +{ +public: + ImplicitTargetValue(const SourceValue &) {} + ImplicitTargetValue(SourceValue &&) {} +}; + +template +class ImplicitThrowFromSourceError +{ +public: + ImplicitThrowFromSourceError(const SourceError &) { throw ConstructorException {}; } + ImplicitThrowFromSourceError(SourceError &&) { throw ConstructorException {}; } + + ~ImplicitThrowFromSourceError() { ++destructor_calls_; } + + static int destructor_calls() noexcept { return destructor_calls_; } + static void reset() noexcept { destructor_calls_ = 0; } + +private: + inline static int destructor_calls_ = 0; +}; + +template +class ExplicitThrowFromSourceError +{ +public: + explicit ExplicitThrowFromSourceError(const SourceError &) { throw ConstructorException {}; } + explicit ExplicitThrowFromSourceError(SourceError &&) { throw ConstructorException {}; } + + ~ExplicitThrowFromSourceError() { ++destructor_calls_; } + + static int destructor_calls() noexcept { return destructor_calls_; } + static void reset() noexcept { destructor_calls_ = 0; } + +private: + inline static int destructor_calls_ = 0; +}; + +class ThrowOnCopyValue +{ +public: + ThrowOnCopyValue() = default; + ThrowOnCopyValue(const ThrowOnCopyValue &) { throw ConstructorException {}; } + ThrowOnCopyValue(ThrowOnCopyValue &&) = default; + ThrowOnCopyValue &operator=(const ThrowOnCopyValue &) = default; + ThrowOnCopyValue &operator=(ThrowOnCopyValue &&) = default; +}; + +class ThrowOnMoveValue +{ +public: + ThrowOnMoveValue() = default; + ThrowOnMoveValue(const ThrowOnMoveValue &) = delete; + ThrowOnMoveValue(ThrowOnMoveValue &&) { throw ConstructorException {}; } + ThrowOnMoveValue &operator=(const ThrowOnMoveValue &) = delete; + ThrowOnMoveValue &operator=(ThrowOnMoveValue &&) = default; +}; + +template +class ThrowOnCopyError +{ +public: + ThrowOnCopyError() = default; + ThrowOnCopyError(const ThrowOnCopyError &) { throw ConstructorException {}; } + ThrowOnCopyError(ThrowOnCopyError &&) = default; + ThrowOnCopyError &operator=(const ThrowOnCopyError &) = default; + ThrowOnCopyError &operator=(ThrowOnCopyError &&) = default; + + ~ThrowOnCopyError() { ++destructor_calls_; } + + static int destructor_calls() noexcept { return destructor_calls_; } + static void reset() noexcept { destructor_calls_ = 0; } + +private: + inline static int destructor_calls_ = 0; +}; + +template +class ThrowOnMoveError +{ +public: + ThrowOnMoveError() = default; + ThrowOnMoveError(const ThrowOnMoveError &) = delete; + ThrowOnMoveError(ThrowOnMoveError &&) { throw ConstructorException {}; } + ThrowOnMoveError &operator=(const ThrowOnMoveError &) = delete; + ThrowOnMoveError &operator=(ThrowOnMoveError &&) = default; + + ~ThrowOnMoveError() { ++destructor_calls_; } + + static int destructor_calls() noexcept { return destructor_calls_; } + static void reset() noexcept { destructor_calls_ = 0; } + +private: + inline static int destructor_calls_ = 0; +}; + +template +void require_no_destructor_for_unconstructed_error(Operation &&operation) +{ + ErrorProbe::reset(); + REQUIRE_THROWS_AS(std::forward(operation)(), ConstructorException); + REQUIRE(ErrorProbe::destructor_calls() == 0); +} + +} // namespace + +TEST_CASE("failed construction does not destroy either unconstructed alternative", "[expected][constructor][exception-safety][lifetime]") +{ + using Source = zeus::expected; + using ValueProbe = ThrowingValueDestructorProbe<0>; + using ErrorProbe = ErrorDestructorProbe<0>; + using Target = zeus::expected; + + const Source source(std::in_place); + ValueProbe::reset(); + ErrorProbe::reset(); + + REQUIRE_THROWS_AS(static_cast(Target(source)), ConstructorException); + REQUIRE(ValueProbe::destructor_calls() == 0); + REQUIRE(ErrorProbe::destructor_calls() == 0); +} + +TEST_CASE( + "implicit lvalue converting constructor does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]" +) +{ + using Source = zeus::expected; + using Probe = ErrorDestructorProbe<1>; + using Target = zeus::expected; + + const Source source(std::in_place); + require_no_destructor_for_unconstructed_error( + [&] + { + Target target = source; + (void) target; + } + ); +} + +TEST_CASE( + "explicit lvalue converting constructor does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]" +) +{ + using Source = zeus::expected; + using Probe = ExplicitThrowFromSourceError<2>; + using Target = zeus::expected; + + const Source source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Target target(source); + (void) target; + } + ); +} + +TEST_CASE( + "implicit rvalue converting constructor does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]" +) +{ + using Source = zeus::expected; + using Probe = ErrorDestructorProbe<3>; + using Target = zeus::expected; + + Source source(std::in_place); + require_no_destructor_for_unconstructed_error( + [&] + { + Target target = std::move(source); + (void) target; + } + ); +} + +TEST_CASE( + "explicit rvalue converting constructor does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]" +) +{ + using Source = zeus::expected; + using Probe = ExplicitThrowFromSourceError<4>; + using Target = zeus::expected; + + Source source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Target target(std::move(source)); + (void) target; + } + ); +} + +TEST_CASE("copying a value does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]") +{ + using Probe = ErrorDestructorProbe<5>; + using Expected = zeus::expected; + + const Expected source(std::in_place); + require_no_destructor_for_unconstructed_error( + [&] + { + Expected target(source); + (void) target; + } + ); +} + +TEST_CASE("copying an error does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]") +{ + using Probe = ThrowOnCopyError<6>; + using Expected = zeus::expected; + + const Expected source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Expected target(source); + (void) target; + } + ); +} + +TEST_CASE("moving a value does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]") +{ + using Probe = ErrorDestructorProbe<7>; + using Expected = zeus::expected; + + Expected source(std::in_place); + require_no_destructor_for_unconstructed_error( + [&] + { + Expected target(std::move(source)); + (void) target; + } + ); +} + +TEST_CASE("moving an error does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]") +{ + using Probe = ThrowOnMoveError<8>; + using Expected = zeus::expected; + + Expected source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Expected target(std::move(source)); + (void) target; + } + ); +} + +TEST_CASE("copying a void expected does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]") +{ + using Probe = ThrowOnCopyError<9>; + using Expected = zeus::expected; + + const Expected source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Expected target(source); + (void) target; + } + ); +} + +TEST_CASE("moving a void expected does not destroy an unconstructed error", "[expected][constructor][exception-safety][lifetime]") +{ + using Probe = ThrowOnMoveError<10>; + using Expected = zeus::expected; + + Expected source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Expected target(std::move(source)); + (void) target; + } + ); +} + +TEST_CASE( + "implicit lvalue converting void expected does not destroy an unconstructed error", + "[expected][constructor][exception-safety][lifetime]" +) +{ + using Source = zeus::expected; + using Probe = ImplicitThrowFromSourceError<11>; + using Target = zeus::expected; + + const Source source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Target target = source; + (void) target; + } + ); +} + +TEST_CASE( + "explicit lvalue converting void expected does not destroy an unconstructed error", + "[expected][constructor][exception-safety][lifetime]" +) +{ + using Source = zeus::expected; + using Probe = ExplicitThrowFromSourceError<12>; + using Target = zeus::expected; + + const Source source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Target target(source); + (void) target; + } + ); +} + +TEST_CASE( + "implicit rvalue converting void expected does not destroy an unconstructed error", + "[expected][constructor][exception-safety][lifetime]" +) +{ + using Source = zeus::expected; + using Probe = ImplicitThrowFromSourceError<13>; + using Target = zeus::expected; + + Source source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Target target = std::move(source); + (void) target; + } + ); +} + +TEST_CASE( + "explicit rvalue converting void expected does not destroy an unconstructed error", + "[expected][constructor][exception-safety][lifetime]" +) +{ + using Source = zeus::expected; + using Probe = ExplicitThrowFromSourceError<14>; + using Target = zeus::expected; + + Source source(zeus::unexpect); + require_no_destructor_for_unconstructed_error( + [&] + { + Target target(std::move(source)); + (void) target; + } + ); +} diff --git a/tests/test_expected/converting_constructor_tests.cpp b/tests/test_expected/converting_constructor_tests.cpp new file mode 100644 index 0000000..b905165 --- /dev/null +++ b/tests/test_expected/converting_constructor_tests.cpp @@ -0,0 +1,113 @@ +#include + +#include + +#include + +namespace +{ + +class ResourceOwningValue +{ +public: + ResourceOwningValue() + : resource_(new char[1]) + { + ++active_resources_; + } + + ~ResourceOwningValue() + { + if (resource_ != nullptr) + { + delete[] resource_; + --active_resources_; + } + } + + ResourceOwningValue(const ResourceOwningValue &) = delete; + ResourceOwningValue &operator=(const ResourceOwningValue &) = delete; + ResourceOwningValue &operator=(ResourceOwningValue &&) = delete; + + ResourceOwningValue(ResourceOwningValue &&other) noexcept + : resource_(std::exchange(other.resource_, nullptr)) + { + } + + static int active_resources() noexcept { return active_resources_; } + +private: + inline static int active_resources_ = 0; + char *resource_ = nullptr; +}; + +class ConvertibleValue +{ +public: + operator ResourceOwningValue() { return std::move(value_); } + +private: + ResourceOwningValue value_; +}; + +class NonDefaultConstructibleValue +{ +public: + NonDefaultConstructibleValue() = delete; + NonDefaultConstructibleValue(int value) + : value_(value) + { + } + + int value() const noexcept { return value_; } + +private: + int value_; +}; + +zeus::expected make_resource_owning_value() +{ + zeus::expected source = ConvertibleValue {}; + return source; +} + +} // namespace + +TEST_CASE("converting expected does not leak value resources", "[expected][converting-constructor][lifetime]") +{ + // Adapted from the reproducer in https://github.com/TartanLlama/expected/issues/180. + const int initial_active_resources = ResourceOwningValue::active_resources(); + + { + const auto result = make_resource_owning_value(); + REQUIRE(result.has_value()); + } + + REQUIRE(ResourceOwningValue::active_resources() == initial_active_resources); +} + +TEST_CASE("converting expected does not require a default-constructible value", "[expected][converting-constructor]") +{ + const zeus::expected source = 42; + + const zeus::expected result = source; + + REQUIRE(result.has_value()); + REQUIRE(result->value() == 42); +} + +TEST_CASE("converting an error does not construct the value alternative", "[expected][converting-constructor][lifetime]") +{ + const int initial_active_resources = ResourceOwningValue::active_resources(); + + { + zeus::expected source(zeus::unexpect, 42); + const zeus::expected result(std::move(source)); + + REQUIRE_FALSE(result.has_value()); + REQUIRE(result.error() == 42); + REQUIRE(ResourceOwningValue::active_resources() == initial_active_resources); + } + + REQUIRE(ResourceOwningValue::active_resources() == initial_active_resources); +} diff --git a/tests/test_expected/swap_exception_tests.cpp b/tests/test_expected/swap_exception_tests.cpp new file mode 100644 index 0000000..837b370 --- /dev/null +++ b/tests/test_expected/swap_exception_tests.cpp @@ -0,0 +1,206 @@ +#include +#include + +#include + +#include + +namespace +{ + +struct ConstructionException +{ +}; + +template +class NothrowMoveState +{ +public: + explicit NothrowMoveState(int payload = 0) + : payload_(payload) + { + ++live_instances_; + } + + NothrowMoveState(const NothrowMoveState &other) noexcept + : payload_(other.payload_) + { + ++live_instances_; + } + + NothrowMoveState(NothrowMoveState &&other) noexcept + : payload_(std::exchange(other.payload_, -1)) + { + ++live_instances_; + } + + NothrowMoveState &operator=(const NothrowMoveState &other) noexcept + { + payload_ = other.payload_; + return *this; + } + + NothrowMoveState &operator=(NothrowMoveState &&other) noexcept + { + payload_ = std::exchange(other.payload_, -1); + return *this; + } + + ~NothrowMoveState() { --live_instances_; } + + friend void swap(NothrowMoveState &lhs, NothrowMoveState &rhs) noexcept + { + using std::swap; + swap(lhs.payload_, rhs.payload_); + } + + int payload() const noexcept { return payload_; } + static int live_instances() noexcept { return live_instances_; } + +private: + inline static int live_instances_ = 0; + int payload_ = 0; +}; + +template +class ThrowOnMoveState +{ +public: + explicit ThrowOnMoveState(int payload = 0) + : payload_(payload) + { + ++live_instances_; + } + + ThrowOnMoveState(const ThrowOnMoveState &) = delete; + ThrowOnMoveState &operator=(const ThrowOnMoveState &) = delete; + + ThrowOnMoveState(ThrowOnMoveState &&other) + : payload_(other.payload_) + { + throw ConstructionException {}; + } + + ThrowOnMoveState &operator=(ThrowOnMoveState &&other) noexcept + { + payload_ = std::exchange(other.payload_, -1); + return *this; + } + + ~ThrowOnMoveState() { --live_instances_; } + + friend void swap(ThrowOnMoveState &lhs, ThrowOnMoveState &rhs) noexcept + { + using std::swap; + swap(lhs.payload_, rhs.payload_); + } + + int payload() const noexcept { return payload_; } + static int live_instances() noexcept { return live_instances_; } + +private: + inline static int live_instances_ = 0; + int payload_ = 0; +}; + +} // namespace + +TEST_CASE("swap restores an error when value construction throws", "[expected][swap][exception-safety]") +{ + using Value = ThrowOnMoveState<0>; + using Error = NothrowMoveState<0>; + using Expected = zeus::expected; + + STATIC_REQUIRE_FALSE(std::is_nothrow_move_constructible_v); + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + + const int initial_live_values = Value::live_instances(); + const int initial_live_errors = Error::live_instances(); + + { + Expected value(std::in_place, 17); + Expected error(zeus::unexpect, 42); + + REQUIRE_THROWS_AS(value.swap(error), ConstructionException); + REQUIRE(value.has_value()); + REQUIRE(value->payload() == 17); + REQUIRE_FALSE(error.has_value()); + REQUIRE(error.error().payload() == 42); + REQUIRE(Value::live_instances() == initial_live_values + 1); + REQUIRE(Error::live_instances() == initial_live_errors + 1); + } + + REQUIRE(Value::live_instances() == initial_live_values); + REQUIRE(Error::live_instances() == initial_live_errors); +} + +TEST_CASE("swap restores a value when error construction throws", "[expected][swap][exception-safety]") +{ + using Value = NothrowMoveState<1>; + using Error = ThrowOnMoveState<1>; + using Expected = zeus::expected; + + STATIC_REQUIRE(std::is_nothrow_move_constructible_v); + STATIC_REQUIRE_FALSE(std::is_nothrow_move_constructible_v); + + const int initial_live_values = Value::live_instances(); + const int initial_live_errors = Error::live_instances(); + + { + Expected value(std::in_place, 17); + Expected error(zeus::unexpect, 42); + + REQUIRE_THROWS_AS(value.swap(error), ConstructionException); + REQUIRE(value.has_value()); + REQUIRE(value->payload() == 17); + REQUIRE_FALSE(error.has_value()); + REQUIRE(error.error().payload() == 42); + REQUIRE(Value::live_instances() == initial_live_values + 1); + REQUIRE(Error::live_instances() == initial_live_errors + 1); + } + + REQUIRE(Value::live_instances() == initial_live_values); + REQUIRE(Error::live_instances() == initial_live_errors); +} + +TEST_CASE("void swap retains states when moving an error into the left operand throws", "[expected][swap][exception-safety]") +{ + using Error = ThrowOnMoveState<2>; + using Expected = zeus::expected; + + const int initial_live_errors = Error::live_instances(); + + { + Expected value; + Expected error(zeus::unexpect, 42); + + REQUIRE_THROWS_AS(value.swap(error), ConstructionException); + REQUIRE(value.has_value()); + REQUIRE_FALSE(error.has_value()); + REQUIRE(error.error().payload() == 42); + REQUIRE(Error::live_instances() == initial_live_errors + 1); + } + + REQUIRE(Error::live_instances() == initial_live_errors); +} + +TEST_CASE("void swap retains states when moving an error into the right operand throws", "[expected][swap][exception-safety]") +{ + using Error = ThrowOnMoveState<3>; + using Expected = zeus::expected; + + const int initial_live_errors = Error::live_instances(); + + { + Expected error(zeus::unexpect, 42); + Expected value; + + REQUIRE_THROWS_AS(error.swap(value), ConstructionException); + REQUIRE_FALSE(error.has_value()); + REQUIRE(error.error().payload() == 42); + REQUIRE(value.has_value()); + REQUIRE(Error::live_instances() == initial_live_errors + 1); + } + + REQUIRE(Error::live_instances() == initial_live_errors); +} diff --git a/tests/test_no_exceptions/CMakeLists.txt b/tests/test_no_exceptions/CMakeLists.txt index 454812f..9f902c1 100644 --- a/tests/test_no_exceptions/CMakeLists.txt +++ b/tests/test_no_exceptions/CMakeLists.txt @@ -11,3 +11,4 @@ else () endif () add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) +zeus_expected_configure_test_sanitizers(${PROJECT_NAME}) diff --git a/tests/third_party/CMakeLists.txt b/tests/third_party/CMakeLists.txt index a892335..be59824 100644 --- a/tests/third_party/CMakeLists.txt +++ b/tests/third_party/CMakeLists.txt @@ -1,3 +1,16 @@ +set(_zeus_expected_enable_msvc_stl_tests OFF) if (MSVC) + set(_zeus_expected_enable_msvc_stl_tests ON) +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" + AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15) + set(_zeus_expected_enable_msvc_stl_tests ON) +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" + AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 22) + set(_zeus_expected_enable_msvc_stl_tests ON) +endif () + +if (_zeus_expected_enable_msvc_stl_tests) add_subdirectory(msvc) endif () + +unset(_zeus_expected_enable_msvc_stl_tests) diff --git a/tests/third_party/msvc/CMakeLists.txt b/tests/third_party/msvc/CMakeLists.txt index 7126488..83f06e9 100644 --- a/tests/third_party/msvc/CMakeLists.txt +++ b/tests/third_party/msvc/CMakeLists.txt @@ -1,7 +1,15 @@ include_directories(SYSTEM "include") +if (NOT MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_compile_options( + -Wno-deprecated-volatile + -Wno-unqualified-std-cast-call + ) +endif () + add_subdirectory(msvc_stl_p0323r12_test) -if (MSVC_TOOLSET_VERSION GREATER_EQUAL 143) - # can only built with v143 and later +if (NOT MSVC OR MSVC_TOOLSET_VERSION GREATER_EQUAL 143) + # The MSVC build requires v143 or later. GCC and Clang are already + # version-gated by the parent directory. add_subdirectory(msvc_stl_p2505r5_test) endif () diff --git a/tests/third_party/msvc/include/is_permissive.hpp b/tests/third_party/msvc/include/is_permissive.hpp index 007ae88..c022e10 100644 --- a/tests/third_party/msvc/include/is_permissive.hpp +++ b/tests/third_party/msvc/include/is_permissive.hpp @@ -3,6 +3,7 @@ #pragma once +#ifdef _MSC_VER #include namespace detail { @@ -29,3 +30,6 @@ template constexpr bool is_permissive_v = detail::PermissiveTest::test(); _INLINE_VAR constexpr bool is_permissive = is_permissive_v; +#else +inline constexpr bool is_permissive = false; +#endif diff --git a/tests/third_party/msvc/msvc_stl_p0323r12_test/CMakeLists.txt b/tests/third_party/msvc/msvc_stl_p0323r12_test/CMakeLists.txt index bf211e0..be58645 100644 --- a/tests/third_party/msvc/msvc_stl_p0323r12_test/CMakeLists.txt +++ b/tests/third_party/msvc/msvc_stl_p0323r12_test/CMakeLists.txt @@ -1,6 +1,10 @@ project(msvc_stl_p0323r12_test LANGUAGES CXX) add_executable(${PROJECT_NAME}) -set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20) +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) target_link_libraries(${PROJECT_NAME} PRIVATE zeus::expected) @@ -10,3 +14,4 @@ target_sources(${PROJECT_NAME} ) add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) +zeus_expected_configure_test_sanitizers(${PROJECT_NAME}) diff --git a/tests/third_party/msvc/msvc_stl_p0323r12_test/msvc_stl_p0323r12_test.cpp b/tests/third_party/msvc/msvc_stl_p0323r12_test/msvc_stl_p0323r12_test.cpp index 4807440..091c01d 100644 --- a/tests/third_party/msvc/msvc_stl_p0323r12_test/msvc_stl_p0323r12_test.cpp +++ b/tests/third_party/msvc/msvc_stl_p0323r12_test/msvc_stl_p0323r12_test.cpp @@ -103,21 +103,21 @@ namespace test_unexpected { constexpr bool compare_is_noexcept = IsYes(nothrowComparable); struct test_error { - constexpr test_error(const int& val) noexcept(copy_construction_is_noexcept) : _val(val) {} - constexpr test_error(int&& val) noexcept(move_construction_is_noexcept) : _val(val) {} + constexpr test_error(const int& val) noexcept(IsYes(nothrowCopyConstructible)) : _val(val) {} + constexpr test_error(int&& val) noexcept(IsYes(nothrowMoveConstructible)) : _val(val) {} - constexpr test_error(initializer_list, const int& val) noexcept(copy_construction_is_noexcept) + constexpr test_error(initializer_list, const int& val) noexcept(IsYes(nothrowCopyConstructible)) : _val(val) {} - constexpr test_error(initializer_list, int&& val) noexcept(move_construction_is_noexcept) + constexpr test_error(initializer_list, int&& val) noexcept(IsYes(nothrowMoveConstructible)) : _val(val) {} - constexpr test_error(const convertible& other) noexcept(copy_construction_is_noexcept) : _val(other._val) {} - constexpr test_error(convertible&& other) noexcept(move_construction_is_noexcept) : _val(other._val) {} + constexpr test_error(const convertible& other) noexcept(IsYes(nothrowCopyConstructible)) : _val(other._val) {} + constexpr test_error(convertible&& other) noexcept(IsYes(nothrowMoveConstructible)) : _val(other._val) {} - [[nodiscard]] constexpr bool operator==(const test_error& right) const noexcept(compare_is_noexcept) { + [[nodiscard]] constexpr bool operator==(const test_error& right) const noexcept(IsYes(nothrowComparable)) { return _val == right._val; } - [[nodiscard]] constexpr bool operator==(const convertible& right) const noexcept(compare_is_noexcept) { + [[nodiscard]] constexpr bool operator==(const convertible& right) const noexcept(IsYes(nothrowComparable)) { return _val == right._val; } @@ -286,8 +286,8 @@ namespace test_expected { struct payload_copy_constructor { payload_copy_constructor() = default; payload_copy_constructor& operator=(const payload_copy_constructor&) = delete; - constexpr payload_copy_constructor(const payload_copy_constructor&) noexcept(should_be_noexcept) - requires (!should_be_trivial) + constexpr payload_copy_constructor(const payload_copy_constructor&) noexcept(IsYes(triviallyCopyConstructible) || IsYes(nothrowCopyConstructible)) + requires (!IsYes(triviallyCopyConstructible)) : _val(42) {} constexpr payload_copy_constructor(const payload_copy_constructor&) = default; @@ -374,8 +374,8 @@ namespace test_expected { payload_move_constructor() = default; payload_move_constructor(const payload_move_constructor&) = default; payload_move_constructor& operator=(payload_move_constructor&&) = delete; - constexpr payload_move_constructor(payload_move_constructor&&) noexcept(should_be_noexcept) - requires (!should_be_trivial) + constexpr payload_move_constructor(payload_move_constructor&&) noexcept(IsYes(triviallyMoveConstructible) || IsYes(nothrowMoveConstructible)) + requires (!IsYes(triviallyMoveConstructible)) : _val(42) {} constexpr payload_move_constructor(payload_move_constructor&&) = default; @@ -521,13 +521,13 @@ namespace test_expected { payload_constructors() = default; // Note clang does not accept local variables in explicit constexpr explicit(IsYes(explicitConstructible)) payload_constructors(const convertible&) - noexcept(should_be_noexcept) + noexcept(IsYes(nothrowConstructible)) : _val(3) {} constexpr explicit(IsYes(explicitConstructible)) payload_constructors(convertible&&) - noexcept(should_be_noexcept) + noexcept(IsYes(nothrowConstructible)) : _val(42) {} constexpr explicit(IsYes(explicitConstructible)) payload_constructors(initializer_list&, convertible) - noexcept(should_be_noexcept) + noexcept(IsYes(nothrowConstructible)) : _val(1337) {} [[nodiscard]] constexpr bool operator==(const int val) const noexcept { @@ -554,10 +554,10 @@ namespace test_expected { assert(move_constructed_value.value() == 42); static_assert(noexcept(Expected{Input{}}) == should_be_noexcept || is_permissive); - const Expected brace_constructed_value{{}}; + const Expected brace_constructed_value{{payload_constructors{}}}; assert(brace_constructed_value); assert(brace_constructed_value.value() == 0); - static_assert(noexcept(Expected{{}})); + static_assert(noexcept(Expected{{payload_constructors{}}})); } { // converting from different expected @@ -739,26 +739,26 @@ namespace test_expected { struct payload_assign { payload_assign() = default; constexpr payload_assign(const int val) noexcept : _val(val) {} - constexpr payload_assign(const payload_assign& other) noexcept(nothrow_copy_constructible) + constexpr payload_assign(const payload_assign& other) noexcept(IsYes(nothrowCopyConstructible)) : _val(other._val) {} - constexpr payload_assign(payload_assign&& other) noexcept(nothrow_move_constructible) : _val(other._val) {} - constexpr payload_assign& operator=(const payload_assign& other) noexcept(nothrow_copy_assignable) { + constexpr payload_assign(payload_assign&& other) noexcept(IsYes(nothrowMoveConstructible)) : _val(other._val) {} + constexpr payload_assign& operator=(const payload_assign& other) noexcept(IsYes(nothrowCopyAssignable)) { _val = other._val; return *this; } - constexpr payload_assign& operator=(payload_assign&& other) noexcept(nothrow_move_assignable) { + constexpr payload_assign& operator=(payload_assign&& other) noexcept(IsYes(nothrowMoveAssignable)) { _val = other._val; return *this; } - constexpr payload_assign(const convertible& other) noexcept(nothrow_copy_constructible) + constexpr payload_assign(const convertible& other) noexcept(IsYes(nothrowCopyConstructible)) : _val(other._val) {} - constexpr payload_assign(convertible&& other) noexcept(nothrow_move_constructible) : _val(other._val) {} - constexpr payload_assign& operator=(const convertible& other) noexcept(nothrow_copy_assignable) { + constexpr payload_assign(convertible&& other) noexcept(IsYes(nothrowMoveConstructible)) : _val(other._val) {} + constexpr payload_assign& operator=(const convertible& other) noexcept(IsYes(nothrowCopyAssignable)) { _val = other._val; return *this; } - constexpr payload_assign& operator=(convertible&& other) noexcept(nothrow_move_assignable) { + constexpr payload_assign& operator=(convertible&& other) noexcept(IsYes(nothrowMoveAssignable)) { _val = other._val; return *this; } @@ -2074,7 +2074,7 @@ namespace test_expected { struct payload_equality { constexpr payload_equality(const int val) noexcept : _val(val) {} - [[nodiscard]] constexpr bool operator==(const payload_equality& right) const noexcept(should_be_noexcept) { + [[nodiscard]] constexpr bool operator==(const payload_equality& right) const noexcept(IsYes(nothrowComparable)) { return _val == right._val; } diff --git a/tests/third_party/msvc/msvc_stl_p2505r5_test/CMakeLists.txt b/tests/third_party/msvc/msvc_stl_p2505r5_test/CMakeLists.txt index d2adfcc..b4367b3 100644 --- a/tests/third_party/msvc/msvc_stl_p2505r5_test/CMakeLists.txt +++ b/tests/third_party/msvc/msvc_stl_p2505r5_test/CMakeLists.txt @@ -1,6 +1,10 @@ project(msvc_stl_p2505r5_test LANGUAGES CXX) add_executable(${PROJECT_NAME}) -set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20) +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) target_link_libraries(${PROJECT_NAME} PRIVATE zeus::expected) @@ -10,3 +14,4 @@ target_sources(${PROJECT_NAME} ) add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) +zeus_expected_configure_test_sanitizers(${PROJECT_NAME})