From 618ff9bdbe869f8546cc918307c1c4c01ec7e0ad Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 18 Jan 2025 20:27:06 +0000 Subject: [PATCH 01/46] Update include guard to new path --- include/fn/and_then.hpp | 4 ++-- include/fn/choice.hpp | 4 ++-- include/fn/concepts.hpp | 4 ++-- include/fn/detail/functional.hpp | 4 ++-- include/fn/detail/fwd.hpp | 4 ++-- include/fn/detail/fwd_macro.hpp | 4 ++-- include/fn/detail/meta.hpp | 4 ++-- include/fn/detail/pack_impl.hpp | 4 ++-- include/fn/detail/traits.hpp | 9 +++++++-- include/fn/detail/variadic_union.hpp | 4 ++-- include/fn/expected.hpp | 4 ++-- include/fn/fail.hpp | 4 ++-- include/fn/filter.hpp | 4 ++-- include/fn/functional.hpp | 4 ++-- include/fn/functor.hpp | 4 ++-- include/fn/fwd.hpp | 4 ++-- include/fn/inspect.hpp | 4 ++-- include/fn/inspect_error.hpp | 4 ++-- include/fn/optional.hpp | 4 ++-- include/fn/or_else.hpp | 4 ++-- include/fn/pack.hpp | 4 ++-- include/fn/recover.hpp | 4 ++-- include/fn/sum.hpp | 4 ++-- include/fn/transform.hpp | 4 ++-- include/fn/transform_error.hpp | 4 ++-- include/fn/utility.hpp | 4 ++-- include/fn/value_or.hpp | 4 ++-- 27 files changed, 59 insertions(+), 54 deletions(-) diff --git a/include/fn/and_then.hpp b/include/fn/and_then.hpp index d0388455..55348714 100644 --- a/include/fn/and_then.hpp +++ b/include/fn/and_then.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_AND_THEN -#define INCLUDE_FUNCTIONAL_AND_THEN +#ifndef INCLUDE_FN_AND_THEN +#define INCLUDE_FN_AND_THEN #include #include diff --git a/include/fn/choice.hpp b/include/fn/choice.hpp index 0e6a7f20..cf88855e 100644 --- a/include/fn/choice.hpp +++ b/include/fn/choice.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_CHOICE -#define INCLUDE_FUNCTIONAL_CHOICE +#ifndef INCLUDE_FN_CHOICE +#define INCLUDE_FN_CHOICE #include #include diff --git a/include/fn/concepts.hpp b/include/fn/concepts.hpp index ae7f2f86..6ca38035 100644 --- a/include/fn/concepts.hpp +++ b/include/fn/concepts.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_CONCEPTS -#define INCLUDE_FUNCTIONAL_CONCEPTS +#ifndef INCLUDE_FN_CONCEPTS +#define INCLUDE_FN_CONCEPTS #include #include diff --git a/include/fn/detail/functional.hpp b/include/fn/detail/functional.hpp index ccec3d61..2ee2ce0f 100644 --- a/include/fn/detail/functional.hpp +++ b/include/fn/detail/functional.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_DETAIL_FUNCTIONAL -#define INCLUDE_FUNCTIONAL_DETAIL_FUNCTIONAL +#ifndef INCLUDE_FN_DETAIL_FUNCTIONAL +#define INCLUDE_FN_DETAIL_FUNCTIONAL #include #include diff --git a/include/fn/detail/fwd.hpp b/include/fn/detail/fwd.hpp index 37cb610c..48e9c751 100644 --- a/include/fn/detail/fwd.hpp +++ b/include/fn/detail/fwd.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_DETAIL_FWD -#define INCLUDE_FUNCTIONAL_DETAIL_FWD +#ifndef INCLUDE_FN_DETAIL_FWD +#define INCLUDE_FN_DETAIL_FWD namespace fn { // NOTE Some forward declarations can lead to hard to troubleshoot compilation diff --git a/include/fn/detail/fwd_macro.hpp b/include/fn/detail/fwd_macro.hpp index 8383dacd..096dd8ec 100644 --- a/include/fn/detail/fwd_macro.hpp +++ b/include/fn/detail/fwd_macro.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_DETAIL_FWD_MACRO -#define INCLUDE_FUNCTIONAL_DETAIL_FWD_MACRO +#ifndef INCLUDE_FN_DETAIL_FWD_MACRO +#define INCLUDE_FN_DETAIL_FWD_MACRO // This FWD macro is a functional equivalent to std::forward(v), // but it saves compilation time (and typing) when used frequently. diff --git a/include/fn/detail/meta.hpp b/include/fn/detail/meta.hpp index 6df6fddc..50a5eead 100644 --- a/include/fn/detail/meta.hpp +++ b/include/fn/detail/meta.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_DETAIL_META -#define INCLUDE_FUNCTIONAL_DETAIL_META +#ifndef INCLUDE_FN_DETAIL_META +#define INCLUDE_FN_DETAIL_META #include #include diff --git a/include/fn/detail/pack_impl.hpp b/include/fn/detail/pack_impl.hpp index e680302c..26d15c47 100644 --- a/include/fn/detail/pack_impl.hpp +++ b/include/fn/detail/pack_impl.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_DETAIL_PACK_IMPL -#define INCLUDE_FUNCTIONAL_DETAIL_PACK_IMPL +#ifndef INCLUDE_FN_DETAIL_PACK_IMPL +#define INCLUDE_FN_DETAIL_PACK_IMPL #include #include diff --git a/include/fn/detail/traits.hpp b/include/fn/detail/traits.hpp index 80d229bd..629c4560 100644 --- a/include/fn/detail/traits.hpp +++ b/include/fn/detail/traits.hpp @@ -1,5 +1,10 @@ -#ifndef INCLUDE_FUNCTIONAL_DETAIL_TRAITS -#define INCLUDE_FUNCTIONAL_DETAIL_TRAITS +// Copyright (c) 2024 Bronek Kozicki +// +// Distributed under the ISC License. See accompanying file LICENSE.md +// or copy at https://opensource.org/licenses/ISC + +#ifndef INCLUDE_FN_DETAIL_TRAITS +#define INCLUDE_FN_DETAIL_TRAITS #include diff --git a/include/fn/detail/variadic_union.hpp b/include/fn/detail/variadic_union.hpp index 2d71c1af..a9106447 100644 --- a/include/fn/detail/variadic_union.hpp +++ b/include/fn/detail/variadic_union.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_DETAIL_VARIADIC_UNION -#define INCLUDE_FUNCTIONAL_DETAIL_VARIADIC_UNION +#ifndef INCLUDE_FN_DETAIL_VARIADIC_UNION +#define INCLUDE_FN_DETAIL_VARIADIC_UNION #include #include diff --git a/include/fn/expected.hpp b/include/fn/expected.hpp index ca4b615e..005e19c9 100644 --- a/include/fn/expected.hpp +++ b/include/fn/expected.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_EXPECTED -#define INCLUDE_FUNCTIONAL_EXPECTED +#ifndef INCLUDE_FN_EXPECTED +#define INCLUDE_FN_EXPECTED #include #include diff --git a/include/fn/fail.hpp b/include/fn/fail.hpp index f75670b6..7528070e 100644 --- a/include/fn/fail.hpp +++ b/include/fn/fail.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_FAIL -#define INCLUDE_FUNCTIONAL_FAIL +#ifndef INCLUDE_FN_FAIL +#define INCLUDE_FN_FAIL #include #include diff --git a/include/fn/filter.hpp b/include/fn/filter.hpp index 4abeeb2a..3f60e4f2 100644 --- a/include/fn/filter.hpp +++ b/include/fn/filter.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_FILTER -#define INCLUDE_FUNCTIONAL_FILTER +#ifndef INCLUDE_FN_FILTER +#define INCLUDE_FN_FILTER #include #include diff --git a/include/fn/functional.hpp b/include/fn/functional.hpp index 34e20e1b..7078fced 100644 --- a/include/fn/functional.hpp +++ b/include/fn/functional.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_FUNCTIONAL -#define INCLUDE_FUNCTIONAL_FUNCTIONAL +#ifndef INCLUDE_FN_FUNCTIONAL +#define INCLUDE_FN_FUNCTIONAL #include diff --git a/include/fn/functor.hpp b/include/fn/functor.hpp index c1695b27..480dfa04 100644 --- a/include/fn/functor.hpp +++ b/include/fn/functor.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_FUNCTOR -#define INCLUDE_FUNCTIONAL_FUNCTOR +#ifndef INCLUDE_FN_FUNCTOR +#define INCLUDE_FN_FUNCTOR #include #include diff --git a/include/fn/fwd.hpp b/include/fn/fwd.hpp index 6b8e530c..82ce460e 100644 --- a/include/fn/fwd.hpp +++ b/include/fn/fwd.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_FWD -#define INCLUDE_FUNCTIONAL_FWD +#ifndef INCLUDE_FN_FWD +#define INCLUDE_FN_FWD // For exposition only #include diff --git a/include/fn/inspect.hpp b/include/fn/inspect.hpp index 93af50cd..b2af9d20 100644 --- a/include/fn/inspect.hpp +++ b/include/fn/inspect.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_INSPECT -#define INCLUDE_FUNCTIONAL_INSPECT +#ifndef INCLUDE_FN_INSPECT +#define INCLUDE_FN_INSPECT #include #include diff --git a/include/fn/inspect_error.hpp b/include/fn/inspect_error.hpp index cdff4594..a9c65ec6 100644 --- a/include/fn/inspect_error.hpp +++ b/include/fn/inspect_error.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_INSPECT_ERROR -#define INCLUDE_FUNCTIONAL_INSPECT_ERROR +#ifndef INCLUDE_FN_INSPECT_ERROR +#define INCLUDE_FN_INSPECT_ERROR #include #include diff --git a/include/fn/optional.hpp b/include/fn/optional.hpp index 77b585d2..59e96e04 100644 --- a/include/fn/optional.hpp +++ b/include/fn/optional.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_OPTIONAL -#define INCLUDE_FUNCTIONAL_OPTIONAL +#ifndef INCLUDE_FN_OPTIONAL +#define INCLUDE_FN_OPTIONAL #include #include diff --git a/include/fn/or_else.hpp b/include/fn/or_else.hpp index 51d1e606..9935fef5 100644 --- a/include/fn/or_else.hpp +++ b/include/fn/or_else.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_OR_ELSE -#define INCLUDE_FUNCTIONAL_OR_ELSE +#ifndef INCLUDE_FN_OR_ELSE +#define INCLUDE_FN_OR_ELSE #include #include diff --git a/include/fn/pack.hpp b/include/fn/pack.hpp index 6e0102ac..c2b918ff 100644 --- a/include/fn/pack.hpp +++ b/include/fn/pack.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_PACK -#define INCLUDE_FUNCTIONAL_PACK +#ifndef INCLUDE_FN_PACK +#define INCLUDE_FN_PACK #include #include diff --git a/include/fn/recover.hpp b/include/fn/recover.hpp index bf274f3c..78551b19 100644 --- a/include/fn/recover.hpp +++ b/include/fn/recover.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_RECOVER -#define INCLUDE_FUNCTIONAL_RECOVER +#ifndef INCLUDE_FN_RECOVER +#define INCLUDE_FN_RECOVER #include #include diff --git a/include/fn/sum.hpp b/include/fn/sum.hpp index ca0620c6..e092e53b 100644 --- a/include/fn/sum.hpp +++ b/include/fn/sum.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_SUM -#define INCLUDE_FUNCTIONAL_SUM +#ifndef INCLUDE_FN_SUM +#define INCLUDE_FN_SUM #include #include diff --git a/include/fn/transform.hpp b/include/fn/transform.hpp index e029e8d4..f4643d45 100644 --- a/include/fn/transform.hpp +++ b/include/fn/transform.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_TRANSFORM -#define INCLUDE_FUNCTIONAL_TRANSFORM +#ifndef INCLUDE_FN_TRANSFORM +#define INCLUDE_FN_TRANSFORM #include #include diff --git a/include/fn/transform_error.hpp b/include/fn/transform_error.hpp index a4176ee6..1b4c76b9 100644 --- a/include/fn/transform_error.hpp +++ b/include/fn/transform_error.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_TRANSFORM_ERROR -#define INCLUDE_FUNCTIONAL_TRANSFORM_ERROR +#ifndef INCLUDE_FN_TRANSFORM_ERROR +#define INCLUDE_FN_TRANSFORM_ERROR #include #include diff --git a/include/fn/utility.hpp b/include/fn/utility.hpp index 758196b0..f0e026a4 100644 --- a/include/fn/utility.hpp +++ b/include/fn/utility.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_UTILITY -#define INCLUDE_FUNCTIONAL_UTILITY +#ifndef INCLUDE_FN_UTILITY +#define INCLUDE_FN_UTILITY #include #include diff --git a/include/fn/value_or.hpp b/include/fn/value_or.hpp index b0316751..d2747fec 100644 --- a/include/fn/value_or.hpp +++ b/include/fn/value_or.hpp @@ -3,8 +3,8 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC -#ifndef INCLUDE_FUNCTIONAL_OR_ELSE -#define INCLUDE_FUNCTIONAL_OR_ELSE +#ifndef INCLUDE_FN_OR_ELSE +#define INCLUDE_FN_OR_ELSE #include #include From acdd08e8261f5a19c7cc8fe59c1b50bc2560fe92 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 18 Jan 2025 21:05:09 +0000 Subject: [PATCH 02/46] Add dummy expected with tests --- include/CMakeLists.txt | 33 ++++- include/pfn/expected.hpp | 266 +++++++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 43 +++---- tests/pfn/expected.cpp | 18 +++ 4 files changed, 333 insertions(+), 27 deletions(-) create mode 100644 include/pfn/expected.hpp create mode 100644 tests/pfn/expected.cpp diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index 0fb35ea9..2fe4e0d3 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -5,6 +5,23 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Pls keep the filenames sorted +set(INCLUDE_PFN_HEADERS + pfn/expected.hpp +) + +add_library(include_pfn INTERFACE) +target_sources(include_pfn INTERFACE + FILE_SET include_pfn_headers + TYPE HEADERS + FILES ${INCLUDE_PFN_HEADERS}) +target_include_directories(include_pfn INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_options(include_pfn INTERFACE -Wno-non-template-friend) + +install(TARGETS include_pfn + FILE_SET include_pfn_headers + DESTINATION include) + # Pls keep the filenames sorted set(INCLUDE_FN_HEADERS fn/detail/functional.hpp @@ -41,7 +58,10 @@ target_sources(include_fn INTERFACE FILE_SET include_fn_headers TYPE HEADERS FILES ${INCLUDE_FN_HEADERS}) -target_include_directories(include_fn INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(include_fn SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_options(include_fn INTERFACE -Wno-missing-braces) +# TODO uncomment when include_pfn is ready +# target_link_libraries(include_fn INTERFACE include_pfn) install(TARGETS include_fn FILE_SET include_fn_headers @@ -50,15 +70,16 @@ install(TARGETS include_fn include(TargetGenerator) # Generate sentinel target for each individual header, as a basic sanity check -foreach(source IN ITEMS ${INCLUDE_FN_HEADERS}) - string(REGEX REPLACE "^fn/(detail|)/?([^\.]+)\.hpp$" "\\1\\2" root_name ${source}) +foreach(source IN ITEMS ${INCLUDE_FN_HEADERS} ${INCLUDE_PFN_HEADERS}) + string(REGEX REPLACE "^(fn|pfn)/(detail|)/?([^\.]+)\.hpp$" "\\1_\\2\\3" root_name ${source}) + string(REGEX REPLACE "^(fn|pfn)/.*$" "\\1" subdir ${source}) setup_target_for_file( - NAME "include_fn_sentinel_${root_name}" + NAME "sentinel_${root_name}" SOURCE "${source}" NEW_SOURCE "#include <${source}>\nint main() {}\n" - DEPENDENCIES include_fn + SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" + DEPENDENCIES "include_${subdir}" COMPILE_OPTIONS -Wall -Wextra -Wpedantic -Wno-missing-braces ) unset(root_name) - endforeach() diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp new file mode 100644 index 00000000..2f95dd53 --- /dev/null +++ b/include/pfn/expected.hpp @@ -0,0 +1,266 @@ +// Copyright (c) 2025 Bronek Kozicki +// +// Distributed under the ISC License. See accompanying file LICENSE.md +// or copy at https://opensource.org/licenses/ISC + +#ifndef INCLUDE_PFN_EXPECTED +#define INCLUDE_PFN_EXPECTED + +#include +#include +#include + +namespace fn::detail { + +template class bad_expected_access; + +template <> class bad_expected_access : public std::exception { +protected: + bad_expected_access() noexcept; + bad_expected_access(bad_expected_access const &) noexcept; + bad_expected_access(bad_expected_access &&) noexcept; + bad_expected_access &operator=(bad_expected_access const &) noexcept; + bad_expected_access &operator=(bad_expected_access &&) noexcept; + ~bad_expected_access(); + +public: + [[nodiscard]] char const *what() const noexcept override; +}; + +template class bad_expected_access : public bad_expected_access { +public: + explicit bad_expected_access(E); + [[nodiscard]] char const *what() const noexcept override; + E &error() & noexcept; + E const &error() const & noexcept; + E &&error() && noexcept; + E const &&error() const && noexcept; + +private: + E e_; +}; + +struct unexpect_t { + explicit unexpect_t() = default; +}; +constexpr inline unexpect_t unexpect{}; + +template class unexpected { +public: + constexpr unexpected(unexpected const &) = default; + constexpr unexpected(unexpected &&) = default; + template constexpr explicit unexpected(Err &&); + template constexpr explicit unexpected(std::in_place_t, Args &&...); + template + constexpr explicit unexpected(std::in_place_t, std::initializer_list, Args &&...); + + constexpr unexpected &operator=(unexpected const &) = default; + constexpr unexpected &operator=(unexpected &&) = default; + + constexpr E const &error() const & noexcept; + constexpr E &error() & noexcept; + constexpr E const &&error() const && noexcept; + constexpr E &&error() && noexcept; + + constexpr void swap(unexpected &other) noexcept(/* TODO */ false); + + template constexpr friend bool operator==(unexpected const &, unexpected const &); + + constexpr friend void swap(unexpected &x, unexpected &y) noexcept(noexcept(x.swap(y))); + +private: + E e_; +}; + +template unexpected(E) -> unexpected; + +template class expected; + +// declare void specialization +template + requires std::is_void_v +class expected; + +template class expected { +public: + using value_type = T; + using error_type = E; + using unexpected_type = unexpected; + + template using rebind = expected; + + // [expected.object.cons], constructors + constexpr expected(); + constexpr expected(expected const &); + constexpr expected(expected &&) noexcept(/* TODO */ false); + template constexpr explicit(/* TODO */ false) expected(expected const &); + template constexpr explicit(/* TODO */ false) expected(expected &&); + + template constexpr explicit(/* TODO */ false) expected(U &&v); + + template constexpr explicit(/* TODO */ false) expected(unexpected const &); + template constexpr explicit(/* TODO */ false) expected(unexpected &&); + + template constexpr explicit expected(std::in_place_t, Args &&...); + template constexpr explicit expected(std::in_place_t, std::initializer_list, Args &&...); + template constexpr explicit expected(unexpect_t, Args &&...); + template constexpr explicit expected(unexpect_t, std::initializer_list, Args &&...); + + // [expected.object.dtor], destructor + constexpr ~expected(); + + // [expected.object.assign], assignment + constexpr expected &operator=(expected const &); + constexpr expected &operator=(expected &&) noexcept(/* TODO */ false); + template constexpr expected &operator=(U &&); + template constexpr expected &operator=(unexpected const &); + template constexpr expected &operator=(unexpected &&); + + template constexpr T &emplace(Args &&...) noexcept; + template constexpr T &emplace(std::initializer_list, Args &&...) noexcept; + + // [expected.object.swap], swap + constexpr void swap(expected &) noexcept(/* TODO */ false); + constexpr friend void swap(expected &x, expected &y) noexcept(noexcept(x.swap(y))); + + // [expected.object.obs], observers + constexpr T const *operator->() const noexcept; + constexpr T *operator->() noexcept; + constexpr T const &operator*() const & noexcept; + constexpr T &operator*() & noexcept; + constexpr T const &&operator*() const && noexcept; + constexpr T &&operator*() && noexcept; + constexpr explicit operator bool() const noexcept; + constexpr bool has_value() const noexcept; + constexpr T const &value() const &; // freestanding-deleted + constexpr T &value() &; // freestanding-deleted + constexpr T const &&value() const &&; // freestanding-deleted + constexpr T &&value() &&; // freestanding-deleted + constexpr E const &error() const & noexcept; + constexpr E &error() & noexcept; + constexpr E const &&error() const && noexcept; + constexpr E &&error() && noexcept; + template constexpr T value_or(U &&) const &; + template constexpr T value_or(U &&) &&; + template constexpr E error_or(G &&) const &; + template constexpr E error_or(G &&) &&; + + // [expected.object.monadic], monadic operations + template constexpr auto and_then(F &&f) &; + template constexpr auto and_then(F &&f) &&; + template constexpr auto and_then(F &&f) const &; + template constexpr auto and_then(F &&f) const &&; + template constexpr auto or_else(F &&f) &; + template constexpr auto or_else(F &&f) &&; + template constexpr auto or_else(F &&f) const &; + template constexpr auto or_else(F &&f) const &&; + template constexpr auto transform(F &&f) &; + template constexpr auto transform(F &&f) &&; + template constexpr auto transform(F &&f) const &; + template constexpr auto transform(F &&f) const &&; + template constexpr auto transform_error(F &&f) &; + template constexpr auto transform_error(F &&f) &&; + template constexpr auto transform_error(F &&f) const &; + template constexpr auto transform_error(F &&f) const &&; + + // [expected.object.eq], equality operators + template + requires(!std::is_void_v) + constexpr friend bool operator==(expected const &x, expected const &y); + template constexpr friend bool operator==(expected const &, const T2 &); + template constexpr friend bool operator==(expected const &, unexpected const &); + +private: + union { + T v_; + E e_; + }; + bool set_; +}; + +template + requires std::is_void_v +class expected { +public: + using value_type = T; + using error_type = E; + using unexpected_type = unexpected; + + template using rebind = expected; + + // [expected.void.cons], constructors + constexpr expected() noexcept; + constexpr expected(expected const &); + constexpr expected(expected &&) noexcept(/* TODO */ false); + template constexpr explicit(/* TODO */ false) expected(expected const &); + template constexpr explicit(/* TODO */ false) expected(expected &&); + + template constexpr explicit(/* TODO */ false) expected(unexpected const &); + template constexpr explicit(/* TODO */ false) expected(unexpected &&); + + constexpr explicit expected(std::in_place_t) noexcept; + template constexpr explicit expected(unexpect_t, Args &&...); + template constexpr explicit expected(unexpect_t, std::initializer_list, Args &&...); + + // [expected.void.dtor], destructor + constexpr ~expected(); + + // [expected.void.assign], assignment + constexpr expected &operator=(expected const &); + constexpr expected &operator=(expected &&) noexcept(/* TODO */ false); + template constexpr expected &operator=(unexpected const &); + template constexpr expected &operator=(unexpected &&); + constexpr void emplace() noexcept; + + // [expected.void.swap], swap + constexpr void swap(expected &) noexcept(/* TODO */ false); + constexpr friend void swap(expected &x, expected &y) noexcept(noexcept(x.swap(y))); + + // [expected.void.obs], observers + constexpr explicit operator bool() const noexcept; + constexpr bool has_value() const noexcept; + constexpr void operator*() const noexcept; + constexpr void value() const &; // freestanding-deleted + constexpr void value() &&; // freestanding-deleted + constexpr E const &error() const & noexcept; + constexpr E &error() & noexcept; + constexpr E const &&error() const && noexcept; + constexpr E &&error() && noexcept; + template constexpr E error_or(G &&) const &; + template constexpr E error_or(G &&) &&; + + // [expected.void.monadic], monadic operations + template constexpr auto and_then(F &&f) &; + template constexpr auto and_then(F &&f) &&; + template constexpr auto and_then(F &&f) const &; + template constexpr auto and_then(F &&f) const &&; + template constexpr auto or_else(F &&f) &; + template constexpr auto or_else(F &&f) &&; + template constexpr auto or_else(F &&f) const &; + template constexpr auto or_else(F &&f) const &&; + template constexpr auto transform(F &&f) &; + template constexpr auto transform(F &&f) &&; + template constexpr auto transform(F &&f) const &; + template constexpr auto transform(F &&f) const &&; + template constexpr auto transform_error(F &&f) &; + template constexpr auto transform_error(F &&f) &&; + template constexpr auto transform_error(F &&f) const &; + template constexpr auto transform_error(F &&f) const &&; + + // [expected.void.eq], equality operators + template + requires std::is_void_v + constexpr friend bool operator==(expected const &x, expected const &y); + template constexpr friend bool operator==(expected const &, unexpected const &); + +private: + union { + unsigned char dummy_; + E e_; + }; + bool set_; +}; + +} // namespace fn::detail + +#endif // INCLUDE_PFN_EXPECTED diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e6db075a..ced705b2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,7 +13,6 @@ add_library(tests_util INTERFACE ${TESTS_UTIL_SOURCES}) target_include_directories(tests_util INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(tests_util INTERFACE include_fn) - # Pls keep the filenames sorted set(TESTS_FN_SOURCES fn/detail/fwd_macro.cpp @@ -40,38 +39,40 @@ set(TESTS_FN_SOURCES fn/value_or.cpp ) +set(TESTS_PFN_SOURCES + pfn/expected.cpp +) + include(TargetGenerator) # Generate separate target for each individual test source -foreach(source IN ITEMS ${TESTS_FN_SOURCES}) - string(REGEX REPLACE "^fn/(detail|)/?([^\.]+)\.cpp$" "\\1\\2" root_name ${source}) +foreach(source IN ITEMS ${TESTS_FN_SOURCES} ${TESTS_PFN_SOURCES}) + string(REGEX REPLACE "^(fn|pfn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) + string(REGEX REPLACE "^(fn|pfn)/.*$" "\\1" subdir ${source}) setup_target_for_file( - NAME "tests_fn_${root_name}" + NAME "tests_${root_name}" SOURCE "${source}" - DEPENDENCIES include_fn tests_util Catch2::Catch2WithMain - COMPILE_OPTIONS -Wall -Wextra -Wpedantic -Wno-missing-braces "$<$:-O0>" + DEPENDENCIES "include_${subdir}" tests_util Catch2::Catch2WithMain + COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" TEST_OPTIONS -r console - TEST_LABELS tests_fn ${root_name} + TEST_LABELS "tests_${subdir}" ${root_name} ADD_TEST ) unset(root_name) endforeach() -# Pls keep the filenames sorted +# TODO change examples into subproject and switch to add_subdirectory set(TESTS_EXAMPLES_SOURCES examples/simple.cpp ) -foreach(source IN ITEMS ${TESTS_EXAMPLES_SOURCES}) - string(REGEX REPLACE "^examples/([^\.]+)\.cpp$" "\\1" root_name ${source}) - setup_target_for_file( - NAME "tests_examples_${root_name}" - SOURCE "${source}" - DEPENDENCIES include_fn Catch2::Catch2WithMain - COMPILE_OPTIONS -Wall -Wextra -Wpedantic -Wno-missing-braces - TEST_OPTIONS -r console - TEST_LABELS tests_examples ${root_name} - ADD_TEST - ) - unset(root_name) -endforeach() +add_executable(tests_examples ${TESTS_EXAMPLES_SOURCES}) +target_link_libraries(tests_examples include_fn Catch2::Catch2WithMain) +target_compile_options(tests_examples PRIVATE -Wall -Wextra -Wpedantic) + +add_test( + NAME tests_examples + COMMAND tests_examples -r console + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} +) +set_property(TEST tests_examples PROPERTY LABELS tests_examples) diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp new file mode 100644 index 00000000..036486d3 --- /dev/null +++ b/tests/pfn/expected.cpp @@ -0,0 +1,18 @@ +// Copyright (c) 2025 Bronek Kozicki +// +// Distributed under the ISC License. See accompanying file LICENSE.md +// or copy at https://opensource.org/licenses/ISC + +#include + +#include + +enum Error { unknown }; + +TEST_CASE("expected_polyfill", "[expected][polyfill][cxx20compat]") +{ + using T = fn::detail::expected; + constexpr auto a = sizeof(T); + static_assert(a >= std::max(sizeof(Error), sizeof(int))); + SUCCEED(); +} From 721f0261f8ffd0672c014706d245eb6cfa44ec67 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 19 Jan 2025 17:42:28 +0000 Subject: [PATCH 03/46] Add some basic polyfills with C++20 tests --- include/pfn/expected.hpp | 34 ++++++----- tests/CMakeLists.txt | 16 +++++ tests/pfn/expected.cpp | 126 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 155 insertions(+), 21 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 2f95dd53..95b50a82 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -10,31 +10,35 @@ #include #include -namespace fn::detail { +namespace pfn { template class bad_expected_access; template <> class bad_expected_access : public std::exception { protected: - bad_expected_access() noexcept; - bad_expected_access(bad_expected_access const &) noexcept; - bad_expected_access(bad_expected_access &&) noexcept; - bad_expected_access &operator=(bad_expected_access const &) noexcept; - bad_expected_access &operator=(bad_expected_access &&) noexcept; - ~bad_expected_access(); + bad_expected_access() noexcept = default; + bad_expected_access(bad_expected_access const &) noexcept = default; + bad_expected_access(bad_expected_access &&) noexcept = default; + bad_expected_access &operator=(bad_expected_access const &) noexcept = default; + bad_expected_access &operator=(bad_expected_access &&) noexcept = default; + ~bad_expected_access() noexcept = default; public: - [[nodiscard]] char const *what() const noexcept override; + [[nodiscard]] char const *what() const noexcept override + { + static char const msg_[] = "bad access to expected without expected value"; + return msg_; + } }; template class bad_expected_access : public bad_expected_access { public: - explicit bad_expected_access(E); - [[nodiscard]] char const *what() const noexcept override; - E &error() & noexcept; - E const &error() const & noexcept; - E &&error() && noexcept; - E const &&error() const && noexcept; + explicit bad_expected_access(E e) : e_(std::move(e)) {} + [[nodiscard]] char const *what() const noexcept override { return bad_expected_access::what(); }; + E &error() & noexcept { return e_; } + E const &error() const & noexcept { return e_; } + E &&error() && noexcept { return std::move(e_); } + E const &&error() const && noexcept { return std::move(e_); } private: E e_; @@ -261,6 +265,6 @@ class expected { bool set_; }; -} // namespace fn::detail +} // namespace pfn #endif // INCLUDE_PFN_EXPECTED diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ced705b2..8ea0cac2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -61,6 +61,22 @@ foreach(source IN ITEMS ${TESTS_FN_SOURCES} ${TESTS_PFN_SOURCES}) unset(root_name) endforeach() +# Generate C++20 targets for pfn tests +foreach(source IN ITEMS ${TESTS_PFN_SOURCES}) + string(REGEX REPLACE "^(pfn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) + setup_target_for_file( + NAME "tests_${root_name}_cxx20" + SOURCE "${source}" + DEPENDENCIES include_pfn Catch2::Catch2WithMain + COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" + TEST_OPTIONS -r console + TEST_LABELS tests_pfn cxx20 ${root_name} + ADD_TEST + ) + set_property(TARGET "tests_${root_name}_cxx20" PROPERTY CXX_STANDARD 20) + unset(root_name) +endforeach() + # TODO change examples into subproject and switch to add_subdirectory set(TESTS_EXAMPLES_SOURCES examples/simple.cpp diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 036486d3..ab0b6cea 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -7,12 +7,126 @@ #include -enum Error { unknown }; +#include -TEST_CASE("expected_polyfill", "[expected][polyfill][cxx20compat]") +enum Error { unknown, secret = 142, mystery = 176 }; + +TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") +{ + SECTION("bad_expected_access") + { + struct A : pfn::bad_expected_access {}; + + static_assert(noexcept(A{})); + A a; + static_assert(noexcept(A{a})); + static_assert(noexcept(A{std::move(a)})); + static_assert(noexcept(a.what())); + static_assert(std::is_same_v); + SECTION("constructors and assignment") + { + A a1 = [&]() -> A & { return a; }(); + CHECK(a.what() == a1.what()); + A a2 = [&]() -> A && { return std::move(a); }(); + CHECK(a.what() == a2.what()); + A a3 = [&]() -> A const & { return a; }(); + CHECK(a.what() == a3.what()); + A a4 = [&]() -> A const && { return std::move(a); }(); + CHECK(a.what() == a4.what()); + + a = [&]() -> A & { return a; }(); + CHECK(A{}.what() == a.what()); + a = [&]() -> A && { return std::move(a); }(); + CHECK(A{}.what() == a.what()); + a = [&]() -> A const & { return a; }(); + CHECK(A{}.what() == a.what()); + a = [&]() -> A const && { return std::move(a); }(); + CHECK(A{}.what() == a.what()); + } + CHECK(std::strcmp(a.what(), "bad access to expected without expected value") == 0); + + A const b; + CHECK(&decltype(a)::what == &decltype(b)::what); + CHECK(a.what() == b.what()); + + static_assert(noexcept(A{b})); + static_assert(noexcept(A{std::move(b)})); + static_assert(noexcept(b.what())); + static_assert(std::is_same_v); + } + + SECTION("bad_expected_access") + { + using A = pfn::bad_expected_access; + static_assert(std::is_base_of_v, A>); + + A a{secret}; + static_assert(noexcept(A{a})); + static_assert(noexcept(A{std::move(a)})); + static_assert(noexcept(a.what())); + SECTION("constructors and assignment") + { + A a1 = [&]() -> A & { return a; }(); + CHECK(a1.error() == secret); + A a2 = [&]() -> A && { return std::move(a); }(); + CHECK(a2.error() == secret); + A a3 = [&]() -> A const & { return a; }(); + CHECK(a3.error() == secret); + A a4 = [&]() -> A const && { return std::move(a); }(); + CHECK(a4.error() == secret); + + a = [&]() -> A & { return a; }(); + CHECK(a.error() == secret); + a = [&]() -> A && { return std::move(a); }(); + CHECK(a.error() == secret); + a = [&]() -> A const & { return a; }(); + CHECK(a.error() == secret); + a = [&]() -> A const && { return std::move(a); }(); + CHECK(a.error() == secret); + } + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + CHECK(a.error() == secret); + CHECK(std::move(a).error() == secret); + + A const b{mystery}; + static_assert(noexcept(A{b})); + static_assert(noexcept(A{std::move(b)})); + static_assert(noexcept(b.what())); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + CHECK(b.error() == mystery); + CHECK(std::move(b).error() == mystery); + + CHECK(std::strcmp(a.what(), "bad access to expected without expected value") == 0); + CHECK(a.what() == b.what()); + auto const c = []() { + struct C : pfn::bad_expected_access {}; + return C{}; + }(); + CHECK(a.what() == c.what()); + CHECK(&decltype(a)::what == &decltype(b)::what); + } +} + +namespace test { +template struct A {}; +} // namespace test + +TEST_CASE("unexpect", "[expected][polyfill][unexpect]") { - using T = fn::detail::expected; - constexpr auto a = sizeof(T); - static_assert(a >= std::max(sizeof(Error), sizeof(int))); - SUCCEED(); + SECTION("unexpect") + { + static_assert(std::is_empty_v); + static_assert(noexcept(pfn::unexpect_t{})); + static_assert(std::is_same_v); + // pfn::unexpect can be used as a NTTP + static_assert(std::is_empty_v>); + static constexpr auto a = pfn::unexpect; + static_assert(std::is_empty_v>); + static_assert(std::is_same_v); + static_assert(std::is_same_v, test::A>); + } } From 1090cb4f60fdcba97aed572accd0b834365cf400 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 19 Jan 2025 17:51:17 +0000 Subject: [PATCH 04/46] Add pfn tests to coverage reporting --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c3e7966f..7985eb35 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -75,7 +75,7 @@ jobs: run: | cd .build cmake --build . - ctest -L tests_fn -j1 # generate .gcda files + ctest -L 'tests_p?fn' -j1 # generate .gcda files $GCOV -pbc -r -s $( realpath .. ) $( find tests -type f -name '*.gcno' ) # generate .gcov files - name: Upload .gcov files From d784fbd207ca107d28378f946287a79fd91eb6e1 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 19 Jan 2025 21:50:14 +0000 Subject: [PATCH 05/46] Add C++20 tests to workflow --- .github/workflows/build.yml | 62 ++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 884013f3..aaab566c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ on: - 'cmake/**' jobs: - build-linux: + build-cxx23: runs-on: group: default strategy: @@ -80,3 +80,63 @@ jobs: run: | cd .build ctest --output-on-failure + + build-cxx20: + runs-on: + group: default + strategy: + fail-fast: false + matrix: + configuration: + - Debug + - Release + compiler: + - gcc:12 + - gcc:13 + - gcc:14 + - clang:16 + - clang:17 + - clang:18 + - clang:19 + container: libfn/ci-build-${{ matrix.compiler }} + steps: + - uses: actions/checkout@v4 + + - name: Verify compiler compatibility + env: + SOURCE: | + #include + int main() { + using type=std::optional; + constexpr type a{std::in_place, 1}; + return a.value() - 1; + } + run: | + printf "CXX=%s\nCXXFLAGS=%s\n" "$CXX" "$CXXFLAGS" + $CXX --version | head -1 + FILE=$(mktemp --tmpdir XXXXXX.cpp) + printf "$SOURCE\n" > $FILE + OUT=$(mktemp --tmpdir XXXXXX) + $CXX -std=c++2a $CXXFLAGS -Wall $FILE -o $OUT + $OUT + + - name: Prepare build + run: | + mkdir .build + cd .build + cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. + COMPILER=$( grep -E "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) + FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS:STRING=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) + printf "C++ compiler: %s\n" "$COMPILER" + printf "C++ compiler version: %s\n" "$( $COMPILER --version | head -1 )" + printf "C++ compilation options: %s\n" "$FLAGS" + + - name: Build all + run: | + cd .build + cmake --build . --target $(ctest -L cxx20 --show-only=json-v1 | jq ".tests[].name" | tr -d '"') + + - name: Run tests + run: | + cd .build + ctest -L cxx20 --output-on-failure From f94cb5fdedb638dc949450f063c8f2ab89f3ad79 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Mon, 20 Jan 2025 15:09:16 +0000 Subject: [PATCH 06/46] Add phony target cxx20, simplify TargetGenerator.cmake --- .github/workflows/build.yml | 2 +- CMakeLists.txt | 4 ++++ cmake/TargetGenerator.cmake | 27 +++---------------------- include/CMakeLists.txt | 9 ++++++--- tests/CMakeLists.txt | 39 ++++++++++++++++++++++++++----------- 5 files changed, 42 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aaab566c..076d05a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -134,7 +134,7 @@ jobs: - name: Build all run: | cd .build - cmake --build . --target $(ctest -L cxx20 --show-only=json-v1 | jq ".tests[].name" | tr -d '"') + cmake --build . --target cxx20 - name: Run tests run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index bf367450..7fe08468 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,10 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") ) endif() +# Phony for all C++20 targets; the same label is set on all such tests +# Targets and tests are expected to have a `_cxx20` suffix at the end of name +add_custom_target(cxx20) + add_subdirectory(include) if(TESTS) diff --git a/cmake/TargetGenerator.cmake b/cmake/TargetGenerator.cmake index 258ae29f..ccd5bb4b 100644 --- a/cmake/TargetGenerator.cmake +++ b/cmake/TargetGenerator.cmake @@ -1,23 +1,14 @@ # Generate target for a given source file, and optionally add it to tests -function(setup_target_for_file) - set(options ADD_TEST) +function(create_target_for_file) set(oneValueArgs NAME SOURCE SOURCE_ROOT NEW_SOURCE) - set(multiValueArgs DEPENDENCIES COMPILE_OPTIONS TEST_OPTIONS TEST_LABELS) - cmake_parse_arguments(Generator "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + set(multiValueArgs DEPENDENCIES COMPILE_OPTIONS) + cmake_parse_arguments(Generator "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(NOT DEFINED Generator_SOURCE) message(FATAL_ERROR "SOURCE must be set") endif() - if(DEFINED Generator_TEST_OPTIONS AND (NOT Generator_ADD_TEST)) - message(FATAL_ERROR "ADD_TEST must be set if TEST_OPTIONS is set") - endif() - - if(DEFINED Generator_TEST_LABELS AND (NOT Generator_ADD_TEST)) - message(FATAL_ERROR "ADD_TEST must be set if TEST_LABELS is set") - endif() - if(NOT DEFINED Generator_NAME) string(REGEX REPLACE "[\\./]" "-" Generator_NAME ${Generator_SOURCE}) endif() @@ -38,16 +29,4 @@ function(setup_target_for_file) target_link_libraries(${Generator_NAME} ${Generator_DEPENDENCIES}) target_compile_options(${Generator_NAME} PRIVATE ${Generator_COMPILE_OPTIONS}) - - if(Generator_ADD_TEST) - add_test( - NAME ${Generator_NAME} - COMMAND ${Generator_NAME} ${Generator_TEST_OPTIONS} - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - ) - - if(Generator_TEST_LABELS) - set_property(TEST ${Generator_NAME} PROPERTY LABELS "${Generator_TEST_LABELS}") - endif() - endif() endfunction() diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index 2fe4e0d3..8d3b52f1 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -16,7 +16,10 @@ target_sources(include_pfn INTERFACE TYPE HEADERS FILES ${INCLUDE_PFN_HEADERS}) target_include_directories(include_pfn INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_compile_options(include_pfn INTERFACE -Wno-non-template-friend) + +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_compile_options(include_pfn INTERFACE -Wno-non-template-friend) +endif() install(TARGETS include_pfn FILE_SET include_pfn_headers @@ -73,13 +76,13 @@ include(TargetGenerator) foreach(source IN ITEMS ${INCLUDE_FN_HEADERS} ${INCLUDE_PFN_HEADERS}) string(REGEX REPLACE "^(fn|pfn)/(detail|)/?([^\.]+)\.hpp$" "\\1_\\2\\3" root_name ${source}) string(REGEX REPLACE "^(fn|pfn)/.*$" "\\1" subdir ${source}) - setup_target_for_file( + create_target_for_file( NAME "sentinel_${root_name}" SOURCE "${source}" NEW_SOURCE "#include <${source}>\nint main() {}\n" SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" DEPENDENCIES "include_${subdir}" - COMPILE_OPTIONS -Wall -Wextra -Wpedantic -Wno-missing-braces + COMPILE_OPTIONS -Wall -Wextra -Wpedantic ) unset(root_name) endforeach() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8ea0cac2..a1f00d74 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -49,31 +49,48 @@ include(TargetGenerator) foreach(source IN ITEMS ${TESTS_FN_SOURCES} ${TESTS_PFN_SOURCES}) string(REGEX REPLACE "^(fn|pfn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) string(REGEX REPLACE "^(fn|pfn)/.*$" "\\1" subdir ${source}) - setup_target_for_file( - NAME "tests_${root_name}" + set(target "tests_${root_name}") + + create_target_for_file( + NAME "${target}" SOURCE "${source}" DEPENDENCIES "include_${subdir}" tests_util Catch2::Catch2WithMain COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" - TEST_OPTIONS -r console - TEST_LABELS "tests_${subdir}" ${root_name} - ADD_TEST ) + + add_test( + NAME "${target}" + COMMAND "${target}" -r console + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + set_property(TEST "${target}" PROPERTY LABELS "tests_${subdir}" ${root_name}) + + unset(target) unset(root_name) endforeach() # Generate C++20 targets for pfn tests foreach(source IN ITEMS ${TESTS_PFN_SOURCES}) string(REGEX REPLACE "^(pfn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) - setup_target_for_file( - NAME "tests_${root_name}_cxx20" + set(target "tests_${root_name}_cxx20") + + create_target_for_file( + NAME "${target}" SOURCE "${source}" DEPENDENCIES include_pfn Catch2::Catch2WithMain COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" - TEST_OPTIONS -r console - TEST_LABELS tests_pfn cxx20 ${root_name} - ADD_TEST ) - set_property(TARGET "tests_${root_name}_cxx20" PROPERTY CXX_STANDARD 20) + add_dependencies(cxx20 "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD 20) + + add_test( + NAME "${target}" + COMMAND "${target}" -r console + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + set_property(TEST "${target}" PROPERTY LABELS "tests_${subdir}" cxx20 ${root_name}) + + unset(target) unset(root_name) endforeach() From 7fe38199c173256ec46536088a04a60cab8862a3 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 25 Jan 2025 20:21:41 +0000 Subject: [PATCH 07/46] Separate phony cxx20 and cxx23 --- .github/workflows/build.yml | 4 +- CMakeLists.txt | 5 +- include/CMakeLists.txt | 67 +++++++++++++----- tests/CMakeLists.txt | 135 ++++++++++++++++++++---------------- 4 files changed, 132 insertions(+), 79 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 076d05a4..d8156b2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,12 +74,12 @@ jobs: - name: Build all run: | cd .build - cmake --build . --target all + cmake --build . --target cxx23 - name: Run tests run: | cd .build - ctest --output-on-failure + ctest -L cxx23 --output-on-failure build-cxx20: runs-on: diff --git a/CMakeLists.txt b/CMakeLists.txt index 7fe08468..071aff63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,7 +33,6 @@ endif() include(Ccache) set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") @@ -45,9 +44,9 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") ) endif() -# Phony for all C++20 targets; the same label is set on all such tests -# Targets and tests are expected to have a `_cxx20` suffix at the end of name +# Phony targets for all supported C++ versions add_custom_target(cxx20) +add_custom_target(cxx23) add_subdirectory(include) diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index 8d3b52f1..be7b733e 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -2,20 +2,22 @@ cmake_minimum_required(VERSION 3.25) project(include) set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) +### include/pfn + # Pls keep the filenames sorted set(INCLUDE_PFN_HEADERS pfn/expected.hpp ) add_library(include_pfn INTERFACE) +set_property(TARGET include_pfn PROPERTY CXX_STANDARD 20) target_sources(include_pfn INTERFACE FILE_SET include_pfn_headers TYPE HEADERS FILES ${INCLUDE_PFN_HEADERS}) -target_include_directories(include_pfn INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(include_pfn SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") target_compile_options(include_pfn INTERFACE -Wno-non-template-friend) @@ -25,6 +27,32 @@ install(TARGETS include_pfn FILE_SET include_pfn_headers DESTINATION include) +include(TargetGenerator) + +# Generate sentinel target for each individual header, as a basic sanity check +foreach(cxxrel 20 23) + foreach(source IN ITEMS ${INCLUDE_PFN_HEADERS}) + string(REGEX REPLACE "^(pfn)/(detail|)/?([^\.]+)\.hpp$" "\\1_\\2\\3" root_name ${source}) + set(target "sentinel_${root_name}_cxx${cxxrel}") + + create_target_for_file( + NAME "${target}" + SOURCE "${source}" + NEW_SOURCE "#include <${source}>\nint main() {}\n" + SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" + DEPENDENCIES include_pfn + COMPILE_OPTIONS -Wall -Wextra -Wpedantic + ) + add_dependencies("cxx${cxxrel}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") + + unset(target) + unset(root_name) + endforeach() +endforeach() + +### include/fn + # Pls keep the filenames sorted set(INCLUDE_FN_HEADERS fn/detail/functional.hpp @@ -57,6 +85,7 @@ set(INCLUDE_FN_HEADERS ) add_library(include_fn INTERFACE) +set_property(TARGET include_fn PROPERTY CXX_STANDARD 23) target_sources(include_fn INTERFACE FILE_SET include_fn_headers TYPE HEADERS @@ -70,19 +99,25 @@ install(TARGETS include_fn FILE_SET include_fn_headers DESTINATION include) -include(TargetGenerator) - # Generate sentinel target for each individual header, as a basic sanity check -foreach(source IN ITEMS ${INCLUDE_FN_HEADERS} ${INCLUDE_PFN_HEADERS}) - string(REGEX REPLACE "^(fn|pfn)/(detail|)/?([^\.]+)\.hpp$" "\\1_\\2\\3" root_name ${source}) - string(REGEX REPLACE "^(fn|pfn)/.*$" "\\1" subdir ${source}) - create_target_for_file( - NAME "sentinel_${root_name}" - SOURCE "${source}" - NEW_SOURCE "#include <${source}>\nint main() {}\n" - SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" - DEPENDENCIES "include_${subdir}" - COMPILE_OPTIONS -Wall -Wextra -Wpedantic - ) - unset(root_name) +# TODO add 20 when we are compatible with C++20 +foreach(cxxrel 23) + foreach(source IN ITEMS ${INCLUDE_FN_HEADERS}) + string(REGEX REPLACE "^(fn)/(detail|)/?([^\.]+)\.hpp$" "\\1_\\2\\3" root_name ${source}) + set(target "sentinel_${root_name}_cxx${cxxrel}") + + create_target_for_file( + NAME "${target}" + SOURCE "${source}" + NEW_SOURCE "#include <${source}>\nint main() {}\n" + SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" + DEPENDENCIES include_fn + COMPILE_OPTIONS -Wall -Wextra -Wpedantic + ) + add_dependencies("cxx${cxxrel}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") + + unset(target) + unset(root_name) + endforeach() endforeach() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a1f00d74..97619f46 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,7 +2,6 @@ cmake_minimum_required(VERSION 3.25) project(tests) set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Pls keep the filenames sorted @@ -10,9 +9,47 @@ set(TESTS_UTIL_SOURCES util/static_check.hpp ) add_library(tests_util INTERFACE ${TESTS_UTIL_SOURCES}) +set_property(TARGET include_fn PROPERTY CXX_STANDARD 23) target_include_directories(tests_util INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(tests_util INTERFACE include_fn) +### tests/pfn + +set(TESTS_PFN_SOURCES + pfn/expected.cpp +) + +include(TargetGenerator) + +# Generate separate target for each individual test source +foreach(cxxrel 20 23) + foreach(source IN ITEMS ${TESTS_PFN_SOURCES}) + string(REGEX REPLACE "^(pfn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) + set(target "tests_${root_name}_cxx${cxxrel}") + + create_target_for_file( + NAME "${target}" + SOURCE "${source}" + DEPENDENCIES include_pfn Catch2::Catch2WithMain + COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" + ) + add_dependencies("cxx${cxxrel}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") + + add_test( + NAME "${target}" + COMMAND "${target}" -r console + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + set_property(TEST "${target}" PROPERTY LABELS tests_pfn "cxx${cxxrel}" "${root_name}") + + unset(target) + unset(root_name) + endforeach() +endforeach() + +### tests/fn + # Pls keep the filenames sorted set(TESTS_FN_SOURCES fn/detail/fwd_macro.cpp @@ -39,73 +76,55 @@ set(TESTS_FN_SOURCES fn/value_or.cpp ) -set(TESTS_PFN_SOURCES - pfn/expected.cpp -) - -include(TargetGenerator) - # Generate separate target for each individual test source -foreach(source IN ITEMS ${TESTS_FN_SOURCES} ${TESTS_PFN_SOURCES}) - string(REGEX REPLACE "^(fn|pfn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) - string(REGEX REPLACE "^(fn|pfn)/.*$" "\\1" subdir ${source}) - set(target "tests_${root_name}") - - create_target_for_file( - NAME "${target}" - SOURCE "${source}" - DEPENDENCIES "include_${subdir}" tests_util Catch2::Catch2WithMain - COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" - ) - - add_test( - NAME "${target}" - COMMAND "${target}" -r console - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - ) - set_property(TEST "${target}" PROPERTY LABELS "tests_${subdir}" ${root_name}) - - unset(target) - unset(root_name) +# TODO add 20 when we are compatible with C++20 +foreach(cxxrel 23) + foreach(source IN ITEMS ${TESTS_FN_SOURCES}) + string(REGEX REPLACE "^(fn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) + set(target "tests_${root_name}_cxx${cxxrel}") + + create_target_for_file( + NAME "${target}" + SOURCE "${source}" + DEPENDENCIES include_fn tests_util Catch2::Catch2WithMain + COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" + ) + add_dependencies("cxx${cxxrel}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") + + add_test( + NAME "${target}" + COMMAND "${target}" -r console + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + set_property(TEST "${target}" PROPERTY LABELS tests_fn "cxx${cxxrel}" "${root_name}") + + unset(target) + unset(root_name) + endforeach() endforeach() -# Generate C++20 targets for pfn tests -foreach(source IN ITEMS ${TESTS_PFN_SOURCES}) - string(REGEX REPLACE "^(pfn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) - set(target "tests_${root_name}_cxx20") +# TODO change examples into subproject and switch to add_subdirectory +set(TESTS_EXAMPLES_SOURCES + examples/simple.cpp +) - create_target_for_file( - NAME "${target}" - SOURCE "${source}" - DEPENDENCIES include_pfn Catch2::Catch2WithMain - COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" - ) - add_dependencies(cxx20 "${target}") - set_property(TARGET "${target}" PROPERTY CXX_STANDARD 20) +# TODO add 20 when we are compatible with C++20 +foreach(cxxrel 23) + set(target "tests_examples_cxx${cxxrel}") + + add_executable("${target}" ${TESTS_EXAMPLES_SOURCES}) + target_link_libraries("${target}" include_fn Catch2::Catch2WithMain) + target_compile_options("${target}" PRIVATE -Wall -Wextra -Wpedantic) + add_dependencies("cxx${cxxrel}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") add_test( NAME "${target}" COMMAND "${target}" -r console WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) - set_property(TEST "${target}" PROPERTY LABELS "tests_${subdir}" cxx20 ${root_name}) + set_property(TEST "${target}" PROPERTY LABELS tests_examples "cxx${cxxrel}") unset(target) - unset(root_name) endforeach() - -# TODO change examples into subproject and switch to add_subdirectory -set(TESTS_EXAMPLES_SOURCES - examples/simple.cpp -) - -add_executable(tests_examples ${TESTS_EXAMPLES_SOURCES}) -target_link_libraries(tests_examples include_fn Catch2::Catch2WithMain) -target_compile_options(tests_examples PRIVATE -Wall -Wextra -Wpedantic) - -add_test( - NAME tests_examples - COMMAND tests_examples -r console - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} -) -set_property(TEST tests_examples PROPERTY LABELS tests_examples) From b595920f9f52decddfea0496fe7e4d4b050f8d28 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 25 Jan 2025 20:31:58 +0000 Subject: [PATCH 08/46] Add --target all to build workflow --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8156b2c..3dfd8396 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,9 +72,12 @@ jobs: printf "C++ compilation options: %s\n" "$FLAGS" - name: Build all + env: + # We want to build "--target all" for at least one arbitrary configuration + TARGET: ${{ ( matrix.configuration == 'Debug' && matrix.compiler == 'gcc:14' ) && 'all' || 'cxx23' }} run: | cd .build - cmake --build . --target cxx23 + cmake --build . --target ${TARGET} - name: Run tests run: | From 89f3e45e09aad7fcc58796b3b8b71954d8c43c3e Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 25 Jan 2025 20:45:33 +0000 Subject: [PATCH 09/46] Add pfn::unexpected, needs unit tests --- include/pfn/expected.hpp | 85 +++++++++++++++++++++++++++++++++++----- tests/pfn/expected.cpp | 6 +++ 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 95b50a82..4bc7d4d8 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -7,9 +7,19 @@ #define INCLUDE_PFN_EXPECTED #include +#include #include #include +#ifdef FWD +#pragma push_macro("FWD") +#define INCLUDE_PFN_EXPECTED__POP_FWD +#undef FWD +#endif + +// Also defined in fn/detail/fwd_macro.hpp but pfn/* headers are standalone +#define FWD(...) static_cast(__VA_ARGS__) + namespace pfn { template class bad_expected_access; @@ -49,28 +59,77 @@ struct unexpect_t { }; constexpr inline unexpect_t unexpect{}; +template class unexpected; + +namespace detail { +template constexpr bool _is_some_unexpected = false; +template constexpr bool _is_some_unexpected<::pfn::unexpected> = true; + +template +constexpr bool _is_valid_unexpected = // + ::std::is_object_v // i.e. not a reference or void or function + && not ::std::is_array_v // + && not _is_some_unexpected // + && not ::std::is_const_v // + && not ::std::is_volatile_v; +} // namespace detail + template class unexpected { + static_assert(detail::_is_valid_unexpected); + public: constexpr unexpected(unexpected const &) = default; constexpr unexpected(unexpected &&) = default; - template constexpr explicit unexpected(Err &&); - template constexpr explicit unexpected(std::in_place_t, Args &&...); + + template + constexpr explicit unexpected(Err &&e) noexcept(::std::is_nothrow_constructible_v) + requires(not ::std::is_same_v<::std::remove_cvref_t, unexpected> && // + not ::std::is_same_v<::std::remove_cvref_t, ::std::in_place_t> && // + ::std::is_constructible_v) + : e_(FWD(e)) + { + } + + template + constexpr explicit unexpected(std::in_place_t, Args &&...a) noexcept(::std::is_nothrow_constructible_v) + requires ::std::is_constructible_v + : e_(FWD(a)...) + { + } + template - constexpr explicit unexpected(std::in_place_t, std::initializer_list, Args &&...); + constexpr explicit unexpected(std::in_place_t, std::initializer_list i, Args &&...a) noexcept( + ::std::is_nothrow_constructible_v &, Args...>) + requires ::std::is_constructible_v &, Args...> + : e_(i, FWD(a)...) + { + } constexpr unexpected &operator=(unexpected const &) = default; constexpr unexpected &operator=(unexpected &&) = default; - constexpr E const &error() const & noexcept; - constexpr E &error() & noexcept; - constexpr E const &&error() const && noexcept; - constexpr E &&error() && noexcept; + constexpr E const &error() const & noexcept { return e_; }; + constexpr E &error() & noexcept { return e_; }; + constexpr E const &&error() const && noexcept { return ::std::move(e_); }; + constexpr E &&error() && noexcept { return ::std::move(e_); }; - constexpr void swap(unexpected &other) noexcept(/* TODO */ false); + constexpr void swap(unexpected &other) noexcept(::std::is_nothrow_swappable_v) + { + static_assert(::std::is_swappable_v); + using ::std::swap; + swap(e_, other.e_); + } - template constexpr friend bool operator==(unexpected const &, unexpected const &); + template constexpr friend bool operator==(unexpected const &x, unexpected const &y) + { + return x.e_ == y.e_; + } - constexpr friend void swap(unexpected &x, unexpected &y) noexcept(noexcept(x.swap(y))); + constexpr friend void swap(unexpected &x, unexpected &y) noexcept(noexcept(x.swap(y))) + requires ::std::is_swappable_v + { + x.swap(y); + } private: E e_; @@ -267,4 +326,10 @@ class expected { } // namespace pfn +#undef FWD + +#ifdef INCLUDE_PFN_EXPECTED__POP_FWD +#pragma pop_macro("FWD") +#endif + #endif // INCLUDE_PFN_EXPECTED diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index ab0b6cea..3a390a92 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -130,3 +130,9 @@ TEST_CASE("unexpect", "[expected][polyfill][unexpect]") static_assert(std::is_same_v, test::A>); } } + +TEST_CASE("unexpected", "[expected][polyfill][unexpected]") +{ + // TODO + SUCCEED(); +} From 57291ca7cdc2356a6c26ec3fe2232ebde40355f1 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Mon, 27 Jan 2025 21:17:47 +0000 Subject: [PATCH 10/46] Unit tests for unexpected in progress --- tests/pfn/expected.cpp | 60 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 3a390a92..7e8a3e38 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -133,6 +133,62 @@ TEST_CASE("unexpect", "[expected][polyfill][unexpect]") TEST_CASE("unexpected", "[expected][polyfill][unexpected]") { - // TODO - SUCCEED(); + SECTION("is_valid_unexpected") + { + using pfn::detail::_is_valid_unexpected; + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected); + static_assert(not _is_valid_unexpected<::pfn::unexpected>); + static_assert(not _is_valid_unexpected<::pfn::unexpected>); + static_assert(_is_valid_unexpected); + static_assert(_is_valid_unexpected); + static_assert(_is_valid_unexpected>); + SUCCEED(); + } + + SECTION("constructors") + { + using pfn::unexpected; + + SECTION("explicit single parameter") + { + SECTION("constexpr, CTAD") + { + constexpr unexpected c1{Error::mystery}; + static_assert(std::is_same_v const>); + static_assert(c1.error() == Error::mystery); + } + + SECTION("constexpr, no CTAD") + { + constexpr unexpected c1{42}; + static_assert(std::is_same_v const>); + static_assert(c1.error() == 42); + } + + SECTION("no conversion, CTAD") + { + unexpected const c2{Error::secret}; + static_assert(std::is_same_v const>); + CHECK(c2.error() == Error::secret); + } + + SECTION("conversion, no CTAD") + { + unexpected c3(12); + static_assert(std::is_same_v>); + CHECK(c3.error() == 12.0); + } + } + + SECTION("in_place") {} + } } From 11c127871d2252cf2eb7954719fdd38327fc063d Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 1 Feb 2025 20:38:57 +0000 Subject: [PATCH 11/46] More unit tests for unexpected --- tests/pfn/expected.cpp | 292 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 271 insertions(+), 21 deletions(-) diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 7e8a3e38..52694e9d 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -7,6 +7,7 @@ #include +#include #include enum Error { unknown, secret = 142, mystery = 176 }; @@ -131,8 +132,87 @@ TEST_CASE("unexpect", "[expected][polyfill][unexpect]") } } +namespace unxp { +static int witness = 0; + +struct Foo { + int v = {}; + + Foo() = delete; + Foo(Foo &&) = default; + + Foo &operator=(Foo &o) noexcept + { + v = o.v; + witness *= 53; + return *this; + } + + Foo &operator=(Foo const &o) noexcept + { + v = o.v; + witness *= 59; + return *this; + } + + Foo &operator=(Foo &&o) noexcept + { + v = o.v; + witness *= 61; + return *this; + } + + Foo &operator=(Foo const &&o) noexcept + { + v = o.v; + witness *= 67; + return *this; + } + + Foo(int a) noexcept : v(a) { witness += a; } + + Foo(auto &&...a) noexcept + requires(sizeof...(a) > 1 && (std::is_same_v, int> && ...)) + : v((1 * ... * a)) + { + witness += v; + } + + Foo(std::initializer_list l, auto... a) noexcept(false) + requires(std::is_same_v, int> && ...) + : v(init(l, a...)) + { + witness += v; + } + + bool operator==(Foo const &) const noexcept = default; + + static int init(std::initializer_list l, auto &&...a) noexcept(false) + { + double ret = (1 * ... * a); + for (auto d : l) { + if (d == 0.0) + throw std::runtime_error("invalid input"); + ret *= d; + } + return static_cast(ret); + } +}; + +void swap(Foo &l, Foo &r) +{ + std::swap(l.v, r.v); + witness *= 97; +} + +} // namespace unxp + TEST_CASE("unexpected", "[expected][polyfill][unexpected]") { + using pfn::unexpected; + using unxp::Foo; + using unxp::witness; + SECTION("is_valid_unexpected") { using pfn::detail::_is_valid_unexpected; @@ -156,39 +236,209 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SECTION("constructors") { - using pfn::unexpected; + SECTION("constexpr, CTAD") + { + constexpr unexpected c{Error::mystery}; + static_assert(c.error() == Error::mystery); + static_assert(std::is_same_v const>); + static_assert(std::is_nothrow_constructible_v); + SUCCEED(); + } - SECTION("explicit single parameter") + SECTION("constexpr, no CTAD") { - SECTION("constexpr, CTAD") - { - constexpr unexpected c1{Error::mystery}; - static_assert(std::is_same_v const>); - static_assert(c1.error() == Error::mystery); - } + constexpr unexpected c{42}; + static_assert(c.error() == 42); + static_assert(std::is_nothrow_constructible_v); + SUCCEED(); + } + + SECTION("no conversion, CTAD") + { + auto const before = witness; + unexpected c{Foo{2}}; + CHECK(witness == before + 2); + CHECK(c.error() == Foo{2}); + CHECK(c == unexpected{2}); + static_assert(std::is_same_v>); + static_assert(std::is_nothrow_constructible_v); + } + + SECTION("conversion, no CTAD") + { + auto const before = witness; + unexpected c(3); + CHECK(witness == before + 3); + CHECK(c.error().v == 3); + CHECK(c == unexpected{3}); + static_assert(std::is_nothrow_constructible_v); + } + + SECTION("in-place, no CTAD") + { + auto const before = witness; + unexpected c(std::in_place, 3, 5); + CHECK(witness == before + 3 * 5); + CHECK(c.error() == Foo{3, 5}); + CHECK(c == unexpected{15}); + static_assert(std::is_nothrow_constructible_v); + } - SECTION("constexpr, no CTAD") + SECTION("in_place, not CTAD, initializer_list, noexcept(false)") + { + SECTION("forwarded args") { - constexpr unexpected c1{42}; - static_assert(std::is_same_v const>); - static_assert(c1.error() == 42); + auto const before = witness; + unexpected c(std::in_place, {3.0, 5.0}, 7, 11); + auto const d = 3 * 5 * 7 * 11; + CHECK(witness == before + d); + CHECK(c.error() == Foo{d}); + CHECK(c == unexpected{Foo{d}}); + static_assert( + not std::is_nothrow_constructible_v, int, int>); + static_assert(std::is_constructible_v, int, int>); } - SECTION("no conversion, CTAD") + SECTION("no forwarded args") { - unexpected const c2{Error::secret}; - static_assert(std::is_same_v const>); - CHECK(c2.error() == Error::secret); + auto const before = witness; + unexpected c(std::in_place, {2.0, 2.5}); + CHECK(witness == before + 5); + CHECK(c.error() == Foo{5}); + CHECK(c == unexpected{5}); + static_assert(not std::is_nothrow_constructible_v>); + static_assert(std::is_constructible_v>); } - SECTION("conversion, no CTAD") + SECTION("exception thrown") { - unexpected c3(12); - static_assert(std::is_same_v>); - CHECK(c3.error() == 12.0); + unexpected t{13}; + auto const before = witness; + try { + t = unexpected{std::in_place, {2.0, 1.0, 0.0}, 5}; + FAIL(); + } catch (std::runtime_error const &) { + SUCCEED(); + } + CHECK(t.error().v == 13); + CHECK(witness == before); } } + } + + SECTION("accessor") + { + Foo v{1}; + + SECTION("lval") + { + unexpected t{13}; + auto before = witness; + v = t.error(); + CHECK(witness == before * 53); + CHECK(v == Foo{13}); + } + + SECTION("lval const") + { + unexpected const t{17}; + auto before = witness; + v = t.error(); + CHECK(witness == before * 59); + CHECK(v == Foo{17}); + } + + SECTION("rval") + { + unexpected t{19}; + auto before = witness; + v = std::move(t).error(); + CHECK(witness == before * 61); + CHECK(v == Foo{19}); + } + + SECTION("rval const") + { + unexpected const t{23}; + auto before = witness; + v = std::move(t).error(); + CHECK(witness == before * 67); + CHECK(v == Foo{23}); + } + } + + SECTION("assignment") + { + unexpected v{0}; + + SECTION("lval") + { + unexpected t{13}; + auto before = witness; + v = t; // t binds to unexpected const & + CHECK(witness == before * 59); + CHECK(v.error() == Foo{13}); + } + + SECTION("lval const") + { + unexpected const t{17}; + auto before = witness; + v = t; // t binds to unexpected const & + CHECK(witness == before * 59); + CHECK(v.error() == Foo{17}); + } + + SECTION("rval") + { + unexpected t{19}; + auto before = witness; + v = std::move(t); // t binds to unexpected && + CHECK(witness == before * 61); + CHECK(v.error() == Foo{19}); + } - SECTION("in_place") {} + SECTION("rval const") + { + unexpected const t{23}; + auto before = witness; + v = std::move(t); // t binds to unexpected const & + CHECK(witness == before * 59); + CHECK(v.error() == Foo{23}); + } + } + + SECTION("swap") + { + unexpected v{2}; + unexpected w{Foo{3}}; + auto before = witness; + v.swap(w); + CHECK(witness == before * 97); + CHECK(v == unexpected{Foo{3}}); + CHECK(w == unexpected{Foo{2}}); + w.error() = Foo{11}; + before = witness; + swap(v, w); + CHECK(v.error() == Foo{11}); + CHECK(w.error() == Foo(3)); + } + + SECTION("constexpr all, CTAD") + { + constexpr auto test = [](auto i) constexpr { + unexpected a{i}; + unexpected b{i * 5}; + swap(a, b); + unexpected c{0}; + c = b; + b.swap(c); + return unexpected{b.error() * a.error() * 7}; + }; + constexpr auto c = test(21); + static_assert(std::is_same_v const>); + static_assert(c.error() == 21 * 21 * 5 * 7); + + SUCCEED(); } } From ce22bd1a279d08ce5f831c21af0c29c5b893e010 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 1 Feb 2025 23:03:40 +0000 Subject: [PATCH 12/46] Trying more compilers --- .github/workflows/build.yml | 108 ++++++++++++++++++++++++++++++++- CMakeLists.txt | 10 +-- cmake/CompilationOptions.cmake | 29 +++++++++ cmake/TargetGenerator.cmake | 4 +- include/CMakeLists.txt | 5 +- tests/CMakeLists.txt | 5 +- 6 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 cmake/CompilationOptions.cmake diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dfd8396..258aa36b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ on: - 'cmake/**' jobs: - build-cxx23: + linux-cxx23: runs-on: group: default strategy: @@ -84,7 +84,7 @@ jobs: cd .build ctest -L cxx23 --output-on-failure - build-cxx20: + linux-cxx20: runs-on: group: default strategy: @@ -143,3 +143,107 @@ jobs: run: | cd .build ctest -L cxx20 --output-on-failure + + macos-cxx23: + runs-on: macos-${{ matrix.env.osver }} + strategy: + fail-fast: false + matrix: + configuration: + - Debug + - Release + env: + - compiler: clang++ + osver: 15 + - compiler: "$(brew --prefix llvm@18)/bin/clang++" + osver: 15 + steps: + - uses: actions/checkout@v4 + + - name: Prepare build + run: | + mkdir .build + cd .build + cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_CXX_COMPILER=${{ matrix.env.compiler }} .. + COMPILER=$( grep -E "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) + FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS:STRING=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) + printf "C++ compiler: %s\n" "$COMPILER" + printf "C++ compiler version: %s\n" "$( $COMPILER --version | head -1 )" + printf "C++ compilation options: %s\n" "$FLAGS" + + - name: Build all + run: | + cd .build + cmake --build . --target cxx23 + + - name: Run tests + run: | + cd .build + ctest -L cxx23 -C ${{ matrix.configuration }} --output-on-failure + + macos-cxx20: + runs-on: macos-${{ matrix.env.osver }} + strategy: + fail-fast: false + matrix: + configuration: + - Debug + - Release + env: + - compiler: clang++ + osver: 15 + - compiler: "$(brew --prefix llvm@18)/bin/clang++" + osver: 15 + steps: + - uses: actions/checkout@v4 + + - name: Prepare build + run: | + mkdir .build + cd .build + cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_CXX_COMPILER=${{ matrix.env.compiler }} .. + COMPILER=$( grep -E "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) + FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS:STRING=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) + printf "C++ compiler: %s\n" "$COMPILER" + printf "C++ compiler version: %s\n" "$( $COMPILER --version | head -1 )" + printf "C++ compilation options: %s\n" "$FLAGS" + + - name: Build all + run: | + cd .build + cmake --build . --target cxx20 + + - name: Run tests + run: | + cd .build + ctest -L cxx20 --output-on-failure + + windows-cxx20: + runs-on: windows-${{ matrix.env.osver }} + strategy: + fail-fast: false + matrix: + configuration: + - Debug + - Release + env: + - vsver: "Visual Studio 17 2022" + osver: 2025 + steps: + - uses: actions/checkout@v4 + + - name: Prepare build + run: | + mkdir .build + cd .build + cmake -G "${{ matrix.env.vsver }}" -A x64 .. + + - name: Build all + run: | + cd .build + cmake --build . --config ${{ matrix.configuration }} --target cxx20 + + - name: Run tests + run: | + cd .build + ctest -L cxx20 -C ${{ matrix.configuration }} --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 071aff63..0222e575 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,13 +35,9 @@ include(Ccache) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options( - -stdlib=libc++ - ) - add_link_options( - -lc++ - ) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND (NOT CMAKE_CXX_COMPILER_ID MATCHES "AppleClang")) + add_compile_options(-stdlib=libc++) + add_link_options(-lc++) endif() # Phony targets for all supported C++ versions diff --git a/cmake/CompilationOptions.cmake b/cmake/CompilationOptions.cmake new file mode 100644 index 00000000..6effaa1d --- /dev/null +++ b/cmake/CompilationOptions.cmake @@ -0,0 +1,29 @@ +# Add compilation options appropriate for the current compiler + +function(append_compilation_options) + set(options WARNINGS OPTIMIZATION) + set(oneValueArgs NAME) + cmake_parse_arguments(Options "${options}" "${oneValueArgs}" "" ${ARGN}) + + if(NOT DEFINED Options_NAME) + message(FATAL_ERROR "NAME must be set") + endif() + + if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + if(Options_WARNINGS) + target_compile_options(${Options_NAME} PRIVATE /W4) + endif() + + if(Options_OPTIMIZATION) + target_compile_options(${Options_NAME} PRIVATE $,/Od,/Ox>) + endif() + else() + if(Options_WARNINGS) + target_compile_options(${Options_NAME} PRIVATE -Wall -Wextra -Wpedantic) + endif() + + if(Options_OPTIMIZATION) + target_compile_options(${Options_NAME} PRIVATE $,-O0,-O2>) + endif() + endif() +endfunction() diff --git a/cmake/TargetGenerator.cmake b/cmake/TargetGenerator.cmake index ccd5bb4b..0bfea092 100644 --- a/cmake/TargetGenerator.cmake +++ b/cmake/TargetGenerator.cmake @@ -2,7 +2,7 @@ function(create_target_for_file) set(oneValueArgs NAME SOURCE SOURCE_ROOT NEW_SOURCE) - set(multiValueArgs DEPENDENCIES COMPILE_OPTIONS) + set(multiValueArgs DEPENDENCIES) cmake_parse_arguments(Generator "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(NOT DEFINED Generator_SOURCE) @@ -27,6 +27,4 @@ function(create_target_for_file) endif() target_link_libraries(${Generator_NAME} ${Generator_DEPENDENCIES}) - - target_compile_options(${Generator_NAME} PRIVATE ${Generator_COMPILE_OPTIONS}) endfunction() diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index be7b733e..244b082d 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -28,6 +28,7 @@ install(TARGETS include_pfn DESTINATION include) include(TargetGenerator) +include(CompilationOptions) # Generate sentinel target for each individual header, as a basic sanity check foreach(cxxrel 20 23) @@ -41,8 +42,8 @@ foreach(cxxrel 20 23) NEW_SOURCE "#include <${source}>\nint main() {}\n" SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" DEPENDENCIES include_pfn - COMPILE_OPTIONS -Wall -Wextra -Wpedantic ) + append_compilation_options(NAME "${target}" WARNINGS) add_dependencies("cxx${cxxrel}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") @@ -112,8 +113,8 @@ foreach(cxxrel 23) NEW_SOURCE "#include <${source}>\nint main() {}\n" SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" DEPENDENCIES include_fn - COMPILE_OPTIONS -Wall -Wextra -Wpedantic ) + append_compilation_options(NAME "${target}" WARNINGS) add_dependencies("cxx${cxxrel}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 97619f46..169e46d5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -20,6 +20,7 @@ set(TESTS_PFN_SOURCES ) include(TargetGenerator) +include(CompilationOptions) # Generate separate target for each individual test source foreach(cxxrel 20 23) @@ -31,8 +32,8 @@ foreach(cxxrel 20 23) NAME "${target}" SOURCE "${source}" DEPENDENCIES include_pfn Catch2::Catch2WithMain - COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" ) + append_compilation_options(NAME "${target}" WARNINGS OPTIMIZATION) add_dependencies("cxx${cxxrel}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") @@ -87,8 +88,8 @@ foreach(cxxrel 23) NAME "${target}" SOURCE "${source}" DEPENDENCIES include_fn tests_util Catch2::Catch2WithMain - COMPILE_OPTIONS -Wall -Wextra -Wpedantic "$<$:-O0>" ) + append_compilation_options(NAME "${target}" WARNINGS OPTIMIZATION) add_dependencies("cxx${cxxrel}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") From e5ebfc1669354c46a7c51ea4a0aeed5f47850389 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 2 Feb 2025 10:10:44 +0000 Subject: [PATCH 13/46] Fold nixOS into build.yaml, refactor build --- .github/workflows/build.yml | 267 ++++++++++++++------------------- .github/workflows/coverage.yml | 9 +- .github/workflows/nix.yml | 48 ------ CMakeLists.txt | 2 +- 4 files changed, 117 insertions(+), 209 deletions(-) delete mode 100644 .github/workflows/nix.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 258aa36b..9f38d66e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,7 @@ on: - 'tests/**' - 'CMakeLists.txt' - 'cmake/**' + - 'nix/**' pull_request: branches: - main @@ -19,9 +20,10 @@ on: - 'tests/**' - 'CMakeLists.txt' - 'cmake/**' + - 'nix/**' jobs: - linux-cxx23: + linux: runs-on: group: default strategy: @@ -30,195 +32,123 @@ jobs: configuration: - Debug - Release - compiler: - - gcc:13 - - gcc:14 - - clang:18 - - clang:19 - container: libfn/ci-build-${{ matrix.compiler }} + env: + - compiler: gcc + release: 12 + modes: "cxx20" + - compiler: clang + release: 16 + modes: "cxx20" + - compiler: clang + release: 17 + modes: "cxx20" + - compiler: gcc + release: 13 + modes: "cxx20 cxx23" + - compiler: gcc + release: 14 + modes: "cxx20 cxx23" + - compiler: clang + release: 18 + modes: "cxx20 cxx23" + - compiler: clang + release: 19 + modes: "cxx20 cxx23" + container: libfn/ci-build-${{ matrix.env.compiler }}:${{ matrix.env.release }} steps: - uses: actions/checkout@v4 - - name: Verify compiler compatibility - env: - SOURCE: | - #include - #include - #include - int main() { - using type1=std::expected; - using type2=std::optional; - return type1{1}.and_then([](int i) -> type1 { std::puts("OK expected"); return {i-1}; }).value() - + type2{2}.and_then([](int i) -> type2 { std::puts("OK optional"); return {i-2}; }).value(); - } - run: | - printf "CXX=%s\nCXXFLAGS=%s\n" "$CXX" "$CXXFLAGS" - $CXX --version | head -1 - FILE=$(mktemp --tmpdir XXXXXX.cpp) - printf "$SOURCE\n" > $FILE - OUT=$(mktemp --tmpdir XXXXXX) - $CXX -std=c++2b $CXXFLAGS -Wall $FILE -o $OUT - $OUT - - name: Prepare build run: | mkdir .build cd .build cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. - COMPILER=$( grep -E "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS:STRING=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - printf "C++ compiler: %s\n" "$COMPILER" - printf "C++ compiler version: %s\n" "$( $COMPILER --version | head -1 )" + COMPILER=$( grep -iE "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -n 's/^[^=]*=//p' ) + FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS(_${{ matrix.configuration }})?:STRING" CMakeCache.txt | sed -n 's/^[^=]*=//p' | tr '\n' ' ' ) + printf "C++ compiler path: %s\n" "$COMPILER" + $COMPILER --version printf "C++ compilation options: %s\n" "$FLAGS" - - name: Build all - env: - # We want to build "--target all" for at least one arbitrary configuration - TARGET: ${{ ( matrix.configuration == 'Debug' && matrix.compiler == 'gcc:14' ) && 'all' || 'cxx23' }} + - name: Build and test C++20 mode + if: ${{ contains( matrix.env.modes, 'cxx20' ) }} run: | cd .build - cmake --build . --target ${TARGET} + cmake --build . --target clean + cmake --build . --target cxx20 + ctest -L cxx20 --output-on-failure - - name: Run tests + - name: Build and test C++23 mode + if: ${{ contains( matrix.env.modes, 'cxx23' ) }} run: | cd .build + cmake --build . --target clean + cmake --build . --target cxx23 ctest -L cxx23 --output-on-failure - linux-cxx20: - runs-on: - group: default + # We want to build all for at least one arbitrary configuration + - name: Build and test all + if: ${{ matrix.env.compiler == 'gcc' && matrix.env.release == '14' }} + run: | + cd .build + cmake --build . --target clean + cmake --build . + ctest --output-on-failure + + macos: + runs-on: macos-${{ matrix.env.osver }} strategy: fail-fast: false matrix: configuration: - Debug - Release - compiler: - - gcc:12 - - gcc:13 - - gcc:14 - - clang:16 - - clang:17 - - clang:18 - - clang:19 - container: libfn/ci-build-${{ matrix.compiler }} + env: + - compiler: appleclang + osver: 15 + modes: "cxx20 cxx23" + - compiler: clang + release: 18 + osver: 15 + modes: "cxx20 cxx23" steps: - uses: actions/checkout@v4 - - name: Verify compiler compatibility - env: - SOURCE: | - #include - int main() { - using type=std::optional; - constexpr type a{std::in_place, 1}; - return a.value() - 1; - } - run: | - printf "CXX=%s\nCXXFLAGS=%s\n" "$CXX" "$CXXFLAGS" - $CXX --version | head -1 - FILE=$(mktemp --tmpdir XXXXXX.cpp) - printf "$SOURCE\n" > $FILE - OUT=$(mktemp --tmpdir XXXXXX) - $CXX -std=c++2a $CXXFLAGS -Wall $FILE -o $OUT - $OUT - - name: Prepare build run: | mkdir .build cd .build + if [[ "${{ matrix.env.compiler }}" == "appleclang" ]]; then + export CXX="$(which clang++)" + export CC="$(which clang)" + fi + if [[ "${{ matrix.env.compiler }}" == "clang" ]]; then + export CXX="$(brew --prefix llvm@${{ matrix.env.release }})/bin/clang++" + export CC="$(brew --prefix llvm@${{ matrix.env.release }})/bin/clang" + fi cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. - COMPILER=$( grep -E "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS:STRING=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - printf "C++ compiler: %s\n" "$COMPILER" - printf "C++ compiler version: %s\n" "$( $COMPILER --version | head -1 )" + COMPILER=$( grep -iE "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -n 's/^[^=]*=//p' ) + FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS(_${{ matrix.configuration }})?:STRING" CMakeCache.txt | sed -n 's/^[^=]*=//p' | tr '\n' ' ' ) + printf "C++ compiler path: %s\n" "$COMPILER" + $COMPILER --version printf "C++ compilation options: %s\n" "$FLAGS" - - name: Build all + - name: Build and test C++20 mode + if: ${{ contains( matrix.env.modes, 'cxx20' ) }} run: | cd .build + cmake --build . --target clean cmake --build . --target cxx20 + ctest -L cxx20 --output-on-failure - - name: Run tests + - name: Build and test C++23 mode + if: ${{ contains( matrix.env.modes, 'cxx23' ) }} run: | cd .build - ctest -L cxx20 --output-on-failure - - macos-cxx23: - runs-on: macos-${{ matrix.env.osver }} - strategy: - fail-fast: false - matrix: - configuration: - - Debug - - Release - env: - - compiler: clang++ - osver: 15 - - compiler: "$(brew --prefix llvm@18)/bin/clang++" - osver: 15 - steps: - - uses: actions/checkout@v4 - - - name: Prepare build - run: | - mkdir .build - cd .build - cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_CXX_COMPILER=${{ matrix.env.compiler }} .. - COMPILER=$( grep -E "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS:STRING=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - printf "C++ compiler: %s\n" "$COMPILER" - printf "C++ compiler version: %s\n" "$( $COMPILER --version | head -1 )" - printf "C++ compilation options: %s\n" "$FLAGS" - - - name: Build all - run: | - cd .build - cmake --build . --target cxx23 - - - name: Run tests - run: | - cd .build - ctest -L cxx23 -C ${{ matrix.configuration }} --output-on-failure - - macos-cxx20: - runs-on: macos-${{ matrix.env.osver }} - strategy: - fail-fast: false - matrix: - configuration: - - Debug - - Release - env: - - compiler: clang++ - osver: 15 - - compiler: "$(brew --prefix llvm@18)/bin/clang++" - osver: 15 - steps: - - uses: actions/checkout@v4 - - - name: Prepare build - run: | - mkdir .build - cd .build - cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_CXX_COMPILER=${{ matrix.env.compiler }} .. - COMPILER=$( grep -E "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS:STRING=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - printf "C++ compiler: %s\n" "$COMPILER" - printf "C++ compiler version: %s\n" "$( $COMPILER --version | head -1 )" - printf "C++ compilation options: %s\n" "$FLAGS" - - - name: Build all - run: | - cd .build - cmake --build . --target cxx20 - - - name: Run tests - run: | - cd .build - ctest -L cxx20 --output-on-failure + cmake --build . --target clean + cmake --build . --target cxx23 + ctest -L cxx23 --output-on-failure - windows-cxx20: + windows: runs-on: windows-${{ matrix.env.osver }} strategy: fail-fast: false @@ -227,8 +157,9 @@ jobs: - Debug - Release env: - - vsver: "Visual Studio 17 2022" + - compiler: "Visual Studio 17 2022" osver: 2025 + modes: "cxx20" steps: - uses: actions/checkout@v4 @@ -236,14 +167,40 @@ jobs: run: | mkdir .build cd .build - cmake -G "${{ matrix.env.vsver }}" -A x64 .. + cmake -G "${{ matrix.env.compiler }}" -A x64 .. - - name: Build all + - name: Build and test C++20 mode + if: ${{ contains( matrix.env.modes, 'cxx20' ) }} run: | cd .build + cmake --build . --config ${{ matrix.configuration }} --target clean cmake --build . --config ${{ matrix.configuration }} --target cxx20 + ctest -L cxx20 -C ${{ matrix.configuration }} --output-on-failure - - name: Run tests + - name: Build and test C++23 mode + if: ${{ contains( matrix.env.modes, 'cxx23' ) }} run: | cd .build - ctest -L cxx20 -C ${{ matrix.configuration }} --output-on-failure + cmake --build . --config ${{ matrix.configuration }} --target clean + cmake --build . --config ${{ matrix.configuration }} --target cxx23 + ctest -L cxx23 -C ${{ matrix.configuration }} --output-on-failure + + nixos: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + compiler: + - gcc + - clang + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v30 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + + - name: Build and test + run: nix -L build '.#${{matrix.compiler}}' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7985eb35..c7129ce4 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -62,13 +62,12 @@ jobs: mkdir .build cd .build cmake -DCMAKE_C_FLAGS="$COVERAGE_OPTS" -DCMAKE_CXX_FLAGS="$COVERAGE_OPTS" .. - COMPILER=$( grep -E "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS:STRING=" CMakeCache.txt | sed -e "s|^[^=]*=||" ) - printf "C++ compiler: %s\n" "$COMPILER" - printf "C++ compiler version: %s\n" "$( $COMPILER --version | head -1 )" + COMPILER=$( grep -iE "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -n 's/^[^=]*=//p' ) + FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS(_${{ matrix.configuration }})?:STRING" CMakeCache.txt | sed -n 's/^[^=]*=//p' | tr '\n' ' ' ) + printf "C++ compiler path: %s\n" "$COMPILER" + $COMPILER --version printf "C++ compilation options: %s\n" "$FLAGS" printf "gcov version: %s\n" "$( $GCOV --version | head -1 )" - [[ "$( realpath $CXX )" == "$( realpath $COMPILER )" ]] || exit 13 - name: Run coverage target shell: bash diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml deleted file mode 100644 index 4536e383..00000000 --- a/.github/workflows/nix.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: nix - -on: - push: - branches: - - main - paths: - - '.github/workflows/nix.yml' - - 'nix/**' - - 'include/**' - - 'tests/**' - - 'CMakeLists.txt' - - 'cmake/**' - - 'README.md' - - 'LICENSE.md' - pull_request: - branches: - - main - paths: - - '.github/workflows/nix.yml' - - 'nix/**' - - 'include/**' - - 'tests/**' - - 'CMakeLists.txt' - - 'cmake/**' - - 'README.md' - - 'LICENSE.md' - -jobs: - build: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - compiler: - - gcc - - clang - steps: - - uses: actions/checkout@v4 - - - name: Install Nix - uses: cachix/install-nix-action@v27 - with: - extra_nix_config: | - access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - - name: Build and test - run: nix -L build '.#${{matrix.compiler}}' diff --git a/CMakeLists.txt b/CMakeLists.txt index 0222e575..85cac091 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,7 @@ include(Ccache) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND (NOT CMAKE_CXX_COMPILER_ID MATCHES "AppleClang")) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND (NOT APPLE)) add_compile_options(-stdlib=libc++) add_link_options(-lc++) endif() From 8a40b92f69c2439d458bcff4580eeeaa4768a1a9 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 2 Feb 2025 12:17:56 +0000 Subject: [PATCH 14/46] Workaround for appleclang build error --- tests/fn/choice.cpp | 88 +++------------------------------------------ 1 file changed, 4 insertions(+), 84 deletions(-) diff --git a/tests/fn/choice.cpp b/tests/fn/choice.cpp index acaa6118..a6b5a297 100644 --- a/tests/fn/choice.cpp +++ b/tests/fn/choice.cpp @@ -443,7 +443,7 @@ TEST_CASE("choice and_then", "[choice][and_then]") TEST_CASE("choice transform", "[choice][transform]") { - WHEN("size 1") + WHEN("size 2, only one set") { using type = fn::choice; constexpr auto init = std::in_place_type; @@ -503,15 +503,14 @@ TEST_CASE("choice transform", "[choice][transform]") == fn::choice{5.25}); } - WHEN("size 4") + WHEN("size 2") { - static constexpr auto sizeof_string = sizeof(std::string); using ::fn::choice; using ::fn::sum; constexpr auto fn1 = [](auto i) noexcept -> std::size_t { return sizeof(i); }; - using type = choice; - static_assert(type::size == 4); + using type = choice; + static_assert(type::size == 2); WHEN("element v0 set") { @@ -583,84 +582,5 @@ TEST_CASE("choice transform", "[choice][transform]") == choice{true}); } } - - WHEN("element v2 set") - { - type a{std::in_place_type, "bar"}; - CHECK(a.data.v2 == "bar"); - WHEN("value only") - { - // TODO Change single CHECK below to static_assert when supported by Clang - CHECK(type{std::in_place_type, "bar"}.transform(fn1) == choice{sizeof_string}); - CHECK(a.transform( // - fn::overload( // - [](auto) -> sum { throw 1; }, - [](std::string &i) -> bool { return i == "bar"; }, [](std::string const &) -> bool { throw 0; }, - [](std::string &&) -> bool { throw 0; }, [](std::string const &&) -> bool { throw 0; })) - == choice{true}); - CHECK(std::as_const(a).transform( // - fn::overload( // - [](auto) -> sum { throw 1; }, [](std::string &) -> bool { throw 0; }, - [](std::string const &i) -> bool { return i == "bar"; }, [](std::string &&) -> bool { throw 0; }, - [](std::string const &&) -> bool { throw 0; })) - == choice{true}); - CHECK(type{std::in_place_type, "bar"}.transform( // - fn::overload( // - [](auto) -> sum { throw 1; }, [](std::string &) -> bool { throw 0; }, - [](std::string const &) -> bool { throw 0; }, [](std::string &&i) -> bool { return i == "bar"; }, - [](std::string const &&) -> bool { throw 0; })) - == choice{true}); - CHECK(std::move(std::as_const(a)) - .transform( // - fn::overload( // - [](auto) -> sum { throw 1; }, [](std::string &) -> bool { throw 0; }, - [](std::string const &) -> bool { throw 0; }, [](std::string &&) -> bool { throw 0; }, - [](std::string const &&i) -> bool { return i == "bar"; })) - == choice{true}); - } - } - - WHEN("element v3 set") - { - type a{std::in_place_type, "baz"}; - CHECK(a.data.v3 == "baz"); - WHEN("value only") - { - static_assert(type{std::in_place_type, "baz"}.transform(fn1) == choice{16uz}); - CHECK(a.transform( // - fn::overload( // - [](auto) -> sum { throw 1; }, - [](std::string_view &i) -> sum { return {i == "baz"}; }, - [](std::string_view const &) -> sum { throw 0; }, - [](std::string_view &&) -> sum { throw 0; }, - [](std::string_view const &&) -> sum { throw 0; })) - == choice{true}); - CHECK(std::as_const(a).transform( // - fn::overload( // - [](auto) -> sum { throw 1; }, - [](std::string_view &) -> sum { throw 0; }, - [](std::string_view const &i) -> sum { return {i == "baz"}; }, - [](std::string_view &&) -> sum { throw 0; }, - [](std::string_view const &&) -> sum { throw 0; })) - == choice{true}); - CHECK(type{std::in_place_type, "baz"}.transform( // - fn::overload( // - [](auto) -> sum { throw 1; }, - [](std::string_view &) -> sum { throw 0; }, - [](std::string_view const &) -> sum { throw 0; }, - [](std::string_view &&i) -> sum { return {i == "baz"}; }, - [](std::string_view const &&) -> sum { throw 0; })) - == choice{true}); - CHECK(std::move(std::as_const(a)) - .transform( // - fn::overload( // - [](auto) -> sum { throw 1; }, - [](std::string_view &) -> sum { throw 0; }, - [](std::string_view const &) -> sum { throw 0; }, - [](std::string_view &&) -> sum { throw 0; }, - [](std::string_view const &&i) -> sum { return {i == "baz"}; })) - == choice{true}); - } - } } } From 4ef3afd75cb71c0ba1218906319f57152a61386b Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 2 Feb 2025 13:04:15 +0000 Subject: [PATCH 15/46] Improve unit tests coverage --- include/CMakeLists.txt | 21 +- tests/CMakeLists.txt | 57 +++-- tests/fn/choice.cpp | 92 +++++++- tests/pfn/expected.cpp | 460 ++++++++++++++++++------------------ tests/util/helper_types.hpp | 104 ++++++++ 5 files changed, 473 insertions(+), 261 deletions(-) create mode 100644 tests/util/helper_types.hpp diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index 244b082d..0756c5f5 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -31,10 +31,10 @@ include(TargetGenerator) include(CompilationOptions) # Generate sentinel target for each individual header, as a basic sanity check -foreach(cxxrel 20 23) +foreach(mode 20 23) foreach(source IN ITEMS ${INCLUDE_PFN_HEADERS}) string(REGEX REPLACE "^(pfn)/(detail|)/?([^\.]+)\.hpp$" "\\1_\\2\\3" root_name ${source}) - set(target "sentinel_${root_name}_cxx${cxxrel}") + set(target "sentinel_${root_name}_cxx${mode}") create_target_for_file( NAME "${target}" @@ -44,8 +44,8 @@ foreach(cxxrel 20 23) DEPENDENCIES include_pfn ) append_compilation_options(NAME "${target}" WARNINGS) - add_dependencies("cxx${cxxrel}" "${target}") - set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") + add_dependencies("cxx${mode}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") unset(target) unset(root_name) @@ -92,7 +92,10 @@ target_sources(include_fn INTERFACE TYPE HEADERS FILES ${INCLUDE_FN_HEADERS}) target_include_directories(include_fn SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_compile_options(include_fn INTERFACE -Wno-missing-braces) + +if(NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + target_compile_options(include_fn INTERFACE -Wno-missing-braces) +endif() # TODO uncomment when include_pfn is ready # target_link_libraries(include_fn INTERFACE include_pfn) @@ -102,10 +105,10 @@ install(TARGETS include_fn # Generate sentinel target for each individual header, as a basic sanity check # TODO add 20 when we are compatible with C++20 -foreach(cxxrel 23) +foreach(mode 23) foreach(source IN ITEMS ${INCLUDE_FN_HEADERS}) string(REGEX REPLACE "^(fn)/(detail|)/?([^\.]+)\.hpp$" "\\1_\\2\\3" root_name ${source}) - set(target "sentinel_${root_name}_cxx${cxxrel}") + set(target "sentinel_${root_name}_cxx${mode}") create_target_for_file( NAME "${target}" @@ -115,8 +118,8 @@ foreach(cxxrel 23) DEPENDENCIES include_fn ) append_compilation_options(NAME "${target}" WARNINGS) - add_dependencies("cxx${cxxrel}" "${target}") - set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") + add_dependencies("cxx${mode}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") unset(target) unset(root_name) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 169e46d5..aa51d8e8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,12 +7,35 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Pls keep the filenames sorted set(TESTS_UTIL_SOURCES util/static_check.hpp + util/helper_types.hpp ) add_library(tests_util INTERFACE ${TESTS_UTIL_SOURCES}) set_property(TARGET include_fn PROPERTY CXX_STANDARD 23) target_include_directories(tests_util INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(tests_util INTERFACE include_fn) +# Generate sentinel target for each individual header, as a basic sanity check +foreach(mode 23) + foreach(source IN ITEMS ${TESTS_UTIL_SOURCES}) + string(REGEX REPLACE "^(util)/([^\.]+)\.hpp$" "\\1_\\2" root_name ${source}) + set(target "sentinel_${root_name}_cxx${mode}") + + create_target_for_file( + NAME "${target}" + SOURCE "${source}" + NEW_SOURCE "#include <${source}>\nint main() {}\n" + SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" + DEPENDENCIES include_fn tests_util + ) + append_compilation_options(NAME "${target}" WARNINGS) + add_dependencies("cxx${mode}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") + + unset(target) + unset(root_name) + endforeach() +endforeach() + ### tests/pfn set(TESTS_PFN_SOURCES @@ -23,26 +46,26 @@ include(TargetGenerator) include(CompilationOptions) # Generate separate target for each individual test source -foreach(cxxrel 20 23) +foreach(mode 20 23) foreach(source IN ITEMS ${TESTS_PFN_SOURCES}) string(REGEX REPLACE "^(pfn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) - set(target "tests_${root_name}_cxx${cxxrel}") + set(target "tests_${root_name}_cxx${mode}") create_target_for_file( NAME "${target}" SOURCE "${source}" - DEPENDENCIES include_pfn Catch2::Catch2WithMain + DEPENDENCIES include_pfn tests_util Catch2::Catch2WithMain ) append_compilation_options(NAME "${target}" WARNINGS OPTIMIZATION) - add_dependencies("cxx${cxxrel}" "${target}") - set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") + add_dependencies("cxx${mode}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") add_test( NAME "${target}" COMMAND "${target}" -r console WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) - set_property(TEST "${target}" PROPERTY LABELS tests_pfn "cxx${cxxrel}" "${root_name}") + set_property(TEST "${target}" PROPERTY LABELS tests_pfn "cxx${mode}" "${root_name}") unset(target) unset(root_name) @@ -79,10 +102,10 @@ set(TESTS_FN_SOURCES # Generate separate target for each individual test source # TODO add 20 when we are compatible with C++20 -foreach(cxxrel 23) +foreach(mode 23) foreach(source IN ITEMS ${TESTS_FN_SOURCES}) string(REGEX REPLACE "^(fn)/(detail|)/?([^\.]+)\.cpp$" "\\1_\\2\\3" root_name ${source}) - set(target "tests_${root_name}_cxx${cxxrel}") + set(target "tests_${root_name}_cxx${mode}") create_target_for_file( NAME "${target}" @@ -90,15 +113,15 @@ foreach(cxxrel 23) DEPENDENCIES include_fn tests_util Catch2::Catch2WithMain ) append_compilation_options(NAME "${target}" WARNINGS OPTIMIZATION) - add_dependencies("cxx${cxxrel}" "${target}") - set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") + add_dependencies("cxx${mode}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") add_test( NAME "${target}" COMMAND "${target}" -r console WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) - set_property(TEST "${target}" PROPERTY LABELS tests_fn "cxx${cxxrel}" "${root_name}") + set_property(TEST "${target}" PROPERTY LABELS tests_fn "cxx${mode}" "${root_name}") unset(target) unset(root_name) @@ -111,21 +134,21 @@ set(TESTS_EXAMPLES_SOURCES ) # TODO add 20 when we are compatible with C++20 -foreach(cxxrel 23) - set(target "tests_examples_cxx${cxxrel}") +foreach(mode 23) + set(target "tests_examples_cxx${mode}") add_executable("${target}" ${TESTS_EXAMPLES_SOURCES}) target_link_libraries("${target}" include_fn Catch2::Catch2WithMain) - target_compile_options("${target}" PRIVATE -Wall -Wextra -Wpedantic) - add_dependencies("cxx${cxxrel}" "${target}") - set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${cxxrel}") + append_compilation_options(NAME "${target}" WARNINGS) + add_dependencies("cxx${mode}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") add_test( NAME "${target}" COMMAND "${target}" -r console WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) - set_property(TEST "${target}" PROPERTY LABELS tests_examples "cxx${cxxrel}") + set_property(TEST "${target}" PROPERTY LABELS tests_examples "cxx${mode}") unset(target) endforeach() diff --git a/tests/fn/choice.cpp b/tests/fn/choice.cpp index a6b5a297..d90ba4b0 100644 --- a/tests/fn/choice.cpp +++ b/tests/fn/choice.cpp @@ -4,8 +4,11 @@ // or copy at https://opensource.org/licenses/ISC #include + #include +#include + #include #include @@ -40,18 +43,18 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(std::same_as, typename choice::value_type>); static_assert(std::same_as, typename choice::value_type>); - using type = choice; - using value_type = fn::sum; + using type = fn::choice; + using value_type = fn::sum; static_assert(std::same_as().value())>); static_assert(std::same_as().value())>); static_assert(std::same_as().value())>); static_assert(std::same_as().value())>); - type s{42}; + type s{helper{42}}; constexpr auto fn = fn::overload{ [](auto &&) -> int { throw 0; }, // - [](int &i) -> int { return i + 1; }, [](int const &i) -> int { return i + 2; }, - [](int &&i) -> int { return i + 3; }, [](int const &&i) -> int { return i + 4; }, + [](helper &o) -> int { return o.v + 1; }, [](helper const &o) -> int { return o.v + 2; }, + [](helper &&o) -> int { return o.v + 3; }, [](helper const &&o) -> int { return o.v + 4; }, }; static_assert(std::same_as); CHECK((s.value().invoke(fn)) == 43); @@ -146,7 +149,7 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(c.invoke([](auto &&a) -> bool { return a.size() == 3 && a[0] == 3 && a[1] == 14 && a[2] == 15; })); } - WHEN("move from rvalue") + WHEN("constexpr move from rvalue") { using T = fn::choice; constexpr auto fn = [](auto i) constexpr noexcept -> T { return {std::move(i)}; }; @@ -159,7 +162,22 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(b.has_value()); } - WHEN("copy from lvalue") + WHEN("move from rvalue") + { + using T = fn::choice; + constexpr auto fn = [](auto i) noexcept -> T { return {std::move(i)}; }; + auto const before = helper::witness; + auto const a = fn(helper{12}); + CHECK(helper::witness == (before + 12) * helper::from_rval); + static_assert(std::is_same_v); + CHECK(a.has_value()); + + auto b = fn(true); + static_assert(std::is_same_v); + CHECK(b.has_value()); + } + + WHEN("constexpr copy from lvalue") { using T = fn::choice; constexpr auto fn = [](auto i) constexpr noexcept -> T { return {i}; }; @@ -171,6 +189,66 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(std::is_same_v); static_assert(b.has_value()); } + + WHEN("copy from lvalue") + { + using T = fn::choice; + constexpr auto fn = [](auto i) noexcept -> T { return {i}; }; + auto const a = fn(true); + static_assert(std::is_same_v); + CHECK(a.has_value()); + + auto const before = helper::witness; + auto b = fn(helper{13}); + CHECK(helper::witness == (before + 13) * helper::from_lval_const); + static_assert(std::is_same_v); + CHECK(b.has_value()); + } + + WHEN("copy ctor") + { + using T = fn::choice; + auto const h = helper{23}; + auto const a = T{h}; + auto const before = helper::witness; + auto const b = a; + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(b.has_value()); + CHECK(b.value() == fn::sum{h}); + } + + WHEN("move ctor") + { + using T = fn::choice; + auto const h = helper{29}; + auto a = T{h}; + auto const before = helper::witness; + auto const b = std::move(a); + CHECK(helper::witness == before * helper::from_rval); + CHECK(b.has_value()); + CHECK(b.value() == fn::sum{h}); + } + } + + WHEN("constructor from sum") + { + using T = fn::choice; + WHEN("move from rvalue") + { + fn::sum h{helper{17}}; + auto const before = helper::witness; + T const a{std::move(h)}; + CHECK(helper::witness == before * helper::from_rval); + CHECK(a == h); + } + WHEN("copy from const lvalue") + { + fn::sum h{helper{19}}; + auto const before = helper::witness; + T const a{h}; + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(a == h); + } } WHEN("forwarding constructors (immovable)") diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 52694e9d..1377dcf2 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -5,6 +5,8 @@ #include +#include + #include #include @@ -16,202 +18,204 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") { SECTION("bad_expected_access") { - struct A : pfn::bad_expected_access {}; + struct T : pfn::bad_expected_access {}; - static_assert(noexcept(A{})); - A a; - static_assert(noexcept(A{a})); - static_assert(noexcept(A{std::move(a)})); + static_assert(noexcept(T{})); + T a; + static_assert(noexcept(T{a})); + static_assert(noexcept(T{std::move(a)})); static_assert(noexcept(a.what())); static_assert(std::is_same_v); SECTION("constructors and assignment") { - A a1 = [&]() -> A & { return a; }(); + T a1 = [&]() -> T & { return a; }(); CHECK(a.what() == a1.what()); - A a2 = [&]() -> A && { return std::move(a); }(); + T a2 = [&]() -> T && { return std::move(a); }(); CHECK(a.what() == a2.what()); - A a3 = [&]() -> A const & { return a; }(); + T a3 = [&]() -> T const & { return a; }(); CHECK(a.what() == a3.what()); - A a4 = [&]() -> A const && { return std::move(a); }(); + T a4 = [&]() -> T const && { return std::move(a); }(); CHECK(a.what() == a4.what()); - a = [&]() -> A & { return a; }(); - CHECK(A{}.what() == a.what()); - a = [&]() -> A && { return std::move(a); }(); - CHECK(A{}.what() == a.what()); - a = [&]() -> A const & { return a; }(); - CHECK(A{}.what() == a.what()); - a = [&]() -> A const && { return std::move(a); }(); - CHECK(A{}.what() == a.what()); + a = [&]() -> T & { return a; }(); + CHECK(T{}.what() == a.what()); + a = [&]() -> T && { return std::move(a); }(); + CHECK(T{}.what() == a.what()); + a = [&]() -> T const & { return a; }(); + CHECK(T{}.what() == a.what()); + a = [&]() -> T const && { return std::move(a); }(); + CHECK(T{}.what() == a.what()); } CHECK(std::strcmp(a.what(), "bad access to expected without expected value") == 0); - A const b; + T const b; CHECK(&decltype(a)::what == &decltype(b)::what); CHECK(a.what() == b.what()); - static_assert(noexcept(A{b})); - static_assert(noexcept(A{std::move(b)})); + static_assert(noexcept(T{b})); + static_assert(noexcept(T{std::move(b)})); static_assert(noexcept(b.what())); static_assert(std::is_same_v); } SECTION("bad_expected_access") { - using A = pfn::bad_expected_access; - static_assert(std::is_base_of_v, A>); + using T = pfn::bad_expected_access; + static_assert(std::is_base_of_v, T>); - A a{secret}; - static_assert(noexcept(A{a})); - static_assert(noexcept(A{std::move(a)})); + T a{12}; + static_assert(noexcept(T{a})); + static_assert(noexcept(T{std::move(a)})); static_assert(noexcept(a.what())); - SECTION("constructors and assignment") - { - A a1 = [&]() -> A & { return a; }(); - CHECK(a1.error() == secret); - A a2 = [&]() -> A && { return std::move(a); }(); - CHECK(a2.error() == secret); - A a3 = [&]() -> A const & { return a; }(); - CHECK(a3.error() == secret); - A a4 = [&]() -> A const && { return std::move(a); }(); - CHECK(a4.error() == secret); - - a = [&]() -> A & { return a; }(); - CHECK(a.error() == secret); - a = [&]() -> A && { return std::move(a); }(); - CHECK(a.error() == secret); - a = [&]() -> A const & { return a; }(); - CHECK(a.error() == secret); - a = [&]() -> A const && { return std::move(a); }(); - CHECK(a.error() == secret); - } static_assert(std::is_same_v); - static_assert(std::is_same_v); - static_assert(std::is_same_v); - CHECK(a.error() == secret); - CHECK(std::move(a).error() == secret); - - A const b{mystery}; - static_assert(noexcept(A{b})); - static_assert(noexcept(A{std::move(b)})); - static_assert(noexcept(b.what())); - static_assert(std::is_same_v); - static_assert(std::is_same_v); - static_assert(std::is_same_v); - CHECK(b.error() == mystery); - CHECK(std::move(b).error() == mystery); + static_assert(std::is_same_v); + static_assert(std::is_same_v); - CHECK(std::strcmp(a.what(), "bad access to expected without expected value") == 0); - CHECK(a.what() == b.what()); - auto const c = []() { - struct C : pfn::bad_expected_access {}; - return C{}; - }(); - CHECK(a.what() == c.what()); - CHECK(&decltype(a)::what == &decltype(b)::what); - } -} - -namespace test { -template struct A {}; -} // namespace test - -TEST_CASE("unexpect", "[expected][polyfill][unexpect]") -{ - SECTION("unexpect") - { - static_assert(std::is_empty_v); - static_assert(noexcept(pfn::unexpect_t{})); - static_assert(std::is_same_v); - // pfn::unexpect can be used as a NTTP - static_assert(std::is_empty_v>); - static constexpr auto a = pfn::unexpect; - static_assert(std::is_empty_v>); - static_assert(std::is_same_v); - static_assert(std::is_same_v, test::A>); - } -} + SECTION("copy/move constructors") + { + SECTION("lval") + { + auto const before = helper::witness; + T c = a; + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(c.error() == 12); + } -namespace unxp { -static int witness = 0; + SECTION("lval const") + { + T const b{13}; + auto const before = helper::witness; + T c = b; + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(c.error() == 13); + } -struct Foo { - int v = {}; + SECTION("rval") + { + auto const before = helper::witness; + T c = std::move(a); + CHECK(helper::witness == before * helper::from_rval); + CHECK(c.error() == 12); + } - Foo() = delete; - Foo(Foo &&) = default; + SECTION("rval cont") + { + T const b{17}; + auto const before = helper::witness; + T c = std::move(b); + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(c.error() == 17); + } + } - Foo &operator=(Foo &o) noexcept - { - v = o.v; - witness *= 53; - return *this; - } + SECTION("assignment") + { + SECTION("lval") + { + T b{11}; + auto const before = helper::witness; + a = b; + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(a.error() == 11); + } - Foo &operator=(Foo const &o) noexcept - { - v = o.v; - witness *= 59; - return *this; - } + SECTION("lval const") + { + T const b{11}; + auto const before = helper::witness; + a = b; + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(a.error() == 11); + } - Foo &operator=(Foo &&o) noexcept - { - v = o.v; - witness *= 61; - return *this; - } + SECTION("rval") + { + T b{11}; + auto const before = helper::witness; + a = std::move(b); + CHECK(helper::witness == before * helper::from_rval); + CHECK(a.error() == 11); + } - Foo &operator=(Foo const &&o) noexcept - { - v = o.v; - witness *= 67; - return *this; - } + SECTION("rval const") + { + T const b{11}; + auto const before = helper::witness; + a = std::move(b); + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(a.error() == 11); + } + } - Foo(int a) noexcept : v(a) { witness += a; } + SECTION("accessors") + { + helper c{0}; - Foo(auto &&...a) noexcept - requires(sizeof...(a) > 1 && (std::is_same_v, int> && ...)) - : v((1 * ... * a)) - { - witness += v; - } + SECTION("lval") + { + T b{11}; + auto const before = helper::witness; + c = b.error(); + CHECK(helper::witness == before * helper::from_lval); + CHECK(c == 11); + } - Foo(std::initializer_list l, auto... a) noexcept(false) - requires(std::is_same_v, int> && ...) - : v(init(l, a...)) - { - witness += v; - } + SECTION("lval const") + { + T const b{13}; + auto const before = helper::witness; + c = b.error(); + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(c == 13); + } - bool operator==(Foo const &) const noexcept = default; + SECTION("rval") + { + T b{17}; + auto const before = helper::witness; + c = std::move(b).error(); + CHECK(helper::witness == before * helper::from_rval); + CHECK(c == 17); + } - static int init(std::initializer_list l, auto &&...a) noexcept(false) - { - double ret = (1 * ... * a); - for (auto d : l) { - if (d == 0.0) - throw std::runtime_error("invalid input"); - ret *= d; + SECTION("rval const") + { + T const b{19}; + auto const before = helper::witness; + c = std::move(b).error(); + CHECK(helper::witness == before * helper::from_rval_const); + CHECK(c == 19); + } } - return static_cast(ret); + + CHECK(std::strcmp(a.what(), "bad access to expected without expected value") == 0); + auto const c = []() { + struct C : pfn::bad_expected_access {}; + return C{}; + }(); + CHECK(a.what() == c.what()); } -}; +} -void swap(Foo &l, Foo &r) +TEST_CASE("unexpect", "[expected][polyfill][unexpect]") { - std::swap(l.v, r.v); - witness *= 97; + static_assert(std::is_empty_v); + static_assert(noexcept(pfn::unexpect_t{})); + static_assert(std::is_same_v); + + // pfn::unexpect can be used as a NTTP + static_assert(not std::is_empty_v>); + static constexpr auto a = pfn::unexpect; + static_assert(not std::is_empty_v>); + static_assert(std::is_same_v); + static_assert(std::is_same_v, helper_t>); + + SUCCEED(); } -} // namespace unxp - TEST_CASE("unexpected", "[expected][polyfill][unexpected]") { using pfn::unexpected; - using unxp::Foo; - using unxp::witness; SECTION("is_valid_unexpected") { @@ -255,32 +259,32 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SECTION("no conversion, CTAD") { - auto const before = witness; - unexpected c{Foo{2}}; - CHECK(witness == before + 2); - CHECK(c.error() == Foo{2}); - CHECK(c == unexpected{2}); - static_assert(std::is_same_v>); - static_assert(std::is_nothrow_constructible_v); + auto const before = helper::witness; + unexpected c{helper{2}}; + CHECK(helper::witness == (before + 2) * helper::from_rval); + CHECK(c.error() == helper{2}); + CHECK(c == unexpected{2}); + static_assert(std::is_same_v>); + static_assert(std::is_nothrow_constructible_v); } SECTION("conversion, no CTAD") { - auto const before = witness; - unexpected c(3); - CHECK(witness == before + 3); + auto const before = helper::witness; + unexpected c(3); + CHECK(helper::witness == before + 3); CHECK(c.error().v == 3); - CHECK(c == unexpected{3}); + CHECK(c == unexpected{3}); static_assert(std::is_nothrow_constructible_v); } SECTION("in-place, no CTAD") { - auto const before = witness; - unexpected c(std::in_place, 3, 5); - CHECK(witness == before + 3 * 5); - CHECK(c.error() == Foo{3, 5}); - CHECK(c == unexpected{15}); + auto const before = helper::witness; + unexpected c(std::in_place, 3, 5); + CHECK(helper::witness == before + 3 * 5); + CHECK(c.error() == helper{3, 5}); + CHECK(c == unexpected{15}); static_assert(std::is_nothrow_constructible_v); } @@ -288,12 +292,12 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") { SECTION("forwarded args") { - auto const before = witness; - unexpected c(std::in_place, {3.0, 5.0}, 7, 11); + auto const before = helper::witness; + unexpected c(std::in_place, {3.0, 5.0}, 7, 11); auto const d = 3 * 5 * 7 * 11; - CHECK(witness == before + d); - CHECK(c.error() == Foo{d}); - CHECK(c == unexpected{Foo{d}}); + CHECK(helper::witness == before + d); + CHECK(c.error() == helper{d}); + CHECK(c == unexpected{helper{d}}); static_assert( not std::is_nothrow_constructible_v, int, int>); static_assert(std::is_constructible_v, int, int>); @@ -301,127 +305,127 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SECTION("no forwarded args") { - auto const before = witness; - unexpected c(std::in_place, {2.0, 2.5}); - CHECK(witness == before + 5); - CHECK(c.error() == Foo{5}); - CHECK(c == unexpected{5}); + auto const before = helper::witness; + unexpected c(std::in_place, {2.0, 2.5}); + CHECK(helper::witness == before + 5); + CHECK(c.error() == helper{5}); + CHECK(c == unexpected{5}); static_assert(not std::is_nothrow_constructible_v>); static_assert(std::is_constructible_v>); } SECTION("exception thrown") { - unexpected t{13}; - auto const before = witness; + unexpected t{13}; + auto const before = helper::witness; try { - t = unexpected{std::in_place, {2.0, 1.0, 0.0}, 5}; + t = unexpected{std::in_place, {2.0, 1.0, 0.0}, 5}; FAIL(); } catch (std::runtime_error const &) { SUCCEED(); } CHECK(t.error().v == 13); - CHECK(witness == before); + CHECK(helper::witness == before); } } } - SECTION("accessor") + SECTION("accessors") { - Foo v{1}; + helper v{1}; SECTION("lval") { - unexpected t{13}; - auto before = witness; + unexpected t{13}; + auto before = helper::witness; v = t.error(); - CHECK(witness == before * 53); - CHECK(v == Foo{13}); + CHECK(helper::witness == before * helper::from_lval); + CHECK(v == helper{13}); } SECTION("lval const") { - unexpected const t{17}; - auto before = witness; + unexpected const t{17}; + auto before = helper::witness; v = t.error(); - CHECK(witness == before * 59); - CHECK(v == Foo{17}); + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(v == helper{17}); } SECTION("rval") { - unexpected t{19}; - auto before = witness; + unexpected t{19}; + auto before = helper::witness; v = std::move(t).error(); - CHECK(witness == before * 61); - CHECK(v == Foo{19}); + CHECK(helper::witness == before * helper::from_rval); + CHECK(v == helper{19}); } SECTION("rval const") { - unexpected const t{23}; - auto before = witness; + unexpected const t{23}; + auto before = helper::witness; v = std::move(t).error(); - CHECK(witness == before * 67); - CHECK(v == Foo{23}); + CHECK(helper::witness == before * helper::from_rval_const); + CHECK(v == helper{23}); } } SECTION("assignment") { - unexpected v{0}; + unexpected v{0}; SECTION("lval") { - unexpected t{13}; - auto before = witness; - v = t; // t binds to unexpected const & - CHECK(witness == before * 59); - CHECK(v.error() == Foo{13}); + unexpected t{13}; + auto before = helper::witness; + v = t; + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(v.error() == helper{13}); } SECTION("lval const") { - unexpected const t{17}; - auto before = witness; - v = t; // t binds to unexpected const & - CHECK(witness == before * 59); - CHECK(v.error() == Foo{17}); + unexpected const t{17}; + auto before = helper::witness; + v = t; + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(v.error() == helper{17}); } SECTION("rval") { - unexpected t{19}; - auto before = witness; - v = std::move(t); // t binds to unexpected && - CHECK(witness == before * 61); - CHECK(v.error() == Foo{19}); + unexpected t{19}; + auto before = helper::witness; + v = std::move(t); + CHECK(helper::witness == before * helper::from_rval); + CHECK(v.error() == helper{19}); } SECTION("rval const") { - unexpected const t{23}; - auto before = witness; - v = std::move(t); // t binds to unexpected const & - CHECK(witness == before * 59); - CHECK(v.error() == Foo{23}); + unexpected const t{23}; + auto before = helper::witness; + v = std::move(t); + CHECK(helper::witness == before * helper::from_lval_const); + CHECK(v.error() == helper{23}); } } SECTION("swap") { - unexpected v{2}; - unexpected w{Foo{3}}; - auto before = witness; + unexpected v{2}; + unexpected w{helper{3}}; + auto before = helper::witness; v.swap(w); - CHECK(witness == before * 97); - CHECK(v == unexpected{Foo{3}}); - CHECK(w == unexpected{Foo{2}}); - w.error() = Foo{11}; - before = witness; + CHECK(helper::witness == before * helper::swapped); + CHECK(v == unexpected{helper{3}}); + CHECK(w == unexpected{helper{2}}); + w.error() = helper{11}; + before = helper::witness; swap(v, w); - CHECK(v.error() == Foo{11}); - CHECK(w.error() == Foo(3)); + CHECK(v.error() == helper{11}); + CHECK(w.error() == helper(3)); } SECTION("constexpr all, CTAD") diff --git a/tests/util/helper_types.hpp b/tests/util/helper_types.hpp new file mode 100644 index 00000000..3fd47415 --- /dev/null +++ b/tests/util/helper_types.hpp @@ -0,0 +1,104 @@ +// Copyright (c) 2025 Bronek Kozicki +// +// Distributed under the ISC License. See accompanying file LICENSE.md +// or copy at https://opensource.org/licenses/ISC + +#include +#include +#include +#include +#include + +template struct helper_t { + static inline int witness = 0; + int v = {}; + + // Use prime numbers to record Foo states in witness + enum { + from_lval = 53, // + from_lval_const = 59, + from_rval = 61, + from_rval_const = 67, + swapped = 97 + }; + + // No default constructor + helper_t() = delete; + + ~helper_t() noexcept = default; + + bool operator==(helper_t const &) const noexcept = default; + + // Assignment operators will multiply witness by a prime + helper_t &operator=(helper_t &o) noexcept + { + v = o.v; + witness *= from_lval; + return *this; + } + + helper_t &operator=(helper_t const &o) noexcept + { + v = o.v; + witness *= from_lval_const; + return *this; + } + + helper_t &operator=(helper_t &&o) noexcept + { + v = o.v; + witness *= from_rval; + return *this; + } + + helper_t &operator=(helper_t const &&o) noexcept + { + v = o.v; + witness *= from_rval_const; + return *this; + } + + // Every constructor will increase witness, including copy/move + helper_t(helper_t &&o) noexcept : v(o.v) { witness *= from_rval; } + helper_t(helper_t const &o) noexcept : v(o.v) { witness *= from_lval_const; } + + helper_t(std::integral auto... a) noexcept + requires(sizeof...(a) > 0) // intentionally implicit when sizeof...(a) == 1 + : v((1 * ... * a)) + { + witness += v; + } + + template // + explicit helper_t(std::integral_constant) noexcept : v(static_cast(T::value)) + { + witness += v; + } + + // Potentially throwing constructor + helper_t(std::initializer_list list, std::integral auto... a) noexcept(false) : v(init(list, a...)) // + { + witness += v; + } + + // ... and the actual exception being thrown + static int init(std::initializer_list l, auto &&...a) noexcept(false) + { + double ret = (1 * ... * a); + for (auto d : l) { + if (d == 0.0) + throw std::runtime_error("invalid input"); + ret *= d; + } + return static_cast(ret); + } +}; + +// Swap will also multiply witness by a prime +template void swap(helper_t &l, helper_t &r) +{ + std::swap(l.v, r.v); + helper_t::witness *= helper_t::swapped; +} + +using helper = helper_t<0>; From 7222f55d01dac7c5a6f80800b06c959c3248f10b Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 8 Feb 2025 13:45:13 +0000 Subject: [PATCH 16/46] Compilation options cleanup --- CMakeLists.txt | 5 ---- cmake/CompilationOptions.cmake | 55 ++++++++++++++++++++++------------ include/CMakeLists.txt | 17 ++++------- tests/CMakeLists.txt | 8 ++--- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 85cac091..5f4fe4b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,11 +35,6 @@ include(Ccache) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND (NOT APPLE)) - add_compile_options(-stdlib=libc++) - add_link_options(-lc++) -endif() - # Phony targets for all supported C++ versions add_custom_target(cxx20) add_custom_target(cxx23) diff --git a/cmake/CompilationOptions.cmake b/cmake/CompilationOptions.cmake index 6effaa1d..579b971c 100644 --- a/cmake/CompilationOptions.cmake +++ b/cmake/CompilationOptions.cmake @@ -1,29 +1,46 @@ # Add compilation options appropriate for the current compiler +# Note, we do not select libc++ like an example below. Instead the user should +# use the CXXFLAGS environment variable for this option. +# +# if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND (NOT APPLE)) +# add_compile_options(-stdlib=libc++) +# add_link_options(-lc++) +# endif() + + function(append_compilation_options) - set(options WARNINGS OPTIMIZATION) - set(oneValueArgs NAME) - cmake_parse_arguments(Options "${options}" "${oneValueArgs}" "" ${ARGN}) + set(options WARNINGS OPTIMIZATION INTERFACE) + set(Options_NAME ${ARGV0}) + cmake_parse_arguments(Options "${options}" "" "" ${ARGN}) if(NOT DEFINED Options_NAME) - message(FATAL_ERROR "NAME must be set") + message(FATAL_ERROR "NAME must be set") endif() if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") - if(Options_WARNINGS) - target_compile_options(${Options_NAME} PRIVATE /W4) - endif() - - if(Options_OPTIMIZATION) - target_compile_options(${Options_NAME} PRIVATE $,/Od,/Ox>) - endif() - else() - if(Options_WARNINGS) - target_compile_options(${Options_NAME} PRIVATE -Wall -Wextra -Wpedantic) - endif() - - if(Options_OPTIMIZATION) - target_compile_options(${Options_NAME} PRIVATE $,-O0,-O2>) - endif() + if(Options_WARNINGS) + target_compile_options(${Options_NAME} PRIVATE /W4) + endif() + + if(Options_OPTIMIZATION) + target_compile_options(${Options_NAME} PRIVATE $,/Od,/Ox>) + endif() + elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?Clang") + if(Options_WARNINGS) + target_compile_options(${Options_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror) + endif() + + if(Options_OPTIMIZATION) + target_compile_options(${Options_NAME} PRIVATE $,-O0,-O2>) + endif() + + if(Options_INTERFACE) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_compile_options(${Options_NAME} INTERFACE -Wno-non-template-friend) + endif() + + target_compile_options(${Options_NAME} INTERFACE -Wno-missing-braces) + endif() endif() endfunction() diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index 0756c5f5..bcc31a49 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -4,6 +4,8 @@ project(include) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) +include(CompilationOptions) + ### include/pfn # Pls keep the filenames sorted @@ -17,18 +19,14 @@ target_sources(include_pfn INTERFACE FILE_SET include_pfn_headers TYPE HEADERS FILES ${INCLUDE_PFN_HEADERS}) +append_compilation_options(include_pfn INTERFACE) target_include_directories(include_pfn SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") - target_compile_options(include_pfn INTERFACE -Wno-non-template-friend) -endif() - install(TARGETS include_pfn FILE_SET include_pfn_headers DESTINATION include) include(TargetGenerator) -include(CompilationOptions) # Generate sentinel target for each individual header, as a basic sanity check foreach(mode 20 23) @@ -43,7 +41,7 @@ foreach(mode 20 23) SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" DEPENDENCIES include_pfn ) - append_compilation_options(NAME "${target}" WARNINGS) + append_compilation_options("${target}" WARNINGS) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") @@ -91,11 +89,8 @@ target_sources(include_fn INTERFACE FILE_SET include_fn_headers TYPE HEADERS FILES ${INCLUDE_FN_HEADERS}) +append_compilation_options(include_fn INTERFACE) target_include_directories(include_fn SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) - -if(NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC") - target_compile_options(include_fn INTERFACE -Wno-missing-braces) -endif() # TODO uncomment when include_pfn is ready # target_link_libraries(include_fn INTERFACE include_pfn) @@ -117,7 +112,7 @@ foreach(mode 23) SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" DEPENDENCIES include_fn ) - append_compilation_options(NAME "${target}" WARNINGS) + append_compilation_options("${target}" WARNINGS) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aa51d8e8..c1e67033 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,7 +27,7 @@ foreach(mode 23) SOURCE_ROOT "${CMAKE_BINARY_DIR}/sentinel" DEPENDENCIES include_fn tests_util ) - append_compilation_options(NAME "${target}" WARNINGS) + append_compilation_options("${target}" WARNINGS) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") @@ -56,7 +56,7 @@ foreach(mode 20 23) SOURCE "${source}" DEPENDENCIES include_pfn tests_util Catch2::Catch2WithMain ) - append_compilation_options(NAME "${target}" WARNINGS OPTIMIZATION) + append_compilation_options("${target}" WARNINGS OPTIMIZATION) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") @@ -112,7 +112,7 @@ foreach(mode 23) SOURCE "${source}" DEPENDENCIES include_fn tests_util Catch2::Catch2WithMain ) - append_compilation_options(NAME "${target}" WARNINGS OPTIMIZATION) + append_compilation_options("${target}" WARNINGS OPTIMIZATION) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") @@ -139,7 +139,7 @@ foreach(mode 23) add_executable("${target}" ${TESTS_EXAMPLES_SOURCES}) target_link_libraries("${target}" include_fn Catch2::Catch2WithMain) - append_compilation_options(NAME "${target}" WARNINGS) + append_compilation_options("${target}" WARNINGS) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") From 261b8ed2588f524f408325e6262a39eaa2bd2912 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 2 Mar 2025 17:46:32 +0000 Subject: [PATCH 17/46] Remove static state from test helper --- cmake/CompilationOptions.cmake | 2 +- tests/fn/choice.cpp | 125 +++++++++++++++------ tests/pfn/expected.cpp | 192 ++++++++++++--------------------- tests/util/helper_types.hpp | 50 +++++---- 4 files changed, 192 insertions(+), 177 deletions(-) diff --git a/cmake/CompilationOptions.cmake b/cmake/CompilationOptions.cmake index 579b971c..c97258c7 100644 --- a/cmake/CompilationOptions.cmake +++ b/cmake/CompilationOptions.cmake @@ -28,7 +28,7 @@ function(append_compilation_options) endif() elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?Clang") if(Options_WARNINGS) - target_compile_options(${Options_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror) + target_compile_options(${Options_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror -Wno-redundant-move) endif() if(Options_OPTIMIZATION) diff --git a/tests/fn/choice.cpp b/tests/fn/choice.cpp index d90ba4b0..8ed92002 100644 --- a/tests/fn/choice.cpp +++ b/tests/fn/choice.cpp @@ -50,7 +50,8 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(std::same_as().value())>); static_assert(std::same_as().value())>); - type s{helper{42}}; + type s{helper{0}}; + s.value().get_ptr()->v = 42; constexpr auto fn = fn::overload{ [](auto &&) -> int { throw 0; }, // [](helper &o) -> int { return o.v + 1; }, [](helper const &o) -> int { return o.v + 2; }, @@ -151,43 +152,77 @@ TEST_CASE("choice non-monadic functionality", "[choice]") WHEN("constexpr move from rvalue") { - using T = fn::choice; + using T = fn::choice; constexpr auto fn = [](auto i) constexpr noexcept -> T { return {std::move(i)}; }; constexpr auto a = fn(true); static_assert(std::is_same_v); static_assert(a.has_value()); + static_assert(a.value() == fn::sum{true}); - constexpr auto b = fn(12); + constexpr auto b = fn(helper{19}); static_assert(std::is_same_v); - static_assert(b.has_value()); + static_assert(b.has_value()); + static_assert(b.value().get_ptr()->v == 19 * helper::from_rval); } WHEN("move from rvalue") { using T = fn::choice; constexpr auto fn = [](auto i) noexcept -> T { return {std::move(i)}; }; - auto const before = helper::witness; - auto const a = fn(helper{12}); - CHECK(helper::witness == (before + 12) * helper::from_rval); + auto const a = fn(helper{9}); + static_assert(std::is_same_v); + CHECK(a.has_value()); + CHECK(a.value().get_ptr()->v == 9 * helper::from_rval); + + auto b = fn(true); + static_assert(std::is_same_v); + CHECK(b.has_value()); + CHECK(b.value() == fn::sum{true}); + } + + WHEN("constexpr move from const rvalue") + { + using T = fn::choice; + constexpr auto fn = [](auto const i) constexpr noexcept -> T { return {std::move(i)}; }; + constexpr auto a = fn(true); + static_assert(std::is_same_v); + static_assert(a.has_value()); + static_assert(a.value() == fn::sum{true}); + + constexpr auto b = fn(helper{17}); + static_assert(std::is_same_v); + static_assert(b.has_value()); + static_assert(b.value().get_ptr()->v == 17 * helper::from_rval_const); + } + + WHEN("move from const rvalue") + { + using T = fn::choice; + constexpr auto fn = [](auto const i) noexcept -> T { return {std::move(i)}; }; + auto const a = fn(helper{7}); static_assert(std::is_same_v); CHECK(a.has_value()); + CHECK(a.value().get_ptr()->v == 7 * helper::from_rval_const); auto b = fn(true); static_assert(std::is_same_v); CHECK(b.has_value()); + CHECK(b.value() == fn::sum{true}); } WHEN("constexpr copy from lvalue") { - using T = fn::choice; + using T = fn::choice; constexpr auto fn = [](auto i) constexpr noexcept -> T { return {i}; }; constexpr auto a = fn(true); static_assert(std::is_same_v); static_assert(a.has_value()); + static_assert(a.value() == fn::sum{true}); - constexpr auto b = fn(12); + constexpr auto b = fn(helper{17}); static_assert(std::is_same_v); - static_assert(b.has_value()); + static_assert(b.has_value()); + static_assert(b.value().get_ptr()->v == 17 * helper::from_lval); } WHEN("copy from lvalue") @@ -197,36 +232,64 @@ TEST_CASE("choice non-monadic functionality", "[choice]") auto const a = fn(true); static_assert(std::is_same_v); CHECK(a.has_value()); + CHECK(a.value() == fn::sum{true}); - auto const before = helper::witness; auto b = fn(helper{13}); - CHECK(helper::witness == (before + 13) * helper::from_lval_const); static_assert(std::is_same_v); CHECK(b.has_value()); + CHECK(b.value().get_ptr()->v == 13 * helper::from_lval); + } + + WHEN("constexpr copy from const lvalue") + { + using T = fn::choice; + constexpr auto fn = [](auto const i) constexpr noexcept -> T { return {i}; }; + constexpr auto a = fn(true); + static_assert(std::is_same_v); + static_assert(a.has_value()); + static_assert(a.value() == fn::sum{true}); + + constexpr auto b = fn(helper{15}); + static_assert(std::is_same_v); + static_assert(b.has_value()); + static_assert(b.value().get_ptr()->v == 15 * helper::from_lval_const); + } + + WHEN("copy from const lvalue") + { + using T = fn::choice; + constexpr auto fn = [](auto const i) noexcept -> T { return {i}; }; + auto const a = fn(true); + static_assert(std::is_same_v); + CHECK(a.has_value()); + CHECK(a.value() == fn::sum{true}); + + auto b = fn(helper{5}); + static_assert(std::is_same_v); + CHECK(b.has_value()); + CHECK(b.value().get_ptr()->v == 5 * helper::from_lval_const); } WHEN("copy ctor") { using T = fn::choice; - auto const h = helper{23}; - auto const a = T{h}; - auto const before = helper::witness; - auto const b = a; - CHECK(helper::witness == before * helper::from_lval_const); + auto a = T{helper{0}}; + a.value().get_ptr()->v = 23; + auto const b = std::as_const(a); + CHECK(b.has_value()); - CHECK(b.value() == fn::sum{h}); + CHECK(b.value().get_ptr()->v == 23 * helper::from_lval_const); } WHEN("move ctor") { using T = fn::choice; - auto const h = helper{29}; - auto a = T{h}; - auto const before = helper::witness; + auto a = T{helper{0}}; + a.value().get_ptr()->v = 29; auto const b = std::move(a); - CHECK(helper::witness == before * helper::from_rval); + CHECK(b.has_value()); - CHECK(b.value() == fn::sum{h}); + CHECK(b.value().get_ptr()->v == 29 * helper::from_rval); } } @@ -235,19 +298,17 @@ TEST_CASE("choice non-monadic functionality", "[choice]") using T = fn::choice; WHEN("move from rvalue") { - fn::sum h{helper{17}}; - auto const before = helper::witness; + fn::sum h{helper{0}}; + h.get_ptr()->v = 17; T const a{std::move(h)}; - CHECK(helper::witness == before * helper::from_rval); - CHECK(a == h); + CHECK(a.value().get_ptr()->v == 17 * helper::from_rval); } WHEN("copy from const lvalue") { - fn::sum h{helper{19}}; - auto const before = helper::witness; - T const a{h}; - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(a == h); + fn::sum h{helper{0}}; + h.get_ptr()->v = 19; + T const a{std::as_const(h)}; + CHECK(a.value().get_ptr()->v == 19 * helper::from_lval_const); } } diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 1377dcf2..318904f1 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -73,118 +73,102 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") SECTION("copy/move constructors") { + T b{0}; + SECTION("lval") { - auto const before = helper::witness; - T c = a; - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(c.error() == 12); + b.error().v = 11; + T c = b; + CHECK(c.error().v == 11 * helper::from_lval_const); } SECTION("lval const") { - T const b{13}; - auto const before = helper::witness; - T c = b; - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(c.error() == 13); + b.error().v = 13; + T c = std::as_const(b); + CHECK(c.error().v == 13 * helper::from_lval_const); } SECTION("rval") { - auto const before = helper::witness; - T c = std::move(a); - CHECK(helper::witness == before * helper::from_rval); - CHECK(c.error() == 12); + b.error().v = 17; + T c = std::move(b); + CHECK(c.error().v == 17 * helper::from_rval); } SECTION("rval cont") { - T const b{17}; - auto const before = helper::witness; - T c = std::move(b); - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(c.error() == 17); + b.error().v = 19; + T c = std::move(std::as_const(b)); + CHECK(c.error().v == 19 * helper::from_lval_const); } } SECTION("assignment") { + T a{12}; + T b{0}; + SECTION("lval") { - T b{11}; - auto const before = helper::witness; + b.error().v = 11; a = b; - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(a.error() == 11); + CHECK(a.error().v == 11 * helper::from_lval_const); } SECTION("lval const") { - T const b{11}; - auto const before = helper::witness; - a = b; - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(a.error() == 11); + b.error().v = 13; + a = std::as_const(b); + CHECK(a.error().v == 13 * helper::from_lval_const); } SECTION("rval") { - T b{11}; - auto const before = helper::witness; + b.error().v = 17; a = std::move(b); - CHECK(helper::witness == before * helper::from_rval); - CHECK(a.error() == 11); + CHECK(a.error().v == 17 * helper::from_rval); } SECTION("rval const") { - T const b{11}; - auto const before = helper::witness; - a = std::move(b); - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(a.error() == 11); + b.error().v = 19; + a = std::move(std::as_const(b)); + CHECK(a.error().v == 19 * helper::from_lval_const); } } SECTION("accessors") { helper c{0}; + T b{0}; SECTION("lval") { - T b{11}; - auto const before = helper::witness; + b.error().v = 11; c = b.error(); - CHECK(helper::witness == before * helper::from_lval); - CHECK(c == 11); + CHECK(c.v == 11 * helper::from_lval); } SECTION("lval const") { - T const b{13}; - auto const before = helper::witness; - c = b.error(); - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(c == 13); + b.error().v = 13; + c = std::as_const(b).error(); + CHECK(c.v == 13 * helper::from_lval_const); } SECTION("rval") { - T b{17}; - auto const before = helper::witness; + b.error().v = 17; c = std::move(b).error(); - CHECK(helper::witness == before * helper::from_rval); - CHECK(c == 17); + CHECK(c.v == 17 * helper::from_rval); } SECTION("rval const") { - T const b{19}; - auto const before = helper::witness; - c = std::move(b).error(); - CHECK(helper::witness == before * helper::from_rval_const); - CHECK(c == 19); + b.error().v = 19; + c = std::move(std::as_const(b)).error(); + CHECK(c.v == 19 * helper::from_rval_const); } } @@ -259,32 +243,23 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SECTION("no conversion, CTAD") { - auto const before = helper::witness; unexpected c{helper{2}}; - CHECK(helper::witness == (before + 2) * helper::from_rval); - CHECK(c.error() == helper{2}); - CHECK(c == unexpected{2}); + CHECK(c.error().v == 2 * helper::from_rval); static_assert(std::is_same_v>); static_assert(std::is_nothrow_constructible_v); } SECTION("conversion, no CTAD") { - auto const before = helper::witness; unexpected c(3); - CHECK(helper::witness == before + 3); CHECK(c.error().v == 3); - CHECK(c == unexpected{3}); static_assert(std::is_nothrow_constructible_v); } SECTION("in-place, no CTAD") { - auto const before = helper::witness; unexpected c(std::in_place, 3, 5); - CHECK(helper::witness == before + 3 * 5); - CHECK(c.error() == helper{3, 5}); - CHECK(c == unexpected{15}); + CHECK(c.error().v == 3 * 5); static_assert(std::is_nothrow_constructible_v); } @@ -292,12 +267,9 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") { SECTION("forwarded args") { - auto const before = helper::witness; unexpected c(std::in_place, {3.0, 5.0}, 7, 11); auto const d = 3 * 5 * 7 * 11; - CHECK(helper::witness == before + d); - CHECK(c.error() == helper{d}); - CHECK(c == unexpected{helper{d}}); + CHECK(c.error().v == d); static_assert( not std::is_nothrow_constructible_v, int, int>); static_assert(std::is_constructible_v, int, int>); @@ -305,11 +277,8 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SECTION("no forwarded args") { - auto const before = helper::witness; unexpected c(std::in_place, {2.0, 2.5}); - CHECK(helper::witness == before + 5); - CHECK(c.error() == helper{5}); - CHECK(c == unexpected{5}); + CHECK(c.error().v == 5); static_assert(not std::is_nothrow_constructible_v>); static_assert(std::is_constructible_v>); } @@ -317,7 +286,6 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SECTION("exception thrown") { unexpected t{13}; - auto const before = helper::witness; try { t = unexpected{std::in_place, {2.0, 1.0, 0.0}, 5}; FAIL(); @@ -325,107 +293,89 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SUCCEED(); } CHECK(t.error().v == 13); - CHECK(helper::witness == before); } } } SECTION("accessors") { - helper v{1}; + helper a{1}; SECTION("lval") { unexpected t{13}; - auto before = helper::witness; - v = t.error(); - CHECK(helper::witness == before * helper::from_lval); - CHECK(v == helper{13}); + a = t.error(); + CHECK(a.v == 13 * helper::from_lval); } SECTION("lval const") { unexpected const t{17}; - auto before = helper::witness; - v = t.error(); - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(v == helper{17}); + a = t.error(); + CHECK(a.v == 17 * helper::from_lval_const); } SECTION("rval") { unexpected t{19}; - auto before = helper::witness; - v = std::move(t).error(); - CHECK(helper::witness == before * helper::from_rval); - CHECK(v == helper{19}); + a = std::move(t).error(); + CHECK(a.v == 19 * helper::from_rval); } SECTION("rval const") { unexpected const t{23}; - auto before = helper::witness; - v = std::move(t).error(); - CHECK(helper::witness == before * helper::from_rval_const); - CHECK(v == helper{23}); + a = std::move(t).error(); + CHECK(a.v == 23 * helper::from_rval_const); } } SECTION("assignment") { - unexpected v{0}; + unexpected a{0}; SECTION("lval") { unexpected t{13}; - auto before = helper::witness; - v = t; - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(v.error() == helper{13}); + a = t; + CHECK(a.error().v == 13 * helper::from_lval_const); } SECTION("lval const") { unexpected const t{17}; - auto before = helper::witness; - v = t; - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(v.error() == helper{17}); + a = t; + CHECK(a.error().v == 17 * helper::from_lval_const); } SECTION("rval") { unexpected t{19}; - auto before = helper::witness; - v = std::move(t); - CHECK(helper::witness == before * helper::from_rval); - CHECK(v.error() == helper{19}); + a = std::move(t); + CHECK(a.error().v == 19 * helper::from_rval); } SECTION("rval const") { unexpected const t{23}; - auto before = helper::witness; - v = std::move(t); - CHECK(helper::witness == before * helper::from_lval_const); - CHECK(v.error() == helper{23}); + a = std::move(t); + CHECK(a.error().v == 23 * helper::from_lval_const); } } SECTION("swap") { - unexpected v{2}; - unexpected w{helper{3}}; - auto before = helper::witness; - v.swap(w); - CHECK(helper::witness == before * helper::swapped); - CHECK(v == unexpected{helper{3}}); - CHECK(w == unexpected{helper{2}}); - w.error() = helper{11}; - before = helper::witness; - swap(v, w); - CHECK(v.error() == helper{11}); - CHECK(w.error() == helper(3)); + unexpected a{0}; + a.error().v = 2; + unexpected b{helper{0}}; + b.error().v = 3; + a.swap(b); + CHECK(a.error().v == 3 * helper::swapped); + CHECK(b.error().v == 2 * helper::swapped); + b.error() = helper{11}; + swap(a, b); + CHECK(a.error().v == 11 * helper::from_rval * helper::swapped); + CHECK(b.error().v == 3 * helper::swapped * helper::swapped); } SECTION("constexpr all, CTAD") diff --git a/tests/util/helper_types.hpp b/tests/util/helper_types.hpp index 3fd47415..33c9a89a 100644 --- a/tests/util/helper_types.hpp +++ b/tests/util/helper_types.hpp @@ -3,6 +3,7 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC +#include #include #include #include @@ -10,7 +11,6 @@ #include template struct helper_t { - static inline int witness = 0; int v = {}; // Use prime numbers to record Foo states in witness @@ -25,64 +25,63 @@ template struct helper_t { // No default constructor helper_t() = delete; - ~helper_t() noexcept = default; + constexpr ~helper_t() noexcept = default; - bool operator==(helper_t const &) const noexcept = default; + constexpr bool operator==(helper_t const &) const noexcept = default; // Assignment operators will multiply witness by a prime - helper_t &operator=(helper_t &o) noexcept + constexpr helper_t &operator=(helper_t &o) noexcept { v = o.v; - witness *= from_lval; + v *= from_lval; return *this; } - helper_t &operator=(helper_t const &o) noexcept + constexpr helper_t &operator=(helper_t const &o) noexcept { v = o.v; - witness *= from_lval_const; + v *= from_lval_const; return *this; } - helper_t &operator=(helper_t &&o) noexcept + constexpr helper_t &operator=(helper_t &&o) noexcept { v = o.v; - witness *= from_rval; + v *= from_rval; return *this; } - helper_t &operator=(helper_t const &&o) noexcept + constexpr helper_t &operator=(helper_t const &&o) noexcept { v = o.v; - witness *= from_rval_const; + v *= from_rval_const; return *this; } - // Every constructor will increase witness, including copy/move - helper_t(helper_t &&o) noexcept : v(o.v) { witness *= from_rval; } - helper_t(helper_t const &o) noexcept : v(o.v) { witness *= from_lval_const; } + constexpr helper_t(helper_t &o) noexcept : v(o.v) { v *= from_lval; } + constexpr helper_t(helper_t const &o) noexcept : v(o.v) { v *= from_lval_const; } + constexpr helper_t(helper_t &&o) noexcept : v(o.v) { v *= from_rval; } + constexpr helper_t(helper_t const &&o) noexcept : v(o.v) { v *= from_rval_const; } - helper_t(std::integral auto... a) noexcept + constexpr helper_t(std::integral auto... a) noexcept requires(sizeof...(a) > 0) // intentionally implicit when sizeof...(a) == 1 : v((1 * ... * a)) { - witness += v; } template // - explicit helper_t(std::integral_constant) noexcept : v(static_cast(T::value)) + constexpr explicit helper_t(std::integral_constant) noexcept : v(static_cast(T::value)) { - witness += v; } // Potentially throwing constructor - helper_t(std::initializer_list list, std::integral auto... a) noexcept(false) : v(init(list, a...)) // + constexpr helper_t(std::initializer_list list, std::integral auto... a) noexcept(false) + : v(init(list, a...)) // { - witness += v; } // ... and the actual exception being thrown - static int init(std::initializer_list l, auto &&...a) noexcept(false) + static constexpr int init(std::initializer_list l, auto &&...a) noexcept(false) { double ret = (1 * ... * a); for (auto d : l) { @@ -92,13 +91,18 @@ template struct helper_t { } return static_cast(ret); } + + // Disable comparison operators; compare .v instead + friend bool operator==(helper_t, std::integral auto) = delete; + friend std::strong_ordering operator<=>(helper_t, helper_t) = delete; }; // Swap will also multiply witness by a prime -template void swap(helper_t &l, helper_t &r) +template constexpr void swap(helper_t &l, helper_t &r) { std::swap(l.v, r.v); - helper_t::witness *= helper_t::swapped; + l.v *= l.swapped; + r.v *= r.swapped; } using helper = helper_t<0>; From fda668ff5e74e0fdd9bc4ca452aa0283834fbd69 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 1 Mar 2025 21:02:50 +0000 Subject: [PATCH 18/46] More expected implementation and unit tests --- include/pfn/expected.hpp | 290 +++++++++++++++++--- tests/pfn/expected.cpp | 517 +++++++++++++++++++++++++++++++++--- tests/util/helper_types.hpp | 2 +- 3 files changed, 739 insertions(+), 70 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 4bc7d4d8..4bafb094 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -6,8 +6,10 @@ #ifndef INCLUDE_PFN_EXPECTED #define INCLUDE_PFN_EXPECTED +#include #include #include +#include #include #include @@ -20,11 +22,19 @@ // Also defined in fn/detail/fwd_macro.hpp but pfn/* headers are standalone #define FWD(...) static_cast(__VA_ARGS__) +#ifdef ASSERT +#pragma push_macro("ASSERT") +#define INCLUDE_PFN_EXPECTED__POP_ASSERT +#undef ASSERT +#endif + +#define ASSERT(...) assert((__VA_ARGS__) == true); + namespace pfn { template class bad_expected_access; -template <> class bad_expected_access : public std::exception { +template <> class bad_expected_access : public ::std::exception { protected: bad_expected_access() noexcept = default; bad_expected_access(bad_expected_access const &) noexcept = default; @@ -47,8 +57,8 @@ template class bad_expected_access : public bad_expected_access [[nodiscard]] char const *what() const noexcept override { return bad_expected_access::what(); }; E &error() & noexcept { return e_; } E const &error() const & noexcept { return e_; } - E &&error() && noexcept { return std::move(e_); } - E const &&error() const && noexcept { return std::move(e_); } + E &&error() && noexcept { return ::std::move(e_); } + E const &&error() const && noexcept { return ::std::move(e_); } private: E e_; @@ -62,7 +72,7 @@ constexpr inline unexpect_t unexpect{}; template class unexpected; namespace detail { -template constexpr bool _is_some_unexpected = false; +template constexpr bool _is_some_unexpected = false; template constexpr bool _is_some_unexpected<::pfn::unexpected> = true; template @@ -98,7 +108,7 @@ template class unexpected { } template - constexpr explicit unexpected(std::in_place_t, std::initializer_list i, Args &&...a) noexcept( + constexpr explicit unexpected(std::in_place_t, ::std::initializer_list i, Args &&...a) noexcept( ::std::is_nothrow_constructible_v &, Args...>) requires ::std::is_constructible_v &, Args...> : e_(i, FWD(a)...) @@ -138,13 +148,50 @@ template class unexpected { template unexpected(E) -> unexpected; template class expected; +namespace detail { +template constexpr bool _is_some_expected = false; +template constexpr bool _is_some_expected<::pfn::expected> = true; +} // namespace detail // declare void specialization template - requires std::is_void_v + requires ::std::is_void_v class expected; template class expected { +private: + template + using _can_convert_detail = ::std::bool_constant< // + not(::std::is_same_v && ::std::is_same_v) // + && ::std::is_constructible_v // + && ::std::is_constructible_v // + && not ::std::is_constructible_v, expected &> // + && not ::std::is_constructible_v, expected> // + && not ::std::is_constructible_v, expected const &> // + && not ::std::is_constructible_v, expected const> // + && (::std::is_same_v> // + || ( // + not ::std::is_constructible_v &> // + && not ::std::is_constructible_v> // + && not ::std::is_constructible_v const &> // + && not ::std::is_constructible_v const> // + && not ::std::is_convertible_v &, T> // + && not ::std::is_convertible_v &&, T> // + && not ::std::is_convertible_v const &, T> // + && not ::std::is_convertible_v const &&, T>))>; + + template using _can_copy_convert = _can_convert_detail; + template using _can_move_convert = _can_convert_detail; + + template + using _can_convert = ::std::bool_constant< // + not ::std::is_same_v<::std::remove_cvref_t, ::std::in_place_t> + && not ::std::is_same_v> // + && not detail::_is_some_unexpected<::std::remove_cvref_t> // + && ::std::is_constructible_v // + && (not ::std::is_same_v> // + || not detail::_is_some_expected<::std::remove_cvref_t>)>; + public: using value_type = T; using error_type = E; @@ -153,24 +200,142 @@ template class expected { template using rebind = expected; // [expected.object.cons], constructors - constexpr expected(); - constexpr expected(expected const &); - constexpr expected(expected &&) noexcept(/* TODO */ false); - template constexpr explicit(/* TODO */ false) expected(expected const &); - template constexpr explicit(/* TODO */ false) expected(expected &&); + constexpr expected() // + noexcept(::std::is_nothrow_default_constructible_v) // extension + requires ::std::is_default_constructible_v + : v_(), set_(true) + { + } + + constexpr expected(expected const &) = delete; + constexpr expected(expected const &s) // + requires(::std::is_copy_constructible_v && ::std::is_copy_constructible_v + && ::std::is_trivially_copy_constructible_v && ::std::is_trivially_copy_constructible_v) + = default; + constexpr expected(expected const &s) // + noexcept(::std::is_nothrow_copy_constructible_v && ::std::is_nothrow_copy_constructible_v) // extension + requires(::std::is_copy_constructible_v && ::std::is_copy_constructible_v + && (not ::std::is_trivially_copy_constructible_v || not ::std::is_trivially_copy_constructible_v)) + : set_(s.set_) + { + if (set_) + ::std::construct_at(&v_, s.v_); + else + ::std::construct_at(&e_, s.e_); + } + + constexpr expected(expected &&s) + requires(::std::is_move_constructible_v && ::std::is_move_constructible_v + && ::std::is_trivially_move_constructible_v && ::std::is_trivially_move_constructible_v) + = default; + constexpr expected(expected &&s) // + noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_move_constructible_v) // required + requires(::std::is_move_constructible_v && ::std::is_move_constructible_v + && (not ::std::is_trivially_move_constructible_v || not ::std::is_trivially_move_constructible_v)) + : set_(s.set_) + { + if (set_) + ::std::construct_at(&v_, ::std::move(s.v_)); + else + ::std::construct_at(&e_, ::std::move(s.e_)); + } + + template + constexpr explicit(not ::std::is_convertible_v || not ::std::is_convertible_v) + expected(expected const &s) // + noexcept(::std::is_nothrow_constructible_v + && ::std::is_nothrow_constructible_v) // extension + requires(_can_copy_convert::value) + : set_(s.set_) + { + if (set_) + ::std::construct_at(&v_, s.v_); + else + ::std::construct_at(&e_, s.e_); + } + + template + constexpr explicit(not ::std::is_convertible_v || not ::std::is_convertible_v) + expected(expected &&s) // + noexcept(::std::is_nothrow_constructible_v && ::std::is_nothrow_constructible_v) // extension + requires(_can_move_convert::value) + : set_(s.set_) + { + if (set_) + ::std::construct_at(&v_, ::std::move(s.v_)); + else + ::std::construct_at(&e_, ::std::move(s.e_)); + } - template constexpr explicit(/* TODO */ false) expected(U &&v); + template > + constexpr explicit(not ::std::is_convertible_v) expected(U &&v) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires(_can_convert::value) + : v_(FWD(v)), set_(true) + { + } template constexpr explicit(/* TODO */ false) expected(unexpected const &); template constexpr explicit(/* TODO */ false) expected(unexpected &&); - template constexpr explicit expected(std::in_place_t, Args &&...); - template constexpr explicit expected(std::in_place_t, std::initializer_list, Args &&...); - template constexpr explicit expected(unexpect_t, Args &&...); - template constexpr explicit expected(unexpect_t, std::initializer_list, Args &&...); + template + constexpr explicit expected(std::in_place_t, Args &&...a) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires ::std::is_constructible_v + : v_(FWD(a)...), set_(true) + { + } + template + constexpr explicit expected(std::in_place_t, ::std::initializer_list il, Args &&...a) // + noexcept(::std::is_nothrow_constructible_v &, Args...>) // extension + requires ::std::is_constructible_v &, Args...> + : v_(il, FWD(a)...), set_(true) + { + } + template + constexpr explicit expected(unexpect_t, Args &&...a) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires ::std::is_constructible_v + : e_(FWD(a)...), set_(false) + { + } + template + constexpr explicit expected(unexpect_t, ::std::initializer_list il, Args &&...a) // + noexcept(::std::is_nothrow_constructible_v &, Args...>) // extension + requires ::std::is_constructible_v &, Args...> + : e_(il, FWD(a)...), set_(false) + { + } // [expected.object.dtor], destructor - constexpr ~expected(); + constexpr ~expected() noexcept + requires(::std::is_trivially_destructible_v && ::std::is_trivially_destructible_v) + = default; + constexpr ~expected() // + noexcept(::std::is_nothrow_destructible_v) // extension + requires(::std::is_trivially_destructible_v && not ::std::is_trivially_destructible_v) + { + if (not set_) + e_.~E(); + // else T is trivially destructible, no need to do anything + } + constexpr ~expected() // + noexcept(::std::is_nothrow_destructible_v) // extension + requires(not ::std::is_trivially_destructible_v && ::std::is_trivially_destructible_v) + { + if (set_) + v_.~T(); + // else E is trivially destructible, no need to do anything + } + constexpr ~expected() // + noexcept(::std::is_nothrow_destructible_v && ::std::is_nothrow_destructible_v) // extension + requires(not ::std::is_trivially_destructible_v && not ::std::is_trivially_destructible_v) + { + if (set_) + v_.~T(); + else + e_.~E(); + } // [expected.object.assign], assignment constexpr expected &operator=(expected const &); @@ -187,22 +352,67 @@ template class expected { constexpr friend void swap(expected &x, expected &y) noexcept(noexcept(x.swap(y))); // [expected.object.obs], observers - constexpr T const *operator->() const noexcept; - constexpr T *operator->() noexcept; - constexpr T const &operator*() const & noexcept; - constexpr T &operator*() & noexcept; - constexpr T const &&operator*() const && noexcept; - constexpr T &&operator*() && noexcept; - constexpr explicit operator bool() const noexcept; - constexpr bool has_value() const noexcept; - constexpr T const &value() const &; // freestanding-deleted - constexpr T &value() &; // freestanding-deleted - constexpr T const &&value() const &&; // freestanding-deleted - constexpr T &&value() &&; // freestanding-deleted - constexpr E const &error() const & noexcept; - constexpr E &error() & noexcept; - constexpr E const &&error() const && noexcept; - constexpr E &&error() && noexcept; + constexpr T const *operator->() const noexcept + { + ASSERT(set_); + return addressof(v_); + } + constexpr T *operator->() noexcept + { + ASSERT(set_); + return addressof(v_); + } + constexpr T const &operator*() const & noexcept { return *(this->operator->()); } + constexpr T &operator*() & noexcept { return *(this->operator->()); } + constexpr T const &&operator*() const && noexcept { return ::std::move(*(this->operator->())); } + constexpr T &&operator*() && noexcept { return ::std::move(*(this->operator->())); } + constexpr explicit operator bool() const noexcept { return set_; } + constexpr bool has_value() const noexcept { return set_; } + constexpr T const &value() const & + { + if (!set_) + throw bad_expected_access(::std::as_const(e_)); + return v_; + } + constexpr T &value() & + { + if (!set_) + throw bad_expected_access(::std::as_const(e_)); + return v_; + } + constexpr T const &&value() const && + { + if (!set_) + throw bad_expected_access(::std::as_const(e_)); + return ::std::move(v_); + } + constexpr T &&value() && + { + if (!set_) + throw bad_expected_access(::std::as_const(e_)); + return ::std::move(v_); + } + constexpr E const &error() const & noexcept + { + ASSERT(!set_); + return e_; + } + constexpr E &error() & noexcept + { + ASSERT(!set_); + return e_; + } + constexpr E const &&error() const && noexcept + { + ASSERT(!set_); + return ::std::move(e_); + } + constexpr E &&error() && noexcept + { + ASSERT(!set_); + return ::std::move(e_); + } + template constexpr T value_or(U &&) const &; template constexpr T value_or(U &&) &&; template constexpr E error_or(G &&) const &; @@ -228,7 +438,7 @@ template class expected { // [expected.object.eq], equality operators template - requires(!std::is_void_v) + requires(not ::std::is_void_v) constexpr friend bool operator==(expected const &x, expected const &y); template constexpr friend bool operator==(expected const &, const T2 &); template constexpr friend bool operator==(expected const &, unexpected const &); @@ -242,7 +452,7 @@ template class expected { }; template - requires std::is_void_v + requires ::std::is_void_v class expected { public: using value_type = T; @@ -263,7 +473,7 @@ class expected { constexpr explicit expected(std::in_place_t) noexcept; template constexpr explicit expected(unexpect_t, Args &&...); - template constexpr explicit expected(unexpect_t, std::initializer_list, Args &&...); + template constexpr explicit expected(unexpect_t, ::std::initializer_list, Args &&...); // [expected.void.dtor], destructor constexpr ~expected(); @@ -312,7 +522,7 @@ class expected { // [expected.void.eq], equality operators template - requires std::is_void_v + requires ::std::is_void_v constexpr friend bool operator==(expected const &x, expected const &y); template constexpr friend bool operator==(expected const &, unexpected const &); @@ -326,6 +536,12 @@ class expected { } // namespace pfn +#undef ASSERT + +#ifdef INCLUDE_PFN_EXPECTED__POP_ASSERT +#pragma pop_macro("ASSERT") +#endif + #undef FWD #ifdef INCLUDE_PFN_EXPECTED__POP_FWD diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 318904f1..4714f886 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -11,8 +11,9 @@ #include #include +#include -enum Error { unknown, secret = 142, mystery = 176 }; +enum Error { unknown = 1, file_not_found = 5 }; TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") { @@ -63,13 +64,16 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") using T = pfn::bad_expected_access; static_assert(std::is_base_of_v, T>); - T a{12}; - static_assert(noexcept(T{a})); - static_assert(noexcept(T{std::move(a)})); - static_assert(noexcept(a.what())); - static_assert(std::is_same_v); - static_assert(std::is_same_v); - static_assert(std::is_same_v); + SECTION("type and noexcept") + { + T a{12}; + static_assert(noexcept(T{a})); + static_assert(noexcept(T{std::move(a)})); + static_assert(noexcept(a.what())); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + } SECTION("copy/move constructors") { @@ -172,12 +176,16 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") } } - CHECK(std::strcmp(a.what(), "bad access to expected without expected value") == 0); - auto const c = []() { - struct C : pfn::bad_expected_access {}; - return C{}; - }(); - CHECK(a.what() == c.what()); + SECTION("bad_expected_access") + { + T a{12}; + CHECK(std::strcmp(a.what(), "bad access to expected without expected value") == 0); + auto const c = []() { + struct C : pfn::bad_expected_access {}; + return C{}; + }(); + CHECK(a.what() == c.what()); + } } } @@ -224,36 +232,36 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SECTION("constructors") { - SECTION("constexpr, CTAD") + SECTION("CTAD") { - constexpr unexpected c{Error::mystery}; - static_assert(c.error() == Error::mystery); + constexpr unexpected c{Error::file_not_found}; + static_assert(c.error() == Error::file_not_found); static_assert(std::is_same_v const>); static_assert(std::is_nothrow_constructible_v); SUCCEED(); + + { + unexpected c{helper{2}}; + CHECK(c.error().v == 2 * helper::from_rval); + CHECK(c != unexpected{helper(3)}); + static_assert(std::is_same_v>); + static_assert(std::is_nothrow_constructible_v); + } } - SECTION("constexpr, no CTAD") + SECTION("no CTAD") { constexpr unexpected c{42}; static_assert(c.error() == 42); static_assert(std::is_nothrow_constructible_v); SUCCEED(); - } - - SECTION("no conversion, CTAD") - { - unexpected c{helper{2}}; - CHECK(c.error().v == 2 * helper::from_rval); - static_assert(std::is_same_v>); - static_assert(std::is_nothrow_constructible_v); - } - SECTION("conversion, no CTAD") - { - unexpected c(3); - CHECK(c.error().v == 3); - static_assert(std::is_nothrow_constructible_v); + { + unexpected c(3); + CHECK(c.error().v == 3); + CHECK(c == unexpected(std::in_place, 3)); + static_assert(std::is_nothrow_constructible_v); + } } SECTION("in-place, no CTAD") @@ -261,6 +269,9 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") unexpected c(std::in_place, 3, 5); CHECK(c.error().v == 3 * 5); static_assert(std::is_nothrow_constructible_v); + + c.error().v *= helper::from_rval; + CHECK(c == unexpected(std::in_place, helper{15})); } SECTION("in_place, not CTAD, initializer_list, noexcept(false)") @@ -396,3 +407,445 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SUCCEED(); } } + +TEST_CASE("expected", "[expected][polyfill]") +{ + using pfn::bad_expected_access; + using pfn::expected; + using pfn::unexpect; + constexpr bool extension = true; + + SECTION("default ctor") + { + SECTION("unavailable") + { + static_assert(not std::is_default_constructible_v); // prerequisite + static_assert(not std::is_default_constructible_v>); + static_assert(std::is_default_constructible_v>); + SUCCEED(); + } + + SECTION("trivial") + { + using T = expected; + static_assert(std::is_default_constructible_v); + static_assert(extension && std::is_nothrow_constructible_v); + + constexpr T a; + static_assert(a.has_value()); + static_assert(a.value() == 0); + SUCCEED(); + } + + struct A { + constexpr A() noexcept(true) {} + constexpr bool operator==(A const &) const = default; + + private: + int v = 12; + }; + + SECTION("noexcept(true) from value type") + { + using T = expected; + static_assert(std::is_default_constructible_v); + static_assert(extension && std::is_nothrow_constructible_v); + + constexpr T a; + static_assert(a.has_value()); + static_assert(a.value() == A()); + + T b; + CHECK(b.has_value()); + CHECK(b.value() == A()); + } + + struct B { + constexpr B() noexcept(false) {} + int v = 42; + }; + + SECTION("noexcept(false) from value type") + { + using T = expected; + static_assert(std::is_default_constructible_v); + static_assert(not std::is_nothrow_constructible_v); + + constexpr T a; + static_assert(a.has_value()); + static_assert(a.value().v == 42); + + T b; + CHECK(b.has_value()); + CHECK(b.value().v == 42); + } + + SECTION("ignore noexcept from error type") + { + using T = expected; + static_assert(std::is_default_constructible_v); + static_assert(extension && std::is_nothrow_constructible_v); + SUCCEED(); + } + + struct C { + C() noexcept(false) { throw 7; } + }; + + SECTION("exception thrown") + { + try { + expected b; + FAIL(); + } catch (int i) { + CHECK(i == 7); + } + + // not a problem if error type ctor is throwing + using T = expected; + static_assert(std::is_default_constructible_v); + static_assert(extension && std::is_nothrow_constructible_v); + + constexpr T a; + static_assert(a.has_value()); + static_assert(a.value() == 0); + + T b; + CHECK(b.has_value()); + CHECK(b.value() == 0); + } + } + + SECTION("copy, move and dtor") + { + struct U { + U() = default; + U(U const &) = delete; + }; + + SECTION("unavailable") + { + static_assert(not std::is_copy_constructible_v); // prerequisite + static_assert(not std::is_trivially_copy_constructible_v); // prerequisite + static_assert(not std::is_copy_constructible_v>); + static_assert(not std::is_copy_constructible_v>); + static_assert(not std::is_move_constructible_v>); + static_assert(not std::is_move_constructible_v>); + SUCCEED(); + } + + SECTION("trivial") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(std::is_trivially_copy_constructible_v); + static_assert(extension && std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_trivially_move_constructible_v); + static_assert(extension && std::is_nothrow_move_constructible_v); + static_assert(std::is_trivially_destructible_v); + static_assert(std::is_nothrow_destructible_v); + + constexpr T a; + constexpr T b = a; + static_assert(b.has_value() && a.value() == b.value()); + + { + T a(std::in_place, 13); + T b = a; + CHECK(b.has_value()); + CHECK(b.value() == 13); + + T c = std::move(a); + CHECK(c.has_value()); + CHECK(c.value() == 13); + } + } + + SECTION("non-trivial value type") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); + static_assert(extension && std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); + static_assert(extension && std::is_nothrow_move_constructible_v); + static_assert(not std::is_trivially_destructible_v); + static_assert(std::is_nothrow_destructible_v); + + constexpr T a(std::in_place, 11); + constexpr T b = a; + static_assert(b.has_value() && b.value().v == 11 * helper::from_lval_const); + + { + T a(std::in_place, 13); + T b = a; // no overload for lval + CHECK(b.has_value()); + CHECK(b.value().v == 13 * helper::from_lval_const); + + T c = std::as_const(a); + CHECK(b.has_value()); + CHECK(c.value().v == 13 * helper::from_lval_const); + + T d = std::move(std::as_const(a)); // no overload for lval const + CHECK(b.has_value()); + CHECK(d.value().v == 13 * helper::from_lval_const); + + T e = std::move(a); + CHECK(b.has_value()); + CHECK(e.value().v == 13 * helper::from_rval); + } + } + + SECTION("non-trivial error type") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); + static_assert(extension && std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); + static_assert(extension && std::is_nothrow_move_constructible_v); + static_assert(not std::is_trivially_destructible_v); + static_assert(std::is_nothrow_destructible_v); + + constexpr T a(unexpect, 31); + constexpr T b = a; + static_assert(not b.has_value() && b.error().v == 31 * helper::from_lval_const); + + { + T a(unexpect, 33); + T b = a; // no overload for lval + CHECK(not b.has_value()); + CHECK(b.error().v == 33 * helper::from_lval_const); + + T c = std::as_const(a); + CHECK(not b.has_value()); + CHECK(c.error().v == 33 * helper::from_lval_const); + + T d = std::move(std::as_const(a)); // no overload for lval const + CHECK(not b.has_value()); + CHECK(d.error().v == 33 * helper::from_lval_const); + + T e = std::move(a); + CHECK(not b.has_value()); + CHECK(e.error().v == 33 * helper::from_rval); + } + } + + SECTION("non-trivial both") + { + using T = expected>; + static_assert(std::is_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); + static_assert(extension && std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); + static_assert(extension && std::is_nothrow_move_constructible_v); + static_assert(not std::is_trivially_destructible_v); + static_assert(std::is_nothrow_destructible_v); + + { + T a(std::in_place, 41); + T b = a; // no overload for lval + CHECK(b.has_value()); + CHECK(b.value().v == 41 * helper::from_lval_const); + } + + { + T a(unexpect, 43); + T b = a; // no overload for lval + CHECK(not b.has_value()); + CHECK(b.error().v == 43 * helper::from_lval_const); + } + } + + struct B { + int v; + constexpr B(int v) : v(v) {} + constexpr B(B const &) noexcept(false) = default; + constexpr B(B &&) noexcept(false) = default; + }; + + SECTION("noexcept(false) from value type") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(std::is_trivially_copy_constructible_v); + static_assert(extension && not std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_trivially_move_constructible_v); + static_assert(extension && not std::is_nothrow_move_constructible_v); + static_assert(std::is_trivially_destructible_v); + static_assert(std::is_nothrow_destructible_v); + + constexpr T a(std::in_place, 17); + constexpr T b = a; + static_assert(b.has_value() && a.value().v == b.value().v); + + { + T const a(std::in_place, 19); + T b = a; + CHECK(b.has_value()); + CHECK(b.value().v == 19); + + T c = std::move(a); + CHECK(b.has_value()); + CHECK(c.value().v == 19); + } + } + + SECTION("noexcept(false) from error type") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(std::is_trivially_copy_constructible_v); + static_assert(extension && not std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_trivially_move_constructible_v); + static_assert(extension && not std::is_nothrow_move_constructible_v); + static_assert(std::is_trivially_destructible_v); + static_assert(std::is_nothrow_destructible_v); + + constexpr T a(unexpect, 23); + constexpr T b = a; + static_assert(not b.has_value() && a.error().v == b.error().v); + + { + T const a(unexpect, 29); + T b = a; + CHECK(not b.has_value()); + CHECK(b.error().v == 29); + + T c = std::move(a); + CHECK(not c.has_value()); + CHECK(c.error().v == 29); + } + } + + struct C { + constexpr C() = default; + constexpr ~C() noexcept(false) {}; + }; + + SECTION("noexcept(false) dtor value type") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); + static_assert(extension && not std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); + static_assert(extension && not std::is_nothrow_move_constructible_v); + static_assert(not std::is_trivially_destructible_v); + static_assert(not std::is_nothrow_destructible_v); + + constexpr T a(std::in_place); + constexpr T b = a; + static_assert(b.has_value()); + + { + T const a(std::in_place); + T b = a; + CHECK(b.has_value()); + } + } + + SECTION("noexcept(false) dtor error type") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); + static_assert(extension && not std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); + static_assert(extension && not std::is_nothrow_move_constructible_v); + static_assert(not std::is_trivially_destructible_v); + static_assert(not std::is_nothrow_destructible_v); + + constexpr T a(unexpect); + constexpr T b = a; + static_assert(not b.has_value()); + + { + T const a(unexpect); + T b = a; + CHECK(not b.has_value()); + } + } + } + + SECTION("accessors") + { + SECTION("value") + { + using T = expected; + + T a = {11}; + CHECK(a.value().v == 11); + CHECK(std::as_const(a).value().v == 11); + CHECK(std::move(std::as_const(a)).value().v == 11); + CHECK(std::move(a).value().v == 11); + + { + helper b{0}; + CHECK((b = a.value()).v == 11 * helper::from_lval); + CHECK((b = std::as_const(a).value()).v == 11 * helper::from_lval_const); + CHECK((b = std::move(std::as_const(a)).value()).v == 11 * helper::from_rval_const); + CHECK((b = std::move(a).value()).v == 11 * helper::from_rval); + } + + { + T a{unexpect, Error::file_not_found}; + + try { + auto _ = a.value(); + FAIL(); + } catch (bad_expected_access const &e) { + CHECK(e.error() == Error::file_not_found); + } + + try { + auto _ = std::as_const(a).value(); + FAIL(); + } catch (bad_expected_access const &e) { + CHECK(e.error() == Error::file_not_found); + } + + try { + auto _ = std::move(std::as_const(a)).value(); + FAIL(); + } catch (bad_expected_access const &e) { + CHECK(e.error() == Error::file_not_found); + } + + try { + auto _ = std::move(a).value(); + FAIL(); + } catch (bad_expected_access const &e) { + CHECK(e.error() == Error::file_not_found); + } + } + } + + SECTION("error") + { + using T = expected; + + T a{unexpect, 17}; + CHECK(a.error().v == 17); + CHECK(std::as_const(a).error().v == 17); + CHECK(std::move(std::as_const(a)).error().v == 17); + CHECK(std::move(a).error().v == 17); + + { + helper b{0}; + CHECK((b = a.error()).v == 17 * helper::from_lval); + CHECK((b = std::as_const(a).error()).v == 17 * helper::from_lval_const); + CHECK((b = std::move(std::as_const(a)).error()).v == 17 * helper::from_rval_const); + CHECK((b = std::move(a).error()).v == 17 * helper::from_rval); + } + } + } +} diff --git a/tests/util/helper_types.hpp b/tests/util/helper_types.hpp index 33c9a89a..fe20c8f3 100644 --- a/tests/util/helper_types.hpp +++ b/tests/util/helper_types.hpp @@ -25,7 +25,7 @@ template struct helper_t { // No default constructor helper_t() = delete; - constexpr ~helper_t() noexcept = default; + constexpr ~helper_t() noexcept {}; constexpr bool operator==(helper_t const &) const noexcept = default; From 20cb71d906f6ddc4beca9f507aeaf982101f9ce7 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 9 Mar 2025 16:06:57 +0000 Subject: [PATCH 19/46] Workarounds for older compilers --- cmake/CompilationOptions.cmake | 3 ++- include/pfn/expected.hpp | 20 ++++++++++---------- tests/pfn/expected.cpp | 19 ++++++++----------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/cmake/CompilationOptions.cmake b/cmake/CompilationOptions.cmake index c97258c7..f0179897 100644 --- a/cmake/CompilationOptions.cmake +++ b/cmake/CompilationOptions.cmake @@ -20,7 +20,8 @@ function(append_compilation_options) if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") if(Options_WARNINGS) - target_compile_options(${Options_NAME} PRIVATE /W4) + # disable C4456: declaration of 'b' hides previous local declaration + target_compile_options(${Options_NAME} PRIVATE /W4 /wd4456) endif() if(Options_OPTIMIZATION) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 4bafb094..e70ecd1a 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -101,14 +101,14 @@ template class unexpected { } template - constexpr explicit unexpected(std::in_place_t, Args &&...a) noexcept(::std::is_nothrow_constructible_v) + constexpr explicit unexpected(::std::in_place_t, Args &&...a) noexcept(::std::is_nothrow_constructible_v) requires ::std::is_constructible_v : e_(FWD(a)...) { } template - constexpr explicit unexpected(std::in_place_t, ::std::initializer_list i, Args &&...a) noexcept( + constexpr explicit unexpected(::std::in_place_t, ::std::initializer_list i, Args &&...a) noexcept( ::std::is_nothrow_constructible_v &, Args...>) requires ::std::is_constructible_v &, Args...> : e_(i, FWD(a)...) @@ -279,15 +279,15 @@ template class expected { template constexpr explicit(/* TODO */ false) expected(unexpected &&); template - constexpr explicit expected(std::in_place_t, Args &&...a) // + constexpr explicit expected(::std::in_place_t, Args &&...a) // noexcept(::std::is_nothrow_constructible_v) // extension requires ::std::is_constructible_v : v_(FWD(a)...), set_(true) { } template - constexpr explicit expected(std::in_place_t, ::std::initializer_list il, Args &&...a) // - noexcept(::std::is_nothrow_constructible_v &, Args...>) // extension + constexpr explicit expected(::std::in_place_t, ::std::initializer_list il, Args &&...a) // + noexcept(::std::is_nothrow_constructible_v &, Args...>) // extension requires ::std::is_constructible_v &, Args...> : v_(il, FWD(a)...), set_(true) { @@ -316,7 +316,7 @@ template class expected { requires(::std::is_trivially_destructible_v && not ::std::is_trivially_destructible_v) { if (not set_) - e_.~E(); + ::std::destroy_at(&e_); // else T is trivially destructible, no need to do anything } constexpr ~expected() // @@ -324,7 +324,7 @@ template class expected { requires(not ::std::is_trivially_destructible_v && ::std::is_trivially_destructible_v) { if (set_) - v_.~T(); + ::std::destroy_at(&v_); // else E is trivially destructible, no need to do anything } constexpr ~expected() // @@ -332,9 +332,9 @@ template class expected { requires(not ::std::is_trivially_destructible_v && not ::std::is_trivially_destructible_v) { if (set_) - v_.~T(); + ::std::destroy_at(&v_); else - e_.~E(); + ::std::destroy_at(&e_); } // [expected.object.assign], assignment @@ -471,7 +471,7 @@ class expected { template constexpr explicit(/* TODO */ false) expected(unexpected const &); template constexpr explicit(/* TODO */ false) expected(unexpected &&); - constexpr explicit expected(std::in_place_t) noexcept; + constexpr explicit expected(::std::in_place_t) noexcept; template constexpr explicit expected(unexpect_t, Args &&...); template constexpr explicit expected(unexpect_t, ::std::initializer_list, Args &&...); diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 4714f886..9b44eea5 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -506,10 +506,6 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(std::is_default_constructible_v); static_assert(extension && std::is_nothrow_constructible_v); - constexpr T a; - static_assert(a.has_value()); - static_assert(a.value() == 0); - T b; CHECK(b.has_value()); CHECK(b.value() == 0); @@ -664,18 +660,18 @@ TEST_CASE("expected", "[expected][polyfill]") struct B { int v; constexpr B(int v) : v(v) {} - constexpr B(B const &) noexcept(false) = default; - constexpr B(B &&) noexcept(false) = default; + constexpr B(B const &s) noexcept(false) : v(s.v) {}; + constexpr B(B &&s) noexcept(false) : v(s.v) {}; }; SECTION("noexcept(false) from value type") { using T = expected; static_assert(std::is_copy_constructible_v); - static_assert(std::is_trivially_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); static_assert(extension && not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); - static_assert(std::is_trivially_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); static_assert(extension && not std::is_nothrow_move_constructible_v); static_assert(std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); @@ -700,10 +696,10 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_copy_constructible_v); - static_assert(std::is_trivially_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); static_assert(extension && not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); - static_assert(std::is_trivially_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); static_assert(extension && not std::is_nothrow_move_constructible_v); static_assert(std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); @@ -725,7 +721,8 @@ TEST_CASE("expected", "[expected][polyfill]") } struct C { - constexpr C() = default; + constexpr C() noexcept(true) {}; + constexpr C(C const &) noexcept(true) {}; // WORKAROUND:MSVC constexpr ~C() noexcept(false) {}; }; From 309decedafa5533567897be0c36b592370b57546 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 9 Mar 2025 17:20:41 +0000 Subject: [PATCH 20/46] Add !is_same_v, unexpect_t> --- include/pfn/expected.hpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index e70ecd1a..bd78b6e2 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -184,12 +184,13 @@ template class expected { template using _can_move_convert = _can_convert_detail; template - using _can_convert = ::std::bool_constant< // - not ::std::is_same_v<::std::remove_cvref_t, ::std::in_place_t> - && not ::std::is_same_v> // - && not detail::_is_some_unexpected<::std::remove_cvref_t> // - && ::std::is_constructible_v // - && (not ::std::is_same_v> // + using _can_convert = ::std::bool_constant< // + not ::std::is_same_v<::std::remove_cvref_t, ::std::in_place_t> // + && not ::std::is_same_v<::std::remove_cvref_t, unexpect_t> // + && not ::std::is_same_v> // + && not detail::_is_some_unexpected<::std::remove_cvref_t> // + && ::std::is_constructible_v // + && (not ::std::is_same_v> // || not detail::_is_some_expected<::std::remove_cvref_t>)>; public: From ffe4243b1bbd0fe49219d02c104af52cafb5d76d Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Fri, 14 Mar 2025 13:01:04 +0000 Subject: [PATCH 21/46] Add continue_on_err and LIBFN_MODE --- .github/workflows/build.yml | 11 ++++++++++- include/CMakeLists.txt | 2 ++ include/pfn/expected.hpp | 7 ++++++- tests/CMakeLists.txt | 6 +++++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fdcafeb1..41125b36 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,8 +72,10 @@ jobs: $COMPILER --version printf "C++ compilation options: %s\n" "$FLAGS" + # Separate step for each compilation mode - name: Build and test C++20 mode if: ${{ contains( matrix.env.modes, 'cxx20' ) }} + continue-on-error: true run: | cd .build cmake --build . --target clean @@ -82,13 +84,14 @@ jobs: - name: Build and test C++23 mode if: ${{ contains( matrix.env.modes, 'cxx23' ) }} + continue-on-error: true run: | cd .build cmake --build . --target clean cmake --build . --target cxx23 ctest -L cxx23 --output-on-failure - # We want to build all for at least one arbitrary configuration + # Build and test all for one arbitrary configuration - name: Build and test all if: ${{ matrix.env.compiler == 'gcc' && matrix.env.release == '14' }} run: | @@ -135,8 +138,10 @@ jobs: $COMPILER --version printf "C++ compilation options: %s\n" "$FLAGS" + # Separate step for each compilation mode - name: Build and test C++20 mode if: ${{ contains( matrix.env.modes, 'cxx20' ) }} + continue-on-error: true run: | cd .build cmake --build . --target clean @@ -145,6 +150,7 @@ jobs: - name: Build and test C++23 mode if: ${{ contains( matrix.env.modes, 'cxx23' ) }} + continue-on-error: true run: | cd .build cmake --build . --target clean @@ -172,8 +178,10 @@ jobs: cd .build cmake -G "${{ matrix.env.compiler }}" -A x64 .. + # Separate step for each compilation mode - name: Build and test C++20 mode if: ${{ contains( matrix.env.modes, 'cxx20' ) }} + continue-on-error: true run: | cd .build cmake --build . --config ${{ matrix.configuration }} --target clean @@ -182,6 +190,7 @@ jobs: - name: Build and test C++23 mode if: ${{ contains( matrix.env.modes, 'cxx23' ) }} + continue-on-error: true run: | cd .build cmake --build . --config ${{ matrix.configuration }} --target clean diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index bcc31a49..cd2d3c37 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -44,6 +44,7 @@ foreach(mode 20 23) append_compilation_options("${target}" WARNINGS) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") + target_compile_definitions("${target}" PRIVATE LIBFN_MODE=${mode}) unset(target) unset(root_name) @@ -115,6 +116,7 @@ foreach(mode 23) append_compilation_options("${target}" WARNINGS) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") + target_compile_definitions("${target}" PRIVATE LIBFN_MODE=${mode}) unset(target) unset(root_name) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index bd78b6e2..73adc5fc 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -28,7 +28,12 @@ #undef ASSERT #endif -#define ASSERT(...) assert((__VA_ARGS__) == true); +// LIBFN_ASSERT is a customization point for the user +#ifdef LIBFN_ASSERT +#define ASSERT(...) LIBFN_ASSERT(__VA_ARGS__) +#else +#define ASSERT(...) assert((__VA_ARGS__) == true) +#endif namespace pfn { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c1e67033..2c186fb8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,7 +10,7 @@ set(TESTS_UTIL_SOURCES util/helper_types.hpp ) add_library(tests_util INTERFACE ${TESTS_UTIL_SOURCES}) -set_property(TARGET include_fn PROPERTY CXX_STANDARD 23) +set_property(TARGET tests_util PROPERTY CXX_STANDARD 23) target_include_directories(tests_util INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(tests_util INTERFACE include_fn) @@ -30,6 +30,7 @@ foreach(mode 23) append_compilation_options("${target}" WARNINGS) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") + target_compile_definitions("${target}" PRIVATE LIBFN_MODE=${mode}) unset(target) unset(root_name) @@ -59,6 +60,7 @@ foreach(mode 20 23) append_compilation_options("${target}" WARNINGS OPTIMIZATION) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") + target_compile_definitions("${target}" PRIVATE LIBFN_MODE=${mode}) add_test( NAME "${target}" @@ -115,6 +117,7 @@ foreach(mode 23) append_compilation_options("${target}" WARNINGS OPTIMIZATION) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") + target_compile_definitions("${target}" PRIVATE LIBFN_MODE=${mode}) add_test( NAME "${target}" @@ -142,6 +145,7 @@ foreach(mode 23) append_compilation_options("${target}" WARNINGS) add_dependencies("cxx${mode}" "${target}") set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") + target_compile_definitions("${target}" PRIVATE LIBFN_MODE=${mode}) add_test( NAME "${target}" From 5c46dcfb034696fa399fa3512c446f56d49e88f6 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Fri, 14 Mar 2025 15:20:27 +0000 Subject: [PATCH 22/46] Add unit tests --- include/pfn/expected.hpp | 1 + tests/pfn/expected.cpp | 129 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 73adc5fc..4a82d8dc 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -187,6 +187,7 @@ template class expected { template using _can_copy_convert = _can_convert_detail; template using _can_move_convert = _can_convert_detail; + template friend class expected; template using _can_convert = ::std::bool_constant< // diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 9b44eea5..da931627 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -413,11 +414,12 @@ TEST_CASE("expected", "[expected][polyfill]") using pfn::bad_expected_access; using pfn::expected; using pfn::unexpect; + using pfn::unexpect_t; constexpr bool extension = true; - SECTION("default ctor") + SECTION("constructors") { - SECTION("unavailable") + SECTION("default unavailable") { static_assert(not std::is_default_constructible_v); // prerequisite static_assert(not std::is_default_constructible_v>); @@ -425,7 +427,7 @@ TEST_CASE("expected", "[expected][polyfill]") SUCCEED(); } - SECTION("trivial") + SECTION("default trivial") { using T = expected; static_assert(std::is_default_constructible_v); @@ -445,7 +447,7 @@ TEST_CASE("expected", "[expected][polyfill]") int v = 12; }; - SECTION("noexcept(true) from value type") + SECTION("default noexcept(true) from value type") { using T = expected; static_assert(std::is_default_constructible_v); @@ -465,7 +467,7 @@ TEST_CASE("expected", "[expected][polyfill]") int v = 42; }; - SECTION("noexcept(false) from value type") + SECTION("default noexcept(false) from value type") { using T = expected; static_assert(std::is_default_constructible_v); @@ -510,6 +512,123 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(b.has_value()); CHECK(b.value() == 0); } + + SECTION("from other expected rval") + { + using T = expected; + static_assert(std::is_constructible_v>); + static_assert(std::is_nothrow_constructible_v>); // extension + static_assert(std::is_constructible_v, Error>>); + static_assert( + not std::is_nothrow_constructible_v, Error>>); // extension + + constexpr T a(expected(2)); + static_assert(a.value().v == 2); + constexpr T b(expected(unexpect, Error::unknown)); + static_assert(b.error() == Error::unknown); + + T c(expected(3)); + CHECK(c.value().v == 3); + T d(expected(unexpect, Error::file_not_found)); + CHECK(d.error() == Error::file_not_found); + } + + SECTION("from other expected lval const") + { + using T = expected; + static_assert(std::is_constructible_v const &>); + static_assert(std::is_nothrow_constructible_v const &>); // extension + static_assert(std::is_constructible_v, Error> const &>); + static_assert( + not std::is_nothrow_constructible_v, Error> const &>); // extension + + constexpr expected v(5); + constexpr expected e(unexpect, Error::file_not_found); + constexpr T a(v); + static_assert(a.value().v == 5); + constexpr T b(e); + static_assert(b.error() == Error::file_not_found); + + T c(v); + CHECK(c.value().v == 5); + T d(e); + CHECK(d.error() == Error::file_not_found); + } + + SECTION("converting") + { + using T = expected; + static_assert(std::is_constructible_v); + static_assert(std::is_nothrow_constructible_v); // extension + static_assert(std::is_constructible_v); + static_assert(std::is_nothrow_constructible_v); // extension + static_assert(std::is_constructible_v>); + static_assert(not std::is_nothrow_constructible_v>); // extension + + constexpr T a(7); + static_assert(a.value().v == 7); + + T const b(11); + CHECK(b.value().v == 11); + + T const c(helper(13)); + CHECK(c.value().v == 13 * helper::from_rval); + } + + SECTION("with in_place") + { + using T = expected; + static_assert(std::is_constructible_v); + static_assert(std::is_nothrow_constructible_v); // extension + static_assert(std::is_constructible_v); + static_assert(std::is_nothrow_constructible_v); // extension + static_assert(std::is_constructible_v>); + static_assert( + not std::is_nothrow_constructible_v>); // extension + + constexpr T a(std::in_place, 5, 7); + static_assert(a.value().v == 35); + + T const b(std::in_place, 11, 13); + CHECK(b.value().v == 11 * 13); + + T const c(std::in_place, {2.0, 3.0, 5.0}); + CHECK(c.value().v == 2 * 3 * 5.0); + + try { + T const d(std::in_place, {2.0, 3.0, 0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + } + } + + SECTION("with unexpect") + { + using T = expected; + static_assert(std::is_constructible_v); + static_assert(std::is_nothrow_constructible_v); // extension + static_assert(std::is_constructible_v); + static_assert(std::is_nothrow_constructible_v); // extension + static_assert(std::is_constructible_v>); + static_assert(not std::is_nothrow_constructible_v>); // extension + + constexpr T a(unexpect, 5, 7); + static_assert(a.error().v == 35); + + T const b(unexpect, 11, 13); + CHECK(b.error().v == 11 * 13); + + T const c(unexpect, {2.0, 3.0, 5.0}); + CHECK(c.error().v == 2 * 3 * 5.0); + + try { + T const d(unexpect, {2.0, 3.0, 0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + } + } } SECTION("copy, move and dtor") From eed6b4fb7567e2e8e3b78e325dc79093c9744261 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 16 Mar 2025 10:00:34 +0000 Subject: [PATCH 23/46] Add -fsanitize=address -fno-omit-frame-pointer to Debug build --- cmake/CompilationOptions.cmake | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmake/CompilationOptions.cmake b/cmake/CompilationOptions.cmake index f0179897..ec5116c1 100644 --- a/cmake/CompilationOptions.cmake +++ b/cmake/CompilationOptions.cmake @@ -33,7 +33,12 @@ function(append_compilation_options) endif() if(Options_OPTIMIZATION) - target_compile_options(${Options_NAME} PRIVATE $,-O0,-O2>) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_options(${Options_NAME} PRIVATE -O0 -fsanitize=address -static-libasan -fno-omit-frame-pointer) + target_link_options(${Options_NAME} PRIVATE -fsanitize=address) + else() + target_compile_options(${Options_NAME} PRIVATE $,-O0 -fno-omit-frame-pointer,-O2>) + endif() endif() if(Options_INTERFACE) From 405eeb92ab8d01d02e468f931139c3727bed69ef Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 5 Apr 2025 20:22:46 +0100 Subject: [PATCH 24/46] Improve coverage reporting --- tests/fn/choice.cpp | 18 +++--- tests/pfn/expected.cpp | 110 ++++++++++++++++-------------------- tests/util/helper_types.hpp | 23 +++++--- 3 files changed, 75 insertions(+), 76 deletions(-) diff --git a/tests/fn/choice.cpp b/tests/fn/choice.cpp index 8ed92002..3a6d1959 100644 --- a/tests/fn/choice.cpp +++ b/tests/fn/choice.cpp @@ -50,7 +50,7 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(std::same_as().value())>); static_assert(std::same_as().value())>); - type s{helper{0}}; + type s{helper{1}}; s.value().get_ptr()->v = 42; constexpr auto fn = fn::overload{ [](auto &&) -> int { throw 0; }, // @@ -159,7 +159,7 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(a.has_value()); static_assert(a.value() == fn::sum{true}); - constexpr auto b = fn(helper{19}); + constexpr auto b = fn(helper{{0.5, 2.0}, 19}); static_assert(std::is_same_v); static_assert(b.has_value()); static_assert(b.value().get_ptr()->v == 19 * helper::from_rval); @@ -189,7 +189,7 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(a.has_value()); static_assert(a.value() == fn::sum{true}); - constexpr auto b = fn(helper{17}); + constexpr auto b = fn(helper{{0.5, 2.0}, 17}); static_assert(std::is_same_v); static_assert(b.has_value()); static_assert(b.value().get_ptr()->v == 17 * helper::from_rval_const); @@ -219,7 +219,7 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(a.has_value()); static_assert(a.value() == fn::sum{true}); - constexpr auto b = fn(helper{17}); + constexpr auto b = fn(helper{{0.5, 2.0}, 17}); static_assert(std::is_same_v); static_assert(b.has_value()); static_assert(b.value().get_ptr()->v == 17 * helper::from_lval); @@ -249,7 +249,7 @@ TEST_CASE("choice non-monadic functionality", "[choice]") static_assert(a.has_value()); static_assert(a.value() == fn::sum{true}); - constexpr auto b = fn(helper{15}); + constexpr auto b = fn(helper{{0.5, 2.0}, 15}); static_assert(std::is_same_v); static_assert(b.has_value()); static_assert(b.value().get_ptr()->v == 15 * helper::from_lval_const); @@ -273,7 +273,7 @@ TEST_CASE("choice non-monadic functionality", "[choice]") WHEN("copy ctor") { using T = fn::choice; - auto a = T{helper{0}}; + auto a = T{helper{1}}; a.value().get_ptr()->v = 23; auto const b = std::as_const(a); @@ -284,7 +284,7 @@ TEST_CASE("choice non-monadic functionality", "[choice]") WHEN("move ctor") { using T = fn::choice; - auto a = T{helper{0}}; + auto a = T{helper{1}}; a.value().get_ptr()->v = 29; auto const b = std::move(a); @@ -298,14 +298,14 @@ TEST_CASE("choice non-monadic functionality", "[choice]") using T = fn::choice; WHEN("move from rvalue") { - fn::sum h{helper{0}}; + fn::sum h{helper{1}}; h.get_ptr()->v = 17; T const a{std::move(h)}; CHECK(a.value().get_ptr()->v == 17 * helper::from_rval); } WHEN("copy from const lvalue") { - fn::sum h{helper{0}}; + fn::sum h{helper{1}}; h.get_ptr()->v = 19; T const a{std::as_const(h)}; CHECK(a.value().get_ptr()->v == 19 * helper::from_lval_const); diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index da931627..30525edd 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -78,7 +78,7 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") SECTION("copy/move constructors") { - T b{0}; + T b{1}; SECTION("lval") { @@ -112,7 +112,7 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") SECTION("assignment") { T a{12}; - T b{0}; + T b{1}; SECTION("lval") { @@ -145,8 +145,8 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") SECTION("accessors") { - helper c{0}; - T b{0}; + helper c{1}; + T b{1}; SECTION("lval") { @@ -261,7 +261,7 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") unexpected c(3); CHECK(c.error().v == 3); CHECK(c == unexpected(std::in_place, 3)); - static_assert(std::is_nothrow_constructible_v); + static_assert(not std::is_nothrow_constructible_v); } } @@ -269,7 +269,7 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") { unexpected c(std::in_place, 3, 5); CHECK(c.error().v == 3 * 5); - static_assert(std::is_nothrow_constructible_v); + static_assert(not std::is_nothrow_constructible_v); c.error().v *= helper::from_rval; CHECK(c == unexpected(std::in_place, helper{15})); @@ -279,27 +279,24 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") { SECTION("forwarded args") { - unexpected c(std::in_place, {3.0, 5.0}, 7, 11); - auto const d = 3 * 5 * 7 * 11; + unexpected c(std::in_place, {3.0, 5.0}); + auto const d = 3 * 5; CHECK(c.error().v == d); - static_assert( - not std::is_nothrow_constructible_v, int, int>); - static_assert(std::is_constructible_v, int, int>); + static_assert(std::is_nothrow_constructible_v>); } SECTION("no forwarded args") { unexpected c(std::in_place, {2.0, 2.5}); CHECK(c.error().v == 5); - static_assert(not std::is_nothrow_constructible_v>); - static_assert(std::is_constructible_v>); + static_assert(std::is_nothrow_constructible_v>); } SECTION("exception thrown") { unexpected t{13}; try { - t = unexpected{std::in_place, {2.0, 1.0, 0.0}, 5}; + t = unexpected{std::in_place, 1, 2, 0}; FAIL(); } catch (std::runtime_error const &) { SUCCEED(); @@ -344,7 +341,7 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SECTION("assignment") { - unexpected a{0}; + unexpected a{1}; SECTION("lval") { @@ -377,9 +374,9 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") SECTION("swap") { - unexpected a{0}; + unexpected a{1}; a.error().v = 2; - unexpected b{helper{0}}; + unexpected b{helper{1}}; b.error().v = 3; a.swap(b); CHECK(a.error().v == 3 * helper::swapped); @@ -396,7 +393,7 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") unexpected a{i}; unexpected b{i * 5}; swap(a, b); - unexpected c{0}; + unexpected c{1}; c = b; b.swap(c); return unexpected{b.error() * a.error() * 7}; @@ -513,39 +510,40 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(b.value() == 0); } - SECTION("from other expected rval") + SECTION("value from other expected rval") { using T = expected; static_assert(std::is_constructible_v>); - static_assert(std::is_nothrow_constructible_v>); // extension static_assert(std::is_constructible_v, Error>>); - static_assert( - not std::is_nothrow_constructible_v, Error>>); // extension + static_assert(not std::is_nothrow_constructible_v>); // extension + static_assert(std::is_nothrow_constructible_v, Error>>); // extension - constexpr T a(expected(2)); - static_assert(a.value().v == 2); constexpr T b(expected(unexpect, Error::unknown)); static_assert(b.error() == Error::unknown); T c(expected(3)); CHECK(c.value().v == 3); - T d(expected(unexpect, Error::file_not_found)); - CHECK(d.error() == Error::file_not_found); } - SECTION("from other expected lval const") + SECTION("error from other expected rval") + { + using T = expected; + static_assert(std::is_constructible_v>); + + T d(expected(unexpect, 2)); + CHECK(d.error().v == 2 * helper::from_rval); + } + + SECTION("value from other expected lval const") { using T = expected; static_assert(std::is_constructible_v const &>); - static_assert(std::is_nothrow_constructible_v const &>); // extension - static_assert(std::is_constructible_v, Error> const &>); + static_assert(not std::is_nothrow_constructible_v const &>); // extension static_assert( - not std::is_nothrow_constructible_v, Error> const &>); // extension + std::is_nothrow_constructible_v, Error> const &>); // extension constexpr expected v(5); constexpr expected e(unexpect, Error::file_not_found); - constexpr T a(v); - static_assert(a.value().v == 5); constexpr T b(e); static_assert(b.error() == Error::file_not_found); @@ -555,18 +553,25 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(d.error() == Error::file_not_found); } + SECTION("error from other expected lval const") + { + using T = expected; + static_assert(std::is_constructible_v>); + + expected const e(unexpect, 3); + T d(e); + CHECK(d.error().v == 3 * helper::from_lval_const); + } + SECTION("converting") { using T = expected; static_assert(std::is_constructible_v); - static_assert(std::is_nothrow_constructible_v); // extension + static_assert(not std::is_nothrow_constructible_v); // extension static_assert(std::is_constructible_v); static_assert(std::is_nothrow_constructible_v); // extension static_assert(std::is_constructible_v>); - static_assert(not std::is_nothrow_constructible_v>); // extension - - constexpr T a(7); - static_assert(a.value().v == 7); + static_assert(std::is_nothrow_constructible_v>); // extension T const b(11); CHECK(b.value().v == 11); @@ -579,15 +584,11 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v); - static_assert(std::is_nothrow_constructible_v); // extension + static_assert(not std::is_nothrow_constructible_v); // extension static_assert(std::is_constructible_v); static_assert(std::is_nothrow_constructible_v); // extension static_assert(std::is_constructible_v>); - static_assert( - not std::is_nothrow_constructible_v>); // extension - - constexpr T a(std::in_place, 5, 7); - static_assert(a.value().v == 35); + static_assert(std::is_nothrow_constructible_v>); // extension T const b(std::in_place, 11, 13); CHECK(b.value().v == 11 * 13); @@ -596,7 +597,7 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(c.value().v == 2 * 3 * 5.0); try { - T const d(std::in_place, {2.0, 3.0, 0.0}); + T const d(std::in_place, 1, 2, 0); FAIL(); } catch (std::runtime_error const &e) { CHECK(std::strcmp(e.what(), "invalid input") == 0); @@ -607,14 +608,11 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v); - static_assert(std::is_nothrow_constructible_v); // extension + static_assert(not std::is_nothrow_constructible_v); // extension static_assert(std::is_constructible_v); static_assert(std::is_nothrow_constructible_v); // extension static_assert(std::is_constructible_v>); - static_assert(not std::is_nothrow_constructible_v>); // extension - - constexpr T a(unexpect, 5, 7); - static_assert(a.error().v == 35); + static_assert(std::is_nothrow_constructible_v>); // extension T const b(unexpect, 11, 13); CHECK(b.error().v == 11 * 13); @@ -623,7 +621,7 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(c.error().v == 2 * 3 * 5.0); try { - T const d(unexpect, {2.0, 3.0, 0.0}); + T const d(unexpect, 1, 2, 0); FAIL(); } catch (std::runtime_error const &e) { CHECK(std::strcmp(e.what(), "invalid input") == 0); @@ -689,10 +687,6 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); - constexpr T a(std::in_place, 11); - constexpr T b = a; - static_assert(b.has_value() && b.value().v == 11 * helper::from_lval_const); - { T a(std::in_place, 13); T b = a; // no overload for lval @@ -725,10 +719,6 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); - constexpr T a(unexpect, 31); - constexpr T b = a; - static_assert(not b.has_value() && b.error().v == 31 * helper::from_lval_const); - { T a(unexpect, 33); T b = a; // no overload for lval @@ -905,7 +895,7 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(std::move(a).value().v == 11); { - helper b{0}; + helper b{1}; CHECK((b = a.value()).v == 11 * helper::from_lval); CHECK((b = std::as_const(a).value()).v == 11 * helper::from_lval_const); CHECK((b = std::move(std::as_const(a)).value()).v == 11 * helper::from_rval_const); @@ -956,7 +946,7 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(std::move(a).error().v == 17); { - helper b{0}; + helper b{1}; CHECK((b = a.error()).v == 17 * helper::from_lval); CHECK((b = std::as_const(a).error()).v == 17 * helper::from_lval_const); CHECK((b = std::move(std::as_const(a)).error()).v == 17 * helper::from_rval_const); diff --git a/tests/util/helper_types.hpp b/tests/util/helper_types.hpp index fe20c8f3..525d2d7f 100644 --- a/tests/util/helper_types.hpp +++ b/tests/util/helper_types.hpp @@ -5,12 +5,15 @@ #include #include +#include #include #include #include #include template struct helper_t { + static inline int state = 0; + int v = {}; // Use prime numbers to record Foo states in witness @@ -63,30 +66,36 @@ template struct helper_t { constexpr helper_t(helper_t &&o) noexcept : v(o.v) { v *= from_rval; } constexpr helper_t(helper_t const &&o) noexcept : v(o.v) { v *= from_rval_const; } - constexpr helper_t(std::integral auto... a) noexcept + // The intent of non-constexpr constructors is to make sure that they are never optimized away, + // thus ensuring that any code which relies on them in tests will show up in coverage reports. + helper_t(std::integral auto... a) noexcept(false) requires(sizeof...(a) > 0) // intentionally implicit when sizeof...(a) == 1 : v((1 * ... * a)) { + if (v == 0) + throw std::runtime_error("invalid input"); + state += v; } - template // - constexpr explicit helper_t(std::integral_constant) noexcept : v(static_cast(T::value)) + helper_t(std::initializer_list list) noexcept(true) : v(init(list)) { + if (v == 0) + std::terminate(); + state += v; } // Potentially throwing constructor - constexpr helper_t(std::initializer_list list, std::integral auto... a) noexcept(false) + constexpr helper_t(std::initializer_list list, std::integral auto... a) noexcept(true) + requires(sizeof...(a) > 0) : v(init(list, a...)) // { } // ... and the actual exception being thrown - static constexpr int init(std::initializer_list l, auto &&...a) noexcept(false) + static constexpr int init(std::initializer_list l, auto &&...a) noexcept { double ret = (1 * ... * a); for (auto d : l) { - if (d == 0.0) - throw std::runtime_error("invalid input"); ret *= d; } return static_cast(ret); From b265c74b2e087fb2e2fc556e076af7d35d9ebc50 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Thu, 17 Apr 2025 16:50:22 +0100 Subject: [PATCH 25/46] Add constructors from unexpected --- include/pfn/expected.hpp | 19 +++++-- tests/pfn/expected.cpp | 110 ++++++++++++++++++++++++++------------- 2 files changed, 89 insertions(+), 40 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 4a82d8dc..f884cf1e 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -192,7 +192,7 @@ template class expected { template using _can_convert = ::std::bool_constant< // not ::std::is_same_v<::std::remove_cvref_t, ::std::in_place_t> // - && not ::std::is_same_v<::std::remove_cvref_t, unexpect_t> // + && not ::std::is_same_v<::std::remove_cvref_t, unexpect_t> // LWG4222 && not ::std::is_same_v> // && not detail::_is_some_unexpected<::std::remove_cvref_t> // && ::std::is_constructible_v // @@ -282,8 +282,21 @@ template class expected { { } - template constexpr explicit(/* TODO */ false) expected(unexpected const &); - template constexpr explicit(/* TODO */ false) expected(unexpected &&); + template + constexpr explicit(!::std::is_convertible_v) expected(unexpected const &g) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires(::std::is_constructible_v) + : e_(std::forward(g.error())), set_(false) + { + } + + template + constexpr explicit(!::std::is_convertible_v) expected(unexpected &&g) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires(::std::is_constructible_v) + : e_(std::forward(g.error())), set_(false) + { + } template constexpr explicit expected(::std::in_place_t, Args &&...a) // diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 30525edd..4d4c6de2 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -412,6 +412,7 @@ TEST_CASE("expected", "[expected][polyfill]") using pfn::expected; using pfn::unexpect; using pfn::unexpect_t; + using pfn::unexpected; constexpr bool extension = true; SECTION("constructors") @@ -428,7 +429,7 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_default_constructible_v); - static_assert(extension && std::is_nothrow_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); constexpr T a; static_assert(a.has_value()); @@ -448,7 +449,7 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_default_constructible_v); - static_assert(extension && std::is_nothrow_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); constexpr T a; static_assert(a.has_value()); @@ -468,7 +469,7 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_default_constructible_v); - static_assert(not std::is_nothrow_constructible_v); + static_assert(not extension || not std::is_nothrow_constructible_v); constexpr T a; static_assert(a.has_value()); @@ -479,11 +480,11 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(b.value().v == 42); } - SECTION("ignore noexcept from error type") + SECTION("default ignore noexcept from error type") { using T = expected; static_assert(std::is_default_constructible_v); - static_assert(extension && std::is_nothrow_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); SUCCEED(); } @@ -491,7 +492,7 @@ TEST_CASE("expected", "[expected][polyfill]") C() noexcept(false) { throw 7; } }; - SECTION("exception thrown") + SECTION("default exception thrown") { try { expected b; @@ -503,7 +504,7 @@ TEST_CASE("expected", "[expected][polyfill]") // not a problem if error type ctor is throwing using T = expected; static_assert(std::is_default_constructible_v); - static_assert(extension && std::is_nothrow_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); T b; CHECK(b.has_value()); @@ -515,8 +516,9 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_constructible_v>); static_assert(std::is_constructible_v, Error>>); - static_assert(not std::is_nothrow_constructible_v>); // extension - static_assert(std::is_nothrow_constructible_v, Error>>); // extension + static_assert(not extension || not std::is_nothrow_constructible_v>); + static_assert(not extension + || std::is_nothrow_constructible_v, Error>>); constexpr T b(expected(unexpect, Error::unknown)); static_assert(b.error() == Error::unknown); @@ -538,9 +540,9 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v const &>); - static_assert(not std::is_nothrow_constructible_v const &>); // extension - static_assert( - std::is_nothrow_constructible_v, Error> const &>); // extension + static_assert(not extension || not std::is_nothrow_constructible_v const &>); + static_assert(not extension + || std::is_nothrow_constructible_v, Error> const &>); constexpr expected v(5); constexpr expected e(unexpect, Error::file_not_found); @@ -567,11 +569,11 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v); - static_assert(not std::is_nothrow_constructible_v); // extension + static_assert(not extension || not std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); - static_assert(std::is_nothrow_constructible_v); // extension + static_assert(not extension || std::is_nothrow_constructible_v); static_assert(std::is_constructible_v>); - static_assert(std::is_nothrow_constructible_v>); // extension + static_assert(not extension || std::is_nothrow_constructible_v>); T const b(11); CHECK(b.value().v == 11); @@ -580,15 +582,49 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(c.value().v == 13 * helper::from_rval); } + SECTION("from unexpected rval") + { + using T = expected; + static_assert(std::is_constructible_v>); + static_assert(not extension || not std::is_nothrow_constructible_v>); + static_assert(std::is_constructible_v>>); + static_assert(not extension || std::is_nothrow_constructible_v>>); + + constexpr expected a(unexpected(true)); + static_assert(a.error() == 1); + + T const b(unexpected(5)); + CHECK(b.error().v == 5); + + T const c(unexpected>({2, 3, 5})); + CHECK(c.error().v == 2 * 3 * 5); + } + + SECTION("from unexpected lval const") + { + using T = expected; + constexpr auto g1 = unexpected(5); + constexpr expected a(g1); + static_assert(a.error() == 5); + + T const b(g1); + CHECK(b.error().v == 5); + + auto const g2 = unexpected>({2, 3, 5}); + T const c(g2); + CHECK(c.error().v == 2 * 3 * 5); + } + SECTION("with in_place") { using T = expected; static_assert(std::is_constructible_v); - static_assert(not std::is_nothrow_constructible_v); // extension + static_assert(not extension || not std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); - static_assert(std::is_nothrow_constructible_v); // extension + static_assert(not extension || std::is_nothrow_constructible_v); static_assert(std::is_constructible_v>); - static_assert(std::is_nothrow_constructible_v>); // extension + static_assert(not extension + || std::is_nothrow_constructible_v>); T const b(std::in_place, 11, 13); CHECK(b.value().v == 11 * 13); @@ -608,11 +644,11 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v); - static_assert(not std::is_nothrow_constructible_v); // extension + static_assert(not extension || not std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); - static_assert(std::is_nothrow_constructible_v); // extension + static_assert(not extension || std::is_nothrow_constructible_v); static_assert(std::is_constructible_v>); - static_assert(std::is_nothrow_constructible_v>); // extension + static_assert(not extension || std::is_nothrow_constructible_v>); T const b(unexpect, 11, 13); CHECK(b.error().v == 11 * 13); @@ -652,10 +688,10 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(std::is_trivially_copy_constructible_v); - static_assert(extension && std::is_nothrow_copy_constructible_v); + static_assert(not extension || std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(std::is_trivially_move_constructible_v); - static_assert(extension && std::is_nothrow_move_constructible_v); + static_assert(not extension || std::is_nothrow_move_constructible_v); static_assert(std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); @@ -680,10 +716,10 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(extension && std::is_nothrow_copy_constructible_v); + static_assert(not extension || std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); - static_assert(extension && std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); @@ -712,10 +748,10 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(extension && std::is_nothrow_copy_constructible_v); + static_assert(not extension || std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); - static_assert(extension && std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); @@ -744,10 +780,10 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected>; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(extension && std::is_nothrow_copy_constructible_v); + static_assert(not extension || std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); - static_assert(extension && std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); @@ -778,10 +814,10 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(extension && not std::is_nothrow_copy_constructible_v); + static_assert(not extension || not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); - static_assert(extension && not std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); // required static_assert(std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); @@ -806,10 +842,10 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(extension && not std::is_nothrow_copy_constructible_v); + static_assert(not extension || not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); - static_assert(extension && not std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); // required static_assert(std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); @@ -840,10 +876,10 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(extension && not std::is_nothrow_copy_constructible_v); + static_assert(not extension || not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); - static_assert(extension && not std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); static_assert(not std::is_nothrow_destructible_v); @@ -863,10 +899,10 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(extension && not std::is_nothrow_copy_constructible_v); + static_assert(not extension || not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); - static_assert(extension && not std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); static_assert(not std::is_nothrow_destructible_v); From 921868514dedcd19cb61f4656e3c584bada9f7db Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Fri, 18 Apr 2025 21:15:30 +0100 Subject: [PATCH 26/46] Implemented assignment operators --- include/pfn/expected.hpp | 177 ++++++++-- tests/pfn/expected.cpp | 644 +++++++++++++++++++++++++++++++++++- tests/util/helper_types.hpp | 49 ++- 3 files changed, 822 insertions(+), 48 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index f884cf1e..c31291a4 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -199,6 +199,39 @@ template class expected { && (not ::std::is_same_v> // || not detail::_is_some_expected<::std::remove_cvref_t>)>; + template + using _can_convert_assign = ::std::bool_constant< // + not ::std::is_same_v> // + && not detail::_is_some_unexpected<::std::remove_cvref_t> // + && ::std::is_constructible_v // + && ::std::is_assignable_v // + && (::std::is_nothrow_constructible_v // + || ::std::is_nothrow_move_constructible_v // + || ::std::is_nothrow_move_constructible_v)>; + + // [expected.object.assign] + template + static constexpr void _reinit(New &newval, Old &oldval, Args &&...args) + { + if constexpr (::std::is_nothrow_constructible_v) { + ::std::destroy_at(::std::addressof(oldval)); + ::std::construct_at(::std::addressof(newval), FWD(args)...); + } else if constexpr (::std::is_nothrow_move_constructible_v) { + New tmp(FWD(args)...); + ::std::destroy_at(::std::addressof(oldval)); + ::std::construct_at(::std::addressof(newval), std::move(tmp)); + } else { + Old tmp(std::move(oldval)); + ::std::destroy_at(::std::addressof(oldval)); + try { + ::std::construct_at(::std::addressof(newval), FWD(args)...); + } catch (...) { + ::std::construct_at(::std::addressof(oldval), std::move(tmp)); + throw; + } + } + } + public: using value_type = T; using error_type = E; @@ -226,9 +259,9 @@ template class expected { : set_(s.set_) { if (set_) - ::std::construct_at(&v_, s.v_); + ::std::construct_at(::std::addressof(v_), s.v_); else - ::std::construct_at(&e_, s.e_); + ::std::construct_at(::std::addressof(e_), s.e_); } constexpr expected(expected &&s) @@ -242,9 +275,9 @@ template class expected { : set_(s.set_) { if (set_) - ::std::construct_at(&v_, ::std::move(s.v_)); + ::std::construct_at(::std::addressof(v_), ::std::move(s.v_)); else - ::std::construct_at(&e_, ::std::move(s.e_)); + ::std::construct_at(::std::addressof(e_), ::std::move(s.e_)); } template @@ -256,9 +289,9 @@ template class expected { : set_(s.set_) { if (set_) - ::std::construct_at(&v_, s.v_); + ::std::construct_at(::std::addressof(v_), s.v_); else - ::std::construct_at(&e_, s.e_); + ::std::construct_at(::std::addressof(e_), s.e_); } template @@ -269,9 +302,9 @@ template class expected { : set_(s.set_) { if (set_) - ::std::construct_at(&v_, ::std::move(s.v_)); + ::std::construct_at(::std::addressof(v_), ::std::move(s.v_)); else - ::std::construct_at(&e_, ::std::move(s.e_)); + ::std::construct_at(::std::addressof(e_), ::std::move(s.e_)); } template > @@ -286,7 +319,7 @@ template class expected { constexpr explicit(!::std::is_convertible_v) expected(unexpected const &g) // noexcept(::std::is_nothrow_constructible_v) // extension requires(::std::is_constructible_v) - : e_(std::forward(g.error())), set_(false) + : e_(::std::forward(g.error())), set_(false) { } @@ -294,7 +327,7 @@ template class expected { constexpr explicit(!::std::is_convertible_v) expected(unexpected &&g) // noexcept(::std::is_nothrow_constructible_v) // extension requires(::std::is_constructible_v) - : e_(std::forward(g.error())), set_(false) + : e_(::std::forward(g.error())), set_(false) { } @@ -336,7 +369,7 @@ template class expected { requires(::std::is_trivially_destructible_v && not ::std::is_trivially_destructible_v) { if (not set_) - ::std::destroy_at(&e_); + ::std::destroy_at(::std::addressof(e_)); // else T is trivially destructible, no need to do anything } constexpr ~expected() // @@ -344,7 +377,7 @@ template class expected { requires(not ::std::is_trivially_destructible_v && ::std::is_trivially_destructible_v) { if (set_) - ::std::destroy_at(&v_); + ::std::destroy_at(::std::addressof(v_)); // else E is trivially destructible, no need to do anything } constexpr ~expected() // @@ -352,17 +385,103 @@ template class expected { requires(not ::std::is_trivially_destructible_v && not ::std::is_trivially_destructible_v) { if (set_) - ::std::destroy_at(&v_); + ::std::destroy_at(::std::addressof(v_)); else - ::std::destroy_at(&e_); + ::std::destroy_at(::std::addressof(e_)); } // [expected.object.assign], assignment - constexpr expected &operator=(expected const &); - constexpr expected &operator=(expected &&) noexcept(/* TODO */ false); - template constexpr expected &operator=(U &&); - template constexpr expected &operator=(unexpected const &); - template constexpr expected &operator=(unexpected &&); + constexpr expected &operator=(expected const &) = delete; + constexpr expected &operator=(expected const &s) // + noexcept(::std::is_nothrow_copy_assignable_v && ::std::is_nothrow_copy_constructible_v + && ::std::is_nothrow_copy_assignable_v && ::std::is_nothrow_copy_constructible_v) // extension + requires(::std::is_copy_assignable_v && ::std::is_copy_constructible_v && ::std::is_copy_assignable_v + && ::std::is_copy_constructible_v + && (::std::is_nothrow_move_constructible_v || ::std::is_nothrow_move_constructible_v)) + { + if (set_ && s.set_) { + v_ = s.v_; + } else if (set_) { + _reinit(e_, v_, s.e_); + set_ = false; + } else if (s.set_) { + _reinit(v_, e_, s.v_); + set_ = true; + } else { + e_ = s.e_; + } + return *this; + } + + constexpr expected &operator=(expected &&s) // + noexcept(::std::is_nothrow_move_assignable_v && ::std::is_nothrow_move_constructible_v + && ::std::is_nothrow_move_assignable_v && ::std::is_nothrow_move_constructible_v) // required + requires(::std::is_move_constructible_v && ::std::is_move_assignable_v && ::std::is_move_constructible_v + && ::std::is_move_assignable_v + && (::std::is_nothrow_move_constructible_v || ::std::is_nothrow_move_constructible_v)) + { + if (set_ && s.set_) { + v_ = ::std::move(s.v_); + } else if (set_) { + _reinit(e_, v_, ::std::move(s.e_)); + set_ = false; + } else if (s.set_) { + _reinit(v_, e_, ::std::move(s.v_)); + set_ = true; + } else { + e_ = ::std::move(s.e_); + } + return *this; + } + + template + constexpr expected &operator=(U &&s) // + noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v + && (::std::is_nothrow_move_constructible_v || ::std::is_nothrow_move_constructible_v)) // extension + requires(_can_convert_assign::value) + { + if (set_) { + v_ = FWD(s); + } else { + _reinit(v_, e_, FWD(s)); + set_ = true; + } + return *this; + } + + template + constexpr expected &operator=(unexpected const &s) // + noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v + && (::std::is_nothrow_move_constructible_v || ::std::is_nothrow_move_constructible_v)) // extension + requires(::std::is_constructible_v && ::std::is_assignable_v + && (::std::is_nothrow_constructible_v || ::std::is_nothrow_move_constructible_v + || ::std::is_nothrow_move_constructible_v)) + { + if (not set_) { + e_ = ::std::forward(s.error()); + } else { + _reinit(e_, v_, ::std::forward(s.error())); + set_ = false; + } + return *this; + } + + template + constexpr expected &operator=(unexpected &&s) // + noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v + && (::std::is_nothrow_move_constructible_v || ::std::is_nothrow_move_constructible_v)) // extension + requires(::std::is_constructible_v && ::std::is_assignable_v + && (::std::is_nothrow_constructible_v || ::std::is_nothrow_move_constructible_v + || ::std::is_nothrow_move_constructible_v)) + { + if (not set_) { + e_ = ::std::forward(s.error()); + } else { + _reinit(e_, v_, ::std::forward(s.error())); + set_ = false; + } + return *this; + } template constexpr T &emplace(Args &&...) noexcept; template constexpr T &emplace(std::initializer_list, Args &&...) noexcept; @@ -375,12 +494,12 @@ template class expected { constexpr T const *operator->() const noexcept { ASSERT(set_); - return addressof(v_); + return ::std::addressof(v_); } constexpr T *operator->() noexcept { ASSERT(set_); - return addressof(v_); + return ::std::addressof(v_); } constexpr T const &operator*() const & noexcept { return *(this->operator->()); } constexpr T &operator*() & noexcept { return *(this->operator->()); } @@ -390,46 +509,46 @@ template class expected { constexpr bool has_value() const noexcept { return set_; } constexpr T const &value() const & { - if (!set_) + if (not set_) throw bad_expected_access(::std::as_const(e_)); return v_; } constexpr T &value() & { - if (!set_) + if (not set_) throw bad_expected_access(::std::as_const(e_)); return v_; } constexpr T const &&value() const && { - if (!set_) + if (not set_) throw bad_expected_access(::std::as_const(e_)); return ::std::move(v_); } constexpr T &&value() && { - if (!set_) + if (not set_) throw bad_expected_access(::std::as_const(e_)); return ::std::move(v_); } constexpr E const &error() const & noexcept { - ASSERT(!set_); + ASSERT(not set_); return e_; } constexpr E &error() & noexcept { - ASSERT(!set_); + ASSERT(not set_); return e_; } constexpr E const &&error() const && noexcept { - ASSERT(!set_); + ASSERT(not set_); return ::std::move(e_); } constexpr E &&error() && noexcept { - ASSERT(!set_); + ASSERT(not set_); return ::std::move(e_); } diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 4d4c6de2..a09f1f6e 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -190,6 +190,10 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") } } +template struct dummy final { + decltype(V) value = V; +}; + TEST_CASE("unexpect", "[expected][polyfill][unexpect]") { static_assert(std::is_empty_v); @@ -197,11 +201,11 @@ TEST_CASE("unexpect", "[expected][polyfill][unexpect]") static_assert(std::is_same_v); // pfn::unexpect can be used as a NTTP - static_assert(not std::is_empty_v>); + static_assert(not std::is_empty_v>); static constexpr auto a = pfn::unexpect; - static_assert(not std::is_empty_v>); + static_assert(not std::is_empty_v>); static_assert(std::is_same_v); - static_assert(std::is_same_v, helper_t>); + static_assert(std::is_same_v, dummy>); SUCCEED(); } @@ -595,9 +599,6 @@ TEST_CASE("expected", "[expected][polyfill]") T const b(unexpected(5)); CHECK(b.error().v == 5); - - T const c(unexpected>({2, 3, 5})); - CHECK(c.error().v == 2 * 3 * 5); } SECTION("from unexpected lval const") @@ -609,10 +610,6 @@ TEST_CASE("expected", "[expected][polyfill]") T const b(g1); CHECK(b.error().v == 5); - - auto const g2 = unexpected>({2, 3, 5}); - T const c(g2); - CHECK(c.error().v == 2 * 3 * 5); } SECTION("with in_place") @@ -918,6 +915,633 @@ TEST_CASE("expected", "[expected][polyfill]") } } + SECTION("assign") + { + using M = helper_t<2>; // nothrow move constructible + using E = helper_t<3>; // may throw on move and copy + using C = helper_t<4>; // nothrow copy constructible + static_assert(not std::is_nothrow_copy_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + SECTION("from rval") + { + SECTION("value to value") + { + using T = expected; + + { + static_assert(std::is_nothrow_assignable_v); // required + + T a(std::in_place, 3); + a = T(std::in_place, 5); + CHECK(a.value().v == 5 * helper::from_rval); + } + + { + static_assert(not extension || std::is_nothrow_assignable_v); + + T a(std::in_place, 3); + a = helper(5); + CHECK(a.value().v == 5 * helper::from_rval); + } + } + + SECTION("value to error") + { + SECTION("nothrow move") + { + using T = expected; + static_assert(std::is_nothrow_assignable_v); // required + + { + T a(std::in_place, 3); + a = T(unexpect, 5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place, 4); + try { + a = T(unexpect, {0.0}); + SUCCEED(); + } catch (std::runtime_error const &) { + FAIL(); + } + } + + { + static_assert(not extension || std::is_nothrow_assignable_v &&>); + T a(std::in_place, 4); + a = unexpected(5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place, 4); + try { + a = unexpected({0.0}); + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + } + + SECTION("throwing") + { + using T = expected; + static_assert(not std::is_nothrow_assignable_v); // required + + { + T a(std::in_place, 3); + a = T(unexpect, 5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place, 4); + try { + a = T(unexpect, {0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.value() == 4); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v &&>); + T a(std::in_place, 4); + a = unexpected(5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place, 4); + try { + a = unexpected({0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.value() == 4); + } + } + } + + SECTION("nothrow copy") + { + using T = expected; + static_assert(not std::is_nothrow_assignable_v); // required + + { + T a(std::in_place, 3); + a = T(unexpect, 5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place, 4); + try { + a = T(unexpect, {0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.value() == 4); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v &&>); + T a(std::in_place, 4); + a = unexpected(5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place, 4); + try { + a = unexpected({0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.value() == 4); + } + } + } + } + + SECTION("error to value") + { + SECTION("nothrow move") + { + using T = expected; + static_assert(std::is_nothrow_assignable_v); // required + + { + T a(unexpect, Error::file_not_found); + a = T(std::in_place, 5); + CHECK(a.value().v == 5 * helper::from_rval); + } + + { + T a(unexpect, Error::file_not_found); + try { + a = T(std::in_place, {0.0}); + // expected must not use throwing copy constructor + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + + { + static_assert(not extension || std::is_nothrow_assignable_v); + T a(unexpect, Error::file_not_found); + a = M(5); + CHECK(a.value().v == 5 * helper::from_rval); + } + + { + T a(unexpect, Error::file_not_found); + try { + a = M({0.0}); + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + } + + SECTION("throwing") + { + using T = expected; + static_assert(not std::is_nothrow_assignable_v); // required + + { + T a(unexpect, Error::file_not_found); + a = T(std::in_place, 5); + CHECK(a.value().v == 5 * helper::from_rval); + } + + { + T a(unexpect, Error::file_not_found); + try { + a = T(std::in_place, {0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.error() == Error::file_not_found); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v); + T a(unexpect, Error::file_not_found); + a = E(5); + CHECK(a.value().v == 5 * helper::from_rval); + } + + { + T a(unexpect, Error::file_not_found); + try { + a = E({0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.error() == Error::file_not_found); + } + } + } + + SECTION("nothrow copy") + { + using T = expected; + static_assert(not std::is_nothrow_assignable_v); // required + + { + T a(unexpect, Error::file_not_found); + a = T(std::in_place, 5); + CHECK(a.value().v == 5 * helper::from_rval); + } + + { + T a(unexpect, Error::file_not_found); + try { + a = T(std::in_place, {0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.error() == Error::file_not_found); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v); + T a(unexpect, Error::file_not_found); + a = C(5); + CHECK(a.value().v == 5 * helper::from_rval); + } + + { + T a(unexpect, Error::file_not_found); + try { + a = C({0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.error() == Error::file_not_found); + } + } + } + } + + SECTION("error to error") + { + using T = expected; + static_assert(std::is_nothrow_assignable_v); // required + + T a(unexpect, 3); + a = T(unexpect, 5); + CHECK(a.error().v == 5 * helper::from_rval); + + a = unexpected(7); + CHECK(a.error().v == 7 * helper::from_rval); + } + } + + SECTION("from lval const") + { + SECTION("value to value") + { + using T = expected; + { + static_assert(not extension || std::is_nothrow_assignable_v); + + T a(std::in_place, 3); + T const b(std::in_place, 5); + a = b; + CHECK(a.value().v == 5 * helper::from_lval_const); + + T c(std::in_place, 7); + a = c; + CHECK(a.value().v == 7 * helper::from_lval_const); + + T const d(std::in_place, 11); + a = std::move(d); + CHECK(a.value().v == 11 * helper::from_lval_const); + } + + { + static_assert(not extension || std::is_nothrow_assignable_v); + + T a(std::in_place, 3); + helper const b(5); + a = b; + CHECK(a.value().v == 5 * helper::from_lval_const); + + helper c(7); + a = c; + CHECK(a.value().v == 7 * helper::from_lval); + + helper const d(11); + a = std::move(d); + CHECK(a.value().v == 11 * helper::from_rval_const); + } + } + + SECTION("value to error") + { + SECTION("nothrow move") + { + using T = expected; + static_assert(not extension || not std::is_nothrow_assignable_v); + + { + T a(std::in_place, 3); + T const b(unexpect, 5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const * helper::from_rval); + } + + { + T a(std::in_place, 4); + try { + T const b(unexpect, {0.0}); + a = b; // copy construction on a side of `New tmp` will throw + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.value() == 4); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v const &>); + T a(std::in_place, 4); + unexpected const b(5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const * helper::from_rval); + } + + { + T a(std::in_place, 4); + try { + unexpected const b({0.0}); + a = b; + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.value() == 4); + } + } + } + + SECTION("throwing") + { + using T = expected; + static_assert(not extension || not std::is_nothrow_assignable_v); + + { + T a(std::in_place, 3); + T const b(unexpect, 5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place, 4); + try { + T const b(unexpect, {0.0}); + a = b; + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.value() == 4); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v const &>); + T a(std::in_place, 4); + unexpected const b(5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place, 4); + try { + unexpected const b({0.0}); + a = b; + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.value() == 4); + } + } + } + + SECTION("nothrow copy") + { + using T = expected; + static_assert(not extension || std::is_nothrow_assignable_v); + + { + T a(std::in_place, 3); + T const b(unexpect, 5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place, 4); + try { + T const b(unexpect, {0.0}); + a = b; + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + + { + static_assert(not extension || std::is_nothrow_assignable_v const &>); + T a(std::in_place, 4); + unexpected const b(5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place, 4); + try { + unexpected b(std::in_place, {0.0}); + a = b; + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + } + } + + SECTION("error to value") + { + SECTION("nothrow move") + { + using T = expected; + static_assert(not extension || not std::is_nothrow_assignable_v); + + { + T a(unexpect, Error::file_not_found); + T const b(std::in_place, 5); + a = b; + CHECK(a.value().v == 5 * helper::from_lval_const * helper::from_rval); + } + + { + T a(unexpect, Error::file_not_found); + try { + T const b(std::in_place, {0.0}); + a = b; // copy construction on a side of `New tmp` will throw + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.error() == Error::file_not_found); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v); + T a(unexpect, Error::file_not_found); + M const b(5); + a = b; + CHECK(a.value().v == 5 * helper::from_lval_const * helper::from_rval); + } + + { + T a(unexpect, Error::file_not_found); + try { + M const b({0.0}); + a = b; // copy construction on a side of `New tmp` will throw + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.error() == Error::file_not_found); + } + } + } + + SECTION("throwing") + { + using T = expected; + static_assert(not extension || not std::is_nothrow_assignable_v); + + { + T a(unexpect, Error::file_not_found); + T const b(std::in_place, 5); + a = b; + CHECK(a.value().v == 5 * helper::from_lval_const); + } + + { + T a(unexpect, Error::file_not_found); + try { + T const b(std::in_place, {0.0}); + a = b; + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.error() == Error::file_not_found); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v); + T a(unexpect, Error::file_not_found); + E const b(5); + a = b; + CHECK(a.value().v == 5 * helper::from_lval_const); + } + + { + T a(unexpect, Error::file_not_found); + try { + E const b({0.0}); + a = b; + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.error() == Error::file_not_found); + } + } + } + + SECTION("nothrow copy") + { + using T = expected; + static_assert(not extension || std::is_nothrow_assignable_v); + + { + T a(unexpect, Error::file_not_found); + T const b(std::in_place, 5); + a = b; + CHECK(a.value().v == 5 * helper::from_lval_const); + } + + { + T a(unexpect, Error::file_not_found); + try { + T const b(std::in_place, {0.0}); + a = b; + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + + { + static_assert(not extension || std::is_nothrow_assignable_v); + T a(unexpect, Error::file_not_found); + C const b(5); + a = b; + CHECK(a.value().v == 5 * helper::from_lval_const); + } + + { + T a(unexpect, Error::file_not_found); + try { + C const b({0.0}); + a = b; + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + } + } + + SECTION("error to error") + { + using T = expected; + static_assert(not extension || std::is_nothrow_assignable_v); + + T a(unexpect, 3); + T const b(unexpect, 5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + + unexpected const c(7); + a = c; + CHECK(a.error().v == 7 * helper::from_lval_const); + } + } + } + SECTION("accessors") { SECTION("value") diff --git a/tests/util/helper_types.hpp b/tests/util/helper_types.hpp index 525d2d7f..507e98f7 100644 --- a/tests/util/helper_types.hpp +++ b/tests/util/helper_types.hpp @@ -11,7 +11,7 @@ #include #include -template struct helper_t { +template struct helper_t { static inline int state = 0; int v = {}; @@ -61,9 +61,24 @@ template struct helper_t { return *this; } + // See table below constexpr helper_t(helper_t &o) noexcept : v(o.v) { v *= from_lval; } - constexpr helper_t(helper_t const &o) noexcept : v(o.v) { v *= from_lval_const; } - constexpr helper_t(helper_t &&o) noexcept : v(o.v) { v *= from_rval; } + constexpr helper_t(helper_t const &o) noexcept(V < 2 || V >= 4) : v(o.v) + { + v *= from_lval_const; + if constexpr (V >= 2 && V < 4) { + if (v == 0) + throw std::runtime_error("invalid input"); + } + } + constexpr helper_t(helper_t &&o) noexcept(V < 3 || V >= 5) : v(o.v) + { + v *= from_rval; + if constexpr (V >= 3 && V < 5) { + if (v == 0) + throw std::runtime_error("invalid input"); + } + } constexpr helper_t(helper_t const &&o) noexcept : v(o.v) { v *= from_rval_const; } // The intent of non-constexpr constructors is to make sure that they are never optimized away, @@ -77,12 +92,7 @@ template struct helper_t { state += v; } - helper_t(std::initializer_list list) noexcept(true) : v(init(list)) - { - if (v == 0) - std::terminate(); - state += v; - } + helper_t(std::initializer_list list) noexcept(true) : v(init(list)) { state += v; } // Potentially throwing constructor constexpr helper_t(std::initializer_list list, std::integral auto... a) noexcept(true) @@ -106,6 +116,27 @@ template struct helper_t { friend std::strong_ordering operator<=>(helper_t, helper_t) = delete; }; +// helper_t +// V is_nothrow_copy_constructible is_nothrow_move_constructible +// 0 1 1 +// 1 1 1 +// 2 0 1 +// 3 0 0 +// 4 1 0 +// 5 1 1 +static_assert(std::is_nothrow_copy_constructible_v>); +static_assert(std::is_nothrow_move_constructible_v>); +static_assert(std::is_nothrow_copy_constructible_v>); +static_assert(std::is_nothrow_move_constructible_v>); +static_assert(not std::is_nothrow_copy_constructible_v>); +static_assert(std::is_nothrow_move_constructible_v>); +static_assert(not std::is_nothrow_copy_constructible_v>); +static_assert(not std::is_nothrow_move_constructible_v>); +static_assert(std::is_nothrow_copy_constructible_v>); +static_assert(not std::is_nothrow_move_constructible_v>); +static_assert(std::is_nothrow_copy_constructible_v>); +static_assert(std::is_nothrow_move_constructible_v>); + // Swap will also multiply witness by a prime template constexpr void swap(helper_t &l, helper_t &r) { From cdb1d5b6fc792e589e4ece7996edf47c391ca722 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Mon, 21 Apr 2025 12:22:43 +0100 Subject: [PATCH 27/46] Minor cleanup --- include/pfn/expected.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index c31291a4..ba2e5765 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -215,16 +215,16 @@ template class expected { { if constexpr (::std::is_nothrow_constructible_v) { ::std::destroy_at(::std::addressof(oldval)); - ::std::construct_at(::std::addressof(newval), FWD(args)...); + ::std::construct_at(::std::addressof(newval), ::std::forward(args)...); } else if constexpr (::std::is_nothrow_move_constructible_v) { - New tmp(FWD(args)...); + New tmp(::std::forward(args)...); ::std::destroy_at(::std::addressof(oldval)); ::std::construct_at(::std::addressof(newval), std::move(tmp)); } else { Old tmp(std::move(oldval)); ::std::destroy_at(::std::addressof(oldval)); try { - ::std::construct_at(::std::addressof(newval), FWD(args)...); + ::std::construct_at(::std::addressof(newval), ::std::forward(args)...); } catch (...) { ::std::construct_at(::std::addressof(oldval), std::move(tmp)); throw; @@ -309,7 +309,7 @@ template class expected { template > constexpr explicit(not ::std::is_convertible_v) expected(U &&v) // - noexcept(::std::is_nothrow_constructible_v) // extension + noexcept(::std::is_nothrow_constructible_v) // extension requires(_can_convert::value) : v_(FWD(v)), set_(true) { @@ -436,7 +436,7 @@ template class expected { template constexpr expected &operator=(U &&s) // - noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v + noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v && (::std::is_nothrow_move_constructible_v || ::std::is_nothrow_move_constructible_v)) // extension requires(_can_convert_assign::value) { @@ -468,7 +468,7 @@ template class expected { template constexpr expected &operator=(unexpected &&s) // - noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v + noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v && (::std::is_nothrow_move_constructible_v || ::std::is_nothrow_move_constructible_v)) // extension requires(::std::is_constructible_v && ::std::is_assignable_v && (::std::is_nothrow_constructible_v || ::std::is_nothrow_move_constructible_v From cb48cac6be7d18de04a927fe328145e0327e013f Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Mon, 21 Apr 2025 14:15:26 +0100 Subject: [PATCH 28/46] Implement emplace --- include/pfn/expected.hpp | 27 ++++++++++++- tests/pfn/expected.cpp | 75 ++++++++++++++++++++++++++++++++++++- tests/util/helper_types.hpp | 8 ++-- 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index ba2e5765..d10f867c 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -483,8 +483,31 @@ template class expected { return *this; } - template constexpr T &emplace(Args &&...) noexcept; - template constexpr T &emplace(std::initializer_list, Args &&...) noexcept; + template + constexpr T &emplace(Args &&...args) noexcept(true) + requires(::std::is_nothrow_constructible_v) + { + if (set_) { + ::std::destroy_at(::std::addressof(v_)); + } else { + ::std::destroy_at(::std::addressof(e_)); + set_ = true; + } + return *::std::construct_at(::std::addressof(v_), std::forward(args)...); + } + + template + constexpr T &emplace(::std::initializer_list il, Args &&...args) noexcept(true) + requires(::std::is_nothrow_constructible_v &, Args...>) + { + if (set_) { + ::std::destroy_at(::std::addressof(v_)); + } else { + ::std::destroy_at(::std::addressof(e_)); + set_ = true; + } + return *::std::construct_at(::std::addressof(v_), il, std::forward(args)...); + } // [expected.object.swap], swap constexpr void swap(expected &) noexcept(/* TODO */ false); diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index a09f1f6e..b210ca46 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -915,7 +915,7 @@ TEST_CASE("expected", "[expected][polyfill]") } } - SECTION("assign") + SECTION("assignment") { using M = helper_t<2>; // nothrow move constructible using E = helper_t<3>; // may throw on move and copy @@ -1542,6 +1542,79 @@ TEST_CASE("expected", "[expected][polyfill]") } } + SECTION("emplace") + { + using T = expected, Error>; + SECTION("value to value") + { + { + T a(std::in_place, 13); + a.emplace(2, 3, 5); + CHECK(a.value().v == 2 * 3 * 5); + } + + { + T a(std::in_place, 13); + a.emplace({7.0, 11.0}); + CHECK(a.value().v == 7 * 11); + } + } + + SECTION("error to value") + { + { + T a(unexpect, Error::file_not_found); + a.emplace(2, 3, 5); + CHECK(a.value().v == 2 * 3 * 5); + } + + { + T a(unexpect, Error::file_not_found); + a.emplace({7.0, 11.0}); + CHECK(a.value().v == 7 * 11); + } + } + + SECTION("constexpr") + { + using T = expected; + { + constexpr auto fn = [](auto &&...v) constexpr -> T { + T a(unexpect, Error::unknown); + a.emplace(std::forward(v)...); + return a; + }; + + constexpr auto a = fn(std::initializer_list{5.0, 11.0}, 3); + static_assert(a.value().v == 5 * 11 * 3 * helper::from_rval); + } + + { + constexpr auto fn = [](auto &&...v) constexpr -> T { + T a(std::in_place, {9.0}, 1); + a.emplace(std::forward(v)...); + return a; + }; + + constexpr auto a = fn(std::initializer_list{7.0, 13.0}, 3); + static_assert(a.value().v == 7 * 13 * 3 * helper::from_rval); + } + + { + constexpr auto fn = [](auto &&...args) constexpr -> bool { + return requires { + expected(unexpect, Error::unknown).emplace(std::forward(args)...); + }; + }; + + static_assert(fn(std::initializer_list{3.0, 5.0}, 1)); + static_assert(not fn(1, 2)); + } + + SUCCEED(); + } + } + SECTION("accessors") { SECTION("value") diff --git a/tests/util/helper_types.hpp b/tests/util/helper_types.hpp index 507e98f7..902e38b6 100644 --- a/tests/util/helper_types.hpp +++ b/tests/util/helper_types.hpp @@ -83,12 +83,14 @@ template struct helper_t { // The intent of non-constexpr constructors is to make sure that they are never optimized away, // thus ensuring that any code which relies on them in tests will show up in coverage reports. - helper_t(std::integral auto... a) noexcept(false) + helper_t(std::integral auto... a) noexcept(V >= 8) requires(sizeof...(a) > 0) // intentionally implicit when sizeof...(a) == 1 : v((1 * ... * a)) { - if (v == 0) - throw std::runtime_error("invalid input"); + if constexpr (V < 8) { + if (v == 0) + throw std::runtime_error("invalid input"); + } state += v; } From 8067a2ea198b4695ec479fefe3a356f128f95af0 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 24 May 2025 11:29:45 +0100 Subject: [PATCH 29/46] Add appleclang 15 --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 566ab27a..048639ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,6 +115,9 @@ jobs: - compiler: appleclang osver: 15 modes: "cxx20 cxx23" + - compiler: appleclang + osver: 14 + modes: "cxx20" - compiler: clang release: 18 osver: 15 From 9fafd0943fb5adbefaaed130e1298179ab26fce8 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 24 May 2025 16:55:43 +0100 Subject: [PATCH 30/46] Implement swap --- include/pfn/expected.hpp | 66 ++++++- tests/pfn/expected.cpp | 347 ++++++++++++++++++++++++++++++++++++ tests/util/helper_types.hpp | 15 +- 3 files changed, 424 insertions(+), 4 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index d10f867c..7ada83a8 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -211,7 +211,8 @@ template class expected { // [expected.object.assign] template - static constexpr void _reinit(New &newval, Old &oldval, Args &&...args) + static constexpr void _reinit(New &newval, Old &oldval, Args &&...args) // + noexcept(::std::is_nothrow_constructible_v) { if constexpr (::std::is_nothrow_constructible_v) { ::std::destroy_at(::std::addressof(oldval)); @@ -232,6 +233,36 @@ template class expected { } } + static constexpr void _swap(expected &lhs, expected &rhs) // + noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_move_constructible_v) + { + if constexpr (::std::is_nothrow_move_constructible_v) { + E tmp(::std::move(rhs.e_)); + ::std::destroy_at(::std::addressof(rhs.e_)); + try { + ::std::construct_at(::std::addressof(rhs.v_), ::std::move(lhs.v_)); + ::std::destroy_at(::std::addressof(lhs.v_)); + ::std::construct_at(::std::addressof(lhs.e_), ::std::move(tmp)); + } catch (...) { + ::std::construct_at(::std::addressof(rhs.e_), ::std::move(tmp)); + throw; + } + } else { + T tmp(::std::move(lhs.v_)); + ::std::destroy_at(::std::addressof(lhs.v_)); + try { + ::std::construct_at(::std::addressof(lhs.e_), ::std::move(rhs.e_)); + ::std::destroy_at(::std::addressof(rhs.e_)); + ::std::construct_at(::std::addressof(rhs.v_), ::std::move(tmp)); + } catch (...) { + ::std::construct_at(::std::addressof(lhs.v_), ::std::move(tmp)); + throw; + } + } + lhs.set_ = false; + rhs.set_ = true; + } + public: using value_type = T; using error_type = E; @@ -510,8 +541,37 @@ template class expected { } // [expected.object.swap], swap - constexpr void swap(expected &) noexcept(/* TODO */ false); - constexpr friend void swap(expected &x, expected &y) noexcept(noexcept(x.swap(y))); + constexpr void + swap(expected &rhs) noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_swappable_v + && ::std::is_nothrow_move_constructible_v && ::std::is_nothrow_swappable_v) + requires(::std::is_swappable_v && ::std::is_swappable_v && ::std::is_move_constructible_v + && ::std::is_move_constructible_v + && (::std::is_nothrow_move_constructible_v || ::std::is_nothrow_move_constructible_v)) + { + bool const lhset = has_value(); + bool const rhset = rhs.has_value(); + if (lhset == rhset) { + if (lhset) { + using ::std::swap; + swap(v_, rhs.v_); + } else { + using ::std::swap; + swap(e_, rhs.e_); + } + } else { + if (lhset) { + _swap(*this, rhs); + } else { + _swap(rhs, *this); + } + } + } + + constexpr friend void swap(expected &x, expected &y) noexcept(noexcept(x.swap(y))) + requires requires { x.swap(y); } + { + x.swap(y); + } // [expected.object.obs], observers constexpr T const *operator->() const noexcept diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index b210ca46..41512e97 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -410,6 +410,28 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") } } +namespace { + +template struct non_swappable { + friend void swap(T &, T &) = delete; +}; + +template struct swappable { + friend void swap(T &, T &) noexcept(false) {} +}; + +template struct nothrow_swappable { + friend void swap(T &, T &) noexcept(true) {} +}; + +// NOTE: not the same as std::is_swappable https://eel.is/c++draft/swappable.requirements +// because std::is_swappable brings std::swap https://eel.is/c++draft/utility.swap#lib:swap +// into scope, which we do not want for this check. +template +concept is_swappable = requires { swap(std::declval(), std::declval()); }; + +} // namespace + TEST_CASE("expected", "[expected][polyfill]") { using pfn::bad_expected_access; @@ -1615,6 +1637,331 @@ TEST_CASE("expected", "[expected][polyfill]") } } + SECTION("swap") + { + SECTION("non-swappable") + { + struct A : non_swappable {}; + static_assert(not std::is_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + + static_assert(not is_swappable>); + static_assert(not is_swappable>); + + SUCCEED(); + } + + SECTION("non-move-constructible") + { + struct A : swappable { + A(A &&) = delete; + }; + static_assert(std::is_swappable_v); + static_assert(not std::is_nothrow_swappable_v); + static_assert(not std::is_move_constructible_v); + + static_assert(not is_swappable>); + static_assert(not is_swappable>); + + SUCCEED(); + } + + SECTION("non-nothrow-move-constructible") + { + struct A : swappable { + A(A &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(not std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + struct B : swappable { + B(B &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(not std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + static_assert(is_swappable>); + static_assert(not is_swappable>); + + SUCCEED(); + } + + SECTION("nothrow-swappable non-nothrow-move-constructible") + { + struct A : nothrow_swappable { + A(A &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + struct B : nothrow_swappable { + B(B &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + static_assert(is_swappable>); + static_assert(not is_swappable>); + + SUCCEED(); + } + + SECTION("swappabla, non-nothrow-move-constructible") + { + struct A : swappable { + A(A &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(not std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + struct B : swappable { + B(B &&) noexcept(true) = default; + }; + static_assert(std::is_swappable_v); + static_assert(not std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + static_assert(is_swappable>); + + { + using T = expected; + static_assert(is_swappable); + static_assert(not noexcept(swap(std::declval(), std::declval()))); + } + { + using T = expected; + static_assert(is_swappable); + static_assert(not noexcept(swap(std::declval(), std::declval()))); + } + + SUCCEED(); + } + + SECTION("nothrow-swappabla, non-nothrow-move-constructible") + { + struct A : nothrow_swappable { + A(A &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + struct B : nothrow_swappable { + B(B &&) noexcept(true) = default; + }; + static_assert(std::is_swappable_v); + static_assert(std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + static_assert(is_swappable>); + + { + using T = expected; + static_assert(is_swappable); + static_assert(not noexcept(swap(std::declval(), std::declval()))); + } + { + using T = expected; + static_assert(is_swappable); + static_assert(not noexcept(swap(std::declval(), std::declval()))); + } + + SUCCEED(); + } + + SECTION("nothrow-swappabla, nothrow-move-constructible") + { + struct A : nothrow_swappable { + A(A &&) noexcept(true) = default; + }; + static_assert(std::is_swappable_v); + static_assert(std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + + struct B : nothrow_swappable { + B(B &&) noexcept(true) = default; + }; + static_assert(std::is_swappable_v); + static_assert(std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + static_assert(is_swappable>); + + { + using T = expected; + static_assert(is_swappable); + static_assert(noexcept(swap(std::declval(), std::declval()))); + } + { + using T = expected; + static_assert(is_swappable); + static_assert(noexcept(swap(std::declval(), std::declval()))); + } + + SUCCEED(); + } + + SECTION("swap same") + { + SECTION("value") + { + using T = expected; + T a(7); + T b(13); + swap(a, b); + CHECK(a.value().v == 13 * helper::swapped); + CHECK(b.value().v == 7 * helper::swapped); + } + + SECTION("error") + { + using T = expected; + T a(unexpect, 17); + T b(unexpect, 23); + swap(a, b); + CHECK(a.error().v == 23 * helper::swapped); + CHECK(b.error().v == 17 * helper::swapped); + } + } + + SECTION("swap error/value") + { + using T = expected>; + T a(unexpect, 19); + T b(27); + swap(a, b); + CHECK(a.value().v == 27 * helper::from_rval); + CHECK(b.error().v == 19 * helper::from_rval * helper::from_rval); + } + + SECTION("swap value/error") + { + SECTION("nothrow") + { + using T = expected>; + T a(17); + T b(unexpect, 29); + swap(a, b); + CHECK(a.error().v == 29 * helper::from_rval * helper::from_rval); + CHECK(b.value().v == 17 * helper::from_rval); + } + + static_assert(not std::is_nothrow_move_constructible_v>); + static_assert(std::is_nothrow_move_constructible_v>); + SECTION("nothrow error") + { + using T = expected, helper_t<30>>; + SECTION("happy path") + { + T a(13); + T b(unexpect, 23); + swap(a, b); + CHECK(a.error().v == 23 * helper::from_rval * helper::from_rval); + CHECK(b.value().v == 13 * helper::from_rval); + } + + SECTION("exception") + { + T a(std::in_place, {0.0}); + T b(unexpect, 23); + try { + swap(a, b); + FAIL(); + } catch (std::runtime_error const &) { + CHECK(a.value().v == 0); + CHECK(b.error().v == 23 * helper::from_rval * helper::from_rval); + } + } + } + + SECTION("nothrow value") + { + using T = expected, helper_t<33>>; + SECTION("happy path") + { + T a(7); + T b(unexpect, 11); + swap(a, b); + CHECK(a.error().v == 11 * helper::from_rval); + CHECK(b.value().v == 7 * helper::from_rval * helper::from_rval); + } + + SECTION("exception") + { + T a(std::in_place, 13); + T b(unexpect, {0.0}); + try { + swap(a, b); + FAIL(); + } catch (std::runtime_error const &) { + CHECK(a.value().v == 13 * helper::from_rval * helper::from_rval); + CHECK(b.error().v == 0); + } + } + } + } + + SECTION("constexpr") + { + using T = expected; + + SECTION("to error") + { + constexpr auto fn = [](T &&v) constexpr -> T { + T tmp{unexpect, Error::unknown}; + swap(tmp, v); + return v; + }; + + constexpr T a = fn(T(unexpect, Error::file_not_found)); + static_assert(a.error() == Error::unknown); + + constexpr T b = fn(T(12)); + static_assert(b.error() == Error::unknown); + + SUCCEED(); + } + + SECTION("to value") + { + constexpr auto fn = [](T &&v) constexpr -> T { + T tmp{7}; + swap(tmp, v); + return v; + }; + + constexpr T a = fn(T(unexpect, Error::file_not_found)); + static_assert(a.value() == 7); + + constexpr T b = fn(T(12)); + static_assert(b.value() == 7); + + SUCCEED(); + } + } + } + SECTION("accessors") { SECTION("value") diff --git a/tests/util/helper_types.hpp b/tests/util/helper_types.hpp index 902e38b6..244482a4 100644 --- a/tests/util/helper_types.hpp +++ b/tests/util/helper_types.hpp @@ -71,7 +71,9 @@ template struct helper_t { throw std::runtime_error("invalid input"); } } - constexpr helper_t(helper_t &&o) noexcept(V < 3 || V >= 5) : v(o.v) + constexpr helper_t(helper_t &&o) noexcept(V < 3 || V >= 5) + requires(V < 30) + : v(o.v) { v *= from_rval; if constexpr (V >= 3 && V < 5) { @@ -79,6 +81,17 @@ template struct helper_t { throw std::runtime_error("invalid input"); } } + helper_t(helper_t &&o) noexcept(V < 33 || V >= 35) + requires(V >= 30) + : v(o.v) + { + v *= from_rval; + state += v; + if constexpr (V >= 33 && V < 35) { + if (v == 0) + throw std::runtime_error("invalid input"); + } + } constexpr helper_t(helper_t const &&o) noexcept : v(o.v) { v *= from_rval_const; } // The intent of non-constexpr constructors is to make sure that they are never optimized away, From dd9fb03924ba5fd883ebf3abe833963c51575cff Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Mon, 26 May 2025 11:08:34 +0100 Subject: [PATCH 31/46] Accessor unit tests --- tests/pfn/expected.cpp | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 41512e97..4454cbd5 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -1968,22 +1968,37 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; - T a = {11}; - CHECK(a.value().v == 11); - CHECK(std::as_const(a).value().v == 11); - CHECK(std::move(std::as_const(a)).value().v == 11); - CHECK(std::move(a).value().v == 11); + { + T a = {11}; + CHECK(a.value().v == 11); + CHECK(std::as_const(a).value().v == 11); + CHECK(std::move(std::as_const(a)).value().v == 11); + CHECK(std::move(a).value().v == 11); + } + + { + T a = {13}; + CHECK(a); + helper b{1}; + CHECK((b = a.value()).v == 13 * helper::from_lval); + CHECK((b = std::as_const(a).value()).v == 13 * helper::from_lval_const); + CHECK((b = std::move(std::as_const(a)).value()).v == 13 * helper::from_rval_const); + CHECK((b = std::move(a).value()).v == 13 * helper::from_rval); + } { + T a = {17}; helper b{1}; - CHECK((b = a.value()).v == 11 * helper::from_lval); - CHECK((b = std::as_const(a).value()).v == 11 * helper::from_lval_const); - CHECK((b = std::move(std::as_const(a)).value()).v == 11 * helper::from_rval_const); - CHECK((b = std::move(a).value()).v == 11 * helper::from_rval); + CHECK(a); + CHECK((b = *a).v == 17 * helper::from_lval); + CHECK((b = *std::as_const(a)).v == 17 * helper::from_lval_const); + CHECK((b = *std::move(std::as_const(a))).v == 17 * helper::from_rval_const); + CHECK((b = *std::move(a)).v == 17 * helper::from_rval); } { T a{unexpect, Error::file_not_found}; + CHECK(!a); try { auto _ = a.value(); From 4500820681509295e6322f1407ef1bd5944e6d9a Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Mon, 26 May 2025 12:53:44 +0100 Subject: [PATCH 32/46] Implement value_or and error_or --- include/pfn/expected.hpp | 42 ++++++++++-- tests/pfn/expected.cpp | 134 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 4 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 7ada83a8..fb88a048 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -635,10 +635,44 @@ template class expected { return ::std::move(e_); } - template constexpr T value_or(U &&) const &; - template constexpr T value_or(U &&) &&; - template constexpr E error_or(G &&) const &; - template constexpr E error_or(G &&) &&; + template + constexpr T value_or(U &&v) const & // + noexcept(::std::is_nothrow_copy_constructible_v && ::std::is_nothrow_convertible_v) // extension + { + static_assert(::std::is_copy_constructible_v); + static_assert(::std::is_convertible_v); + return set_ ? v_ : static_cast(FWD(v)); + } + template + constexpr T value_or(U &&v) && // + noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_convertible_v) // extension + { + static_assert(::std::is_move_constructible_v); + static_assert(::std::is_convertible_v); + return set_ ? ::std::move(v_) : static_cast(FWD(v)); + } + template + constexpr E error_or(G &&e) const & // + noexcept(::std::is_nothrow_copy_constructible_v && ::std::is_nothrow_convertible_v) // extension + { + static_assert(::std::is_copy_constructible_v); + static_assert(::std::is_convertible_v); + if (set_) { + return FWD(e); + } + return e_; + } + template + constexpr E error_or(G &&e) && // + noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_convertible_v) // extension + { + static_assert(::std::is_move_constructible_v); + static_assert(::std::is_convertible_v); + if (set_) { + return FWD(e); + } + return ::std::move(e_); + } // [expected.object.monadic], monadic operations template constexpr auto and_then(F &&f) &; diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 4454cbd5..48f36734 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -2048,5 +2048,139 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK((b = std::move(a).error()).v == 17 * helper::from_rval); } } + + SECTION("value_or") + { + using T = expected; + static_assert(not noexcept(std::declval().value_or(std::declval()))); + static_assert(noexcept(std::declval().value_or(std::declval>()))); + static_assert(not noexcept(std::declval().value_or(std::declval()))); + static_assert(noexcept(std::declval().value_or(std::declval>()))); + + SECTION("value") + { + T a(7); + CHECK(a.value_or(0) == helper(7 * helper::from_lval_const)); + CHECK(std::as_const(a).value_or(0) == helper(7 * helper::from_lval_const)); + CHECK(std::move(std::as_const(a)).value_or(0) == helper(7 * helper::from_lval_const)); + CHECK(std::move(a).value_or(0) == helper(7 * helper::from_rval)); + } + + SECTION("error") + { + { + T a(unexpect, Error::file_not_found); + CHECK(a.value_or(13) == helper(13)); + CHECK(std::move(a).value_or(5) == helper(5)); + } + + { + T const a(unexpect, Error::unknown); + helper b(11); + CHECK(a.value_or(b) == helper(11 * helper::from_lval)); + CHECK(a.value_or(std::as_const(b)) == helper(11 * helper::from_lval_const)); + CHECK(a.value_or(std::move(std::as_const(b))) == helper(11 * helper::from_rval_const)); + CHECK(a.value_or(std::move(b)) == helper(11 * helper::from_rval)); + } + } + + SECTION("noexcept extension") + { + { + using T = expected, Error>; + static_assert(not std::is_nothrow_copy_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_convertible_v); + static_assert(std::is_nothrow_convertible_v, T::value_type>); + + static_assert(not noexcept(std::declval().value_or(std::declval()))); + static_assert(noexcept(std::declval().value_or(std::declval>()))); + static_assert(not noexcept(std::declval().value_or(std::declval()))); + static_assert(not noexcept(std::declval().value_or(std::declval>()))); + } + + { + using T = expected, Error>; + static_assert(std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_convertible_v); + static_assert(std::is_nothrow_convertible_v, T::value_type>); + + static_assert(not noexcept(std::declval().value_or(std::declval()))); + static_assert(not noexcept(std::declval().value_or(std::declval>()))); + static_assert(not noexcept(std::declval().value_or(std::declval()))); + static_assert(noexcept(std::declval().value_or(std::declval>()))); + } + + SUCCEED(); + } + } + + SECTION("error_or") + { + using T = expected; + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(noexcept(std::declval().error_or(std::declval>()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(noexcept(std::declval().error_or(std::declval>()))); + + SECTION("error") + { + T a(unexpect, 7); + CHECK(a.error_or(0) == helper(7 * helper::from_lval_const)); + CHECK(std::as_const(a).error_or(0) == helper(7 * helper::from_lval_const)); + CHECK(std::move(std::as_const(a)).error_or(0) == helper(7 * helper::from_lval_const)); + CHECK(std::move(a).error_or(0) == helper(7 * helper::from_rval)); + } + + SECTION("value") + { + { + T a(17); + CHECK(a.error_or(17) == helper(17)); + CHECK(std::move(a).error_or(5) == helper(5)); + } + + { + T const a(23); + helper b(11); + CHECK(a.error_or(b) == helper(11 * helper::from_lval)); + CHECK(a.error_or(std::as_const(b)) == helper(11 * helper::from_lval_const)); + CHECK(a.error_or(std::move(std::as_const(b))) == helper(11 * helper::from_rval_const)); + CHECK(a.error_or(std::move(b)) == helper(11 * helper::from_rval)); + } + } + + SECTION("noexcept extension") + { + { + using T = expected>; + static_assert(not std::is_nothrow_copy_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_convertible_v); + static_assert(std::is_nothrow_convertible_v, T::error_type>); + + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(noexcept(std::declval().error_or(std::declval>()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not noexcept(std::declval().error_or(std::declval>()))); + } + + { + using T = expected>; + static_assert(std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_convertible_v); + static_assert(std::is_nothrow_convertible_v, T::error_type>); + + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not noexcept(std::declval().error_or(std::declval>()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(noexcept(std::declval().error_or(std::declval>()))); + } + + SUCCEED(); + } + } } } From c4dc29168c70b53ed74b710fe8fefaede875b87a Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Mon, 26 May 2025 18:23:06 +0100 Subject: [PATCH 33/46] Add empty LIBFN_ASSERT --- tests/pfn/expected.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 48f36734..1da14064 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -3,6 +3,9 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC +// TODO: Add death tests. Until then, empty definition to avoid false "no coverage" reports +#define LIBFN_ASSERT(...) + #include #include From acef61e68389a987e9bef27f944fc472aed19ee9 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Mon, 26 May 2025 15:15:10 +0100 Subject: [PATCH 34/46] Add monadic functions --- include/pfn/expected.hpp | 254 ++++++++++++++++++++++++++++++++++++--- tests/pfn/expected.cpp | 114 ++++++++++++++++++ 2 files changed, 348 insertions(+), 20 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index fb88a048..9041a287 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -156,6 +157,15 @@ template class expected; namespace detail { template constexpr bool _is_some_expected = false; template constexpr bool _is_some_expected<::pfn::expected> = true; + +template +constexpr bool _is_valid_expected = // + not ::std::is_reference_v // + && not ::std::is_function_v // + && not ::std::is_same_v<::std::remove_cv_t, ::std::in_place_t> // + && not ::std::is_same_v<::std::remove_cv_t, unexpect_t> // + && not _is_some_unexpected<::std::remove_cv_t> // + && detail::_is_valid_unexpected; } // namespace detail // declare void specialization @@ -164,7 +174,8 @@ template class expected; template class expected { -private: + static_assert(detail::_is_valid_expected); + template using _can_convert_detail = ::std::bool_constant< // not(::std::is_same_v && ::std::is_same_v) // @@ -263,6 +274,80 @@ template class expected { rhs.set_ = true; } + template + static constexpr auto _and_then(Self &&self, Fn &&fn) // + noexcept(::std::is_nothrow_invocable_v + && ::std::is_nothrow_constructible_v) + requires(::std::is_constructible_v) + { + using result_t = ::std::remove_cvref_t<::std::invoke_result_t>; + static_assert(detail::_is_some_expected); + static_assert(::std::is_same_v::error_type>); + if (self.has_value()) { + return ::std::invoke(FWD(fn), FWD(self).value()); + } + return result_t(unexpect, FWD(self).error()); + } + + template + static constexpr auto _or_else(Self &&self, Fn &&fn) // + noexcept(::std::is_nothrow_invocable_v + && ::std::is_nothrow_constructible_v) + requires(::std::is_constructible_v) + { + using result_t = ::std::remove_cvref_t<::std::invoke_result_t>; + static_assert(detail::_is_some_expected); + static_assert(::std::is_same_v::value_type>); + if (self.has_value()) { + return result_t(::std::in_place, FWD(self).value()); + } + return ::std::invoke(FWD(fn), FWD(self).error()); + } + + template + static constexpr auto _transform(Self &&self, Fn &&fn) // + noexcept(::std::is_nothrow_invocable_v + && ::std::is_nothrow_constructible_v + && (::std::is_void_v<::std::invoke_result_t> + || ::std::is_nothrow_constructible_v< + ::std::remove_cv_t<::std::invoke_result_t>, + ::std::invoke_result_t>)) + requires(::std::is_constructible_v) + { + using value_t = ::std::remove_cv_t<::std::invoke_result_t>; + static_assert(detail::_is_valid_expected); + using result_t = expected; + if (self.has_value()) { + if constexpr (not ::std::is_void_v) { + static_assert(::std::is_constructible_v>); + return result_t(::std::in_place, ::std::invoke(FWD(fn), FWD(self).value())); + } else { + ::std::invoke(FWD(fn), FWD(self).value()); + return result_t(); + } + } + return result_t(unexpect, FWD(self).error()); + } + + template + static constexpr auto _transform_error(Self &&self, Fn &&fn) // + noexcept(::std::is_nothrow_invocable_v + && ::std::is_nothrow_constructible_v + && ::std::is_nothrow_constructible_v< + ::std::remove_cv_t<::std::invoke_result_t>, + ::std::invoke_result_t>) + requires(::std::is_constructible_v) + { + using error_t = ::std::remove_cv_t<::std::invoke_result_t>; + static_assert(detail::_is_valid_unexpected); + static_assert(::std::is_constructible_v>); + using result_t = expected; + if (not self.has_value()) { + return result_t(unexpect, ::std::invoke(FWD(fn), FWD(self).error())); + } + return result_t(::std::in_place, FWD(self).value()); + } + public: using value_type = T; using error_type = E; @@ -675,29 +760,158 @@ template class expected { } // [expected.object.monadic], monadic operations - template constexpr auto and_then(F &&f) &; - template constexpr auto and_then(F &&f) &&; - template constexpr auto and_then(F &&f) const &; - template constexpr auto and_then(F &&f) const &&; - template constexpr auto or_else(F &&f) &; - template constexpr auto or_else(F &&f) &&; - template constexpr auto or_else(F &&f) const &; - template constexpr auto or_else(F &&f) const &&; - template constexpr auto transform(F &&f) &; - template constexpr auto transform(F &&f) &&; - template constexpr auto transform(F &&f) const &; - template constexpr auto transform(F &&f) const &&; - template constexpr auto transform_error(F &&f) &; - template constexpr auto transform_error(F &&f) &&; - template constexpr auto transform_error(F &&f) const &; - template constexpr auto transform_error(F &&f) const &&; + template + constexpr auto and_then(F &&f) & // + noexcept(noexcept(_and_then)) // extension + -> decltype(_and_then(*this, FWD(f))) + { + return _and_then(*this, FWD(f)); + } + template + constexpr auto and_then(F &&f) && // + noexcept(noexcept(_and_then)) // extension + -> decltype(_and_then(::std::move(*this), FWD(f))) + { + return _and_then(::std::move(*this), FWD(f)); + } + template + constexpr auto and_then(F &&f) const & // + noexcept(noexcept(_and_then)) // extension + -> decltype(_and_then(*this, FWD(f))) + { + return _and_then(*this, FWD(f)); + } + template + constexpr auto and_then(F &&f) const && // + noexcept(noexcept(_and_then)) // extension + -> decltype(_and_then(::std::move(*this), FWD(f))) + { + return _and_then(::std::move(*this), FWD(f)); + } + + template + constexpr auto or_else(F &&f) & // + noexcept(noexcept(_or_else)) // extension + -> decltype(_or_else(*this, FWD(f))) + { + return _or_else(*this, FWD(f)); + } + template + constexpr auto or_else(F &&f) && // + noexcept(noexcept(_or_else)) // extension + -> decltype(_or_else(::std::move(*this), FWD(f))) + { + return _or_else(::std::move(*this), FWD(f)); + } + template + constexpr auto or_else(F &&f) const & // + noexcept(noexcept(_or_else)) // extension + -> decltype(_or_else(*this, FWD(f))) + { + return _or_else(*this, FWD(f)); + } + template + constexpr auto or_else(F &&f) const && // + noexcept(noexcept(_or_else)) // extension + -> decltype(_or_else(::std::move(*this), FWD(f))) + { + return _or_else(::std::move(*this), FWD(f)); + } + + template + constexpr auto transform(F &&f) & // + noexcept(noexcept(_transform)) // extension + -> decltype(_transform(*this, FWD(f))) + { + return _transform(*this, FWD(f)); + } + template + constexpr auto transform(F &&f) && // + noexcept(noexcept(_transform)) // extension + -> decltype(_transform(::std::move(*this), FWD(f))) + { + return _transform(::std::move(*this), FWD(f)); + } + template + constexpr auto transform(F &&f) const & // + noexcept(noexcept(_transform)) // extension + -> decltype(_transform(*this, FWD(f))) + { + return _transform(*this, FWD(f)); + } + template + constexpr auto transform(F &&f) const && // + noexcept(noexcept(_transform)) // extension + -> decltype(_transform(::std::move(*this), FWD(f))) + { + return _transform(::std::move(*this), FWD(f)); + } + + template + constexpr auto transform_error(F &&f) & // + noexcept(noexcept(_transform_error)) // extension + -> decltype(_transform_error(*this, FWD(f))) + { + return _transform_error(*this, FWD(f)); + } + template + constexpr auto transform_error(F &&f) && // + noexcept(noexcept(_transform_error)) // extension + -> decltype(_transform_error(::std::move(*this), FWD(f))) + { + return _transform_error(::std::move(*this), FWD(f)); + } + template + constexpr auto transform_error(F &&f) const & // + noexcept(noexcept(_transform_error)) // extension + -> decltype(_transform_error(*this, FWD(f))) + { + return _transform_error(*this, FWD(f)); + } + template + constexpr auto transform_error(F &&f) const && // + noexcept(noexcept(_transform_error)) // extension + -> decltype(_transform_error(::std::move(*this), FWD(f))) + { + return _transform_error(::std::move(*this), FWD(f)); + } // [expected.object.eq], equality operators template requires(not ::std::is_void_v) - constexpr friend bool operator==(expected const &x, expected const &y); - template constexpr friend bool operator==(expected const &, const T2 &); - template constexpr friend bool operator==(expected const &, unexpected const &); + constexpr friend bool operator==(expected const &x, expected const &y) // + noexcept(noexcept(*x == *y) && noexcept(x.error() == y.error())) // extension + requires requires { + { *x == *y } -> ::std::convertible_to; + { x.error() == y.error() } -> ::std::convertible_to; + } + { + if (x.has_value() != y.has_value()) { + return false; + } else if (x.has_value()) { + return *x == *y; + } else { + return x.error() == y.error(); + } + } + template + constexpr friend bool operator==(expected const &x, const T2 &v) // + noexcept(noexcept(*x == v)) // extension + requires(not detail::_is_some_expected) && requires { + { *x == v } -> ::std::convertible_to; + } + { + return x.has_value() && static_cast(*x == v); + } + template + constexpr friend bool operator==(expected const &x, unexpected const &e) // + noexcept(noexcept(x.error() == e.error())) // extension + requires requires { + { x.error() == e.error() } -> ::std::convertible_to; + } + { + return (not x.has_value()) && static_cast(x.error() == e.error()); + } private: union { diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 1da14064..e629a05a 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -2186,4 +2186,118 @@ TEST_CASE("expected", "[expected][polyfill]") } } } + + SECTION("monadic functions") + { + SECTION("and_then") + { + SECTION("value") + { + using T = expected; + constexpr auto fn + = [](auto &&a) constexpr -> expected { return helper(std::forward(a)).v * 2; }; + + T a(7); + CHECK(a.and_then(fn).value() == 7 * 2 * helper::from_lval); + CHECK(std::as_const(a).and_then(fn).value() == 7 * 2 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).and_then(fn).value() == 7 * 2 * helper::from_rval_const); + CHECK(std::move(a).and_then(fn).value() == 7 * 2 * helper::from_rval); + } + + SECTION("error") + { + using T = expected; + constexpr auto fn = [](auto &&) constexpr -> expected { return {0}; }; + + T a(unexpect, 11); + CHECK(a.and_then(fn).error().v == 11 * helper::from_lval); + CHECK(std::as_const(a).and_then(fn).error().v == 11 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).and_then(fn).error().v == 11 * helper::from_rval_const); + CHECK(std::move(a).and_then(fn).error().v == 11 * helper::from_rval); + } + } + + SECTION("or_else") + { + SECTION("error") + { + using T = expected; + constexpr auto fn = [](auto &&a) constexpr -> expected { + return unexpected(helper(std::forward(a)).v * 3); + }; + + T a(unexpect, 5); + CHECK(a.or_else(fn).error() == 5 * 3 * helper::from_lval); + CHECK(std::as_const(a).or_else(fn).error() == 5 * 3 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).or_else(fn).error() == 5 * 3 * helper::from_rval_const); + CHECK(std::move(a).or_else(fn).error() == 5 * 3 * helper::from_rval); + } + + SECTION("value") + { + using T = expected; + constexpr auto fn = [](auto &&) constexpr -> expected { return {0}; }; + + T a(13); + CHECK(a.or_else(fn).value().v == 13 * helper::from_lval); + CHECK(std::as_const(a).or_else(fn).value().v == 13 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).or_else(fn).value().v == 13 * helper::from_rval_const); + CHECK(std::move(a).or_else(fn).value().v == 13 * helper::from_rval); + } + } + + SECTION("transform") + { + SECTION("value") + { + using T = expected; + constexpr auto fn = [](auto &&a) constexpr -> int { return helper(std::forward(a)).v * 2; }; + + T a(7); + CHECK(a.transform(fn).value() == 7 * 2 * helper::from_lval); + CHECK(std::as_const(a).transform(fn).value() == 7 * 2 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).transform(fn).value() == 7 * 2 * helper::from_rval_const); + CHECK(std::move(a).transform(fn).value() == 7 * 2 * helper::from_rval); + } + + SECTION("error") + { + using T = expected; + constexpr auto fn = [](auto &&) constexpr -> int { return 0; }; + + T a(unexpect, 11); + CHECK(a.transform(fn).error().v == 11 * helper::from_lval); + CHECK(std::as_const(a).transform(fn).error().v == 11 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).transform(fn).error().v == 11 * helper::from_rval_const); + CHECK(std::move(a).transform(fn).error().v == 11 * helper::from_rval); + } + } + + SECTION("transform_error") + { + SECTION("error") + { + using T = expected; + constexpr auto fn = [](auto &&a) constexpr -> int { return helper(std::forward(a)).v * 3; }; + + T a(unexpect, 5); + CHECK(a.transform_error(fn).error() == 5 * 3 * helper::from_lval); + CHECK(std::as_const(a).transform_error(fn).error() == 5 * 3 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).transform_error(fn).error() == 5 * 3 * helper::from_rval_const); + CHECK(std::move(a).transform_error(fn).error() == 5 * 3 * helper::from_rval); + } + + SECTION("value") + { + using T = expected; + constexpr auto fn = [](auto &&) constexpr -> int { return 0; }; + + T a(13); + CHECK(a.transform_error(fn).value().v == 13 * helper::from_lval); + CHECK(std::as_const(a).transform_error(fn).value().v == 13 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).transform_error(fn).value().v == 13 * helper::from_rval_const); + CHECK(std::move(a).transform_error(fn).value().v == 13 * helper::from_rval); + } + } + } } From b2539148fa4c2fc753a9d6de6c728fa56de5067c Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Fri, 13 Jun 2025 20:15:13 +0000 Subject: [PATCH 35/46] More unit tests --- include/pfn/expected.hpp | 1 + tests/pfn/expected.cpp | 327 +++++++++++++++++++++++++++++++++--- tests/util/helper_types.hpp | 1 - 3 files changed, 306 insertions(+), 23 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 9041a287..9a87db9c 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -736,6 +736,7 @@ template class expected { static_assert(::std::is_convertible_v); return set_ ? ::std::move(v_) : static_cast(FWD(v)); } + template constexpr E error_or(G &&e) const & // noexcept(::std::is_nothrow_copy_constructible_v && ::std::is_nothrow_convertible_v) // extension diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index e629a05a..8fb0c05e 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -394,9 +394,9 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") CHECK(b.error().v == 3 * helper::swapped * helper::swapped); } - SECTION("constexpr all, CTAD") + SECTION("constexpr") { - constexpr auto test = [](auto i) constexpr { + constexpr auto fn = [](auto i) constexpr { unexpected a{i}; unexpected b{i * 5}; swap(a, b); @@ -405,7 +405,8 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") b.swap(c); return unexpected{b.error() * a.error() * 7}; }; - constexpr auto c = test(21); + + constexpr auto c = fn(21); static_assert(std::is_same_v const>); static_assert(c.error() == 21 * 21 * 5 * 7); @@ -1236,6 +1237,47 @@ TEST_CASE("expected", "[expected][polyfill]") a = unexpected(7); CHECK(a.error().v == 7 * helper::from_rval); } + + SECTION("constexpr") + { + using T = expected; + + SECTION("from error") + { + constexpr auto fn = [](T &&v) constexpr -> T { + T tmp{unexpect, Error::unknown}; + tmp = std::move(v); + return tmp; + }; + + constexpr T a = fn(T(unexpect, Error::file_not_found)); + static_assert(a.error() == Error::file_not_found); + + constexpr T b = fn(T(std::in_place, std::initializer_list(), 7)); + static_assert(b.value().v == 7 * helper::from_rval * helper::from_rval); + + SUCCEED(); + } + + SECTION("from value") + { + { + constexpr auto fn = [](T &&v) constexpr -> T { + T tmp{std::in_place, std::initializer_list(), 13}; + tmp = std::move(v); + return tmp; + }; + + constexpr T a = fn(T(unexpect, Error::file_not_found)); + static_assert(a.error() == Error::file_not_found); + + constexpr T b = fn(T(std::in_place, std::initializer_list(), 11)); + static_assert(b.value().v == 11 * helper::from_rval * helper::from_rval); + + SUCCEED(); + } + } + } } SECTION("from lval const") @@ -1565,6 +1607,49 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(a.error().v == 7 * helper::from_lval_const); } } + + SECTION("constexpr") + { + using T = expected; + constexpr T c{unexpect, Error::file_not_found}; + constexpr T d{std::in_place, std::initializer_list(), 5}; + + SECTION("from error") + { + constexpr auto fn = [](T const &v) constexpr -> T { + T tmp{unexpect, Error::unknown}; + tmp = v; + return tmp; + }; + + constexpr T a = fn(c); + static_assert(a.error() == Error::file_not_found); + + constexpr T b = fn(d); + static_assert(b.value().v == 5 * helper::from_lval_const * helper::from_rval); + + SUCCEED(); + } + + SECTION("from value") + { + { + constexpr auto fn = [](T const &v) constexpr -> T { + T tmp{std::in_place, std::initializer_list(), 13}; + tmp = v; + return tmp; + }; + + constexpr T a = fn(c); + static_assert(a.error() == Error::file_not_found); + + constexpr T b = fn(d); + static_assert(b.value().v == 5 * helper::from_lval_const * helper::from_rval); + + SUCCEED(); + } + } + } } SECTION("emplace") @@ -1603,40 +1688,54 @@ TEST_CASE("expected", "[expected][polyfill]") SECTION("constexpr") { using T = expected; + constexpr helper c{std::initializer_list(), 5}; + + SECTION("from error") { - constexpr auto fn = [](auto &&...v) constexpr -> T { - T a(unexpect, Error::unknown); - a.emplace(std::forward(v)...); - return a; + constexpr auto fn = [](auto &&...args) constexpr -> T { + T tmp{unexpect, Error::unknown}; + tmp.emplace(std::forward(args)...); + return tmp; }; - constexpr auto a = fn(std::initializer_list{5.0, 11.0}, 3); - static_assert(a.value().v == 5 * 11 * 3 * helper::from_rval); + constexpr T a = fn(c); + static_assert(a.value().v == 5 * helper::from_lval_const * helper::from_rval); + + constexpr T b = fn(std::initializer_list{3.0, 11.0}, 7); + static_assert(b.value().v == 3 * 11 * 7 * helper::from_rval); + + SUCCEED(); } + SECTION("from value") { - constexpr auto fn = [](auto &&...v) constexpr -> T { - T a(std::in_place, {9.0}, 1); - a.emplace(std::forward(v)...); - return a; - }; + { + constexpr auto fn = [](auto &&...args) constexpr -> T { + T tmp{std::in_place, std::initializer_list(), 13}; + tmp.emplace(std::forward(args)...); + return tmp; + }; + + constexpr T a = fn(c); + static_assert(a.value().v == 5 * helper::from_lval_const * helper::from_rval); - constexpr auto a = fn(std::initializer_list{7.0, 13.0}, 3); - static_assert(a.value().v == 7 * 13 * 3 * helper::from_rval); + constexpr T b = fn(std::initializer_list{3.0, 11.0}, 7); + static_assert(b.value().v == 3 * 11 * 7 * helper::from_rval); + + SUCCEED(); + } } + SECTION("throwing constructor") { constexpr auto fn = [](auto &&...args) constexpr -> bool { - return requires { - expected(unexpect, Error::unknown).emplace(std::forward(args)...); - }; + return requires { std::declval().emplace(std::forward(args)...); }; }; - static_assert(fn(std::initializer_list{3.0, 5.0}, 1)); static_assert(not fn(1, 2)); - } - SUCCEED(); + SUCCEED(); + } } } @@ -2117,6 +2216,37 @@ TEST_CASE("expected", "[expected][polyfill]") SUCCEED(); } + + SECTION("constexpr") + { + using T = expected; + constexpr helper c{std::initializer_list(), 7}; + + SECTION("lval const") + { + { + constexpr T a(std::in_place, {3.0}, 5); + static_assert(a.value_or(c).v == 3 * 5 * helper::from_lval_const); + } + + { + constexpr T a(unexpect, Error::unknown); + static_assert(a.value_or(c).v == 7 * helper::from_lval_const); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{std::in_place, {3.0}, 5}.value_or(c).v == 3 * 5 * helper::from_rval); + static_assert(T{unexpect, Error::unknown}.value_or(c).v == 7 * helper::from_lval_const); + static_assert(T{unexpect, Error::unknown}.value_or(helper(std::initializer_list{7.0}, 3)).v + == 7 * 3 * helper::from_rval); + + SUCCEED(); + } + } } SECTION("error_or") @@ -2184,6 +2314,37 @@ TEST_CASE("expected", "[expected][polyfill]") SUCCEED(); } + + SECTION("constexpr") + { + using T = expected; + constexpr helper c{std::initializer_list(), 7}; + + SECTION("lval const") + { + { + constexpr T a(unexpect, {3.0}, 5); + static_assert(a.error_or(c).v == 3 * 5 * helper::from_lval_const); + } + + { + constexpr T a(std::in_place, 13); + static_assert(a.error_or(c).v == 7 * helper::from_lval_const); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{unexpect, {3.0}, 5}.error_or(c).v == 3 * 5 * helper::from_rval); + static_assert(T{std::in_place, 13}.error_or(c).v == 7 * helper::from_lval_const); + static_assert(T{std::in_place, 13}.error_or(helper(std::initializer_list{7.0}, 3)).v + == 7 * 3 * helper::from_rval); + + SUCCEED(); + } + } } } @@ -2215,6 +2376,37 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(std::move(std::as_const(a)).and_then(fn).error().v == 11 * helper::from_rval_const); CHECK(std::move(a).and_then(fn).error().v == 11 * helper::from_rval); } + + SECTION("constexpr") + { + using T = expected; + constexpr helper c{std::initializer_list(), 7}; + constexpr auto fn + = [](auto &&a) constexpr -> expected { return helper(std::forward(a)).v * 3; }; + + SECTION("lval const") + { + { + constexpr T a(std::in_place, {3.0}, 5); + static_assert(a.and_then(fn).value() == 3 * 3 * 5 * helper::from_lval_const); + } + + { + constexpr T a(unexpect, Error::file_not_found); + static_assert(a.and_then(fn).error() == Error::file_not_found); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{std::in_place, {3.0}, 5}.and_then(fn) == 3 * 3 * 5 * helper::from_rval); + static_assert(T{unexpect, Error::file_not_found}.and_then(fn).error() == Error::file_not_found); + + SUCCEED(); + } + } } SECTION("or_else") @@ -2244,6 +2436,37 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(std::move(std::as_const(a)).or_else(fn).value().v == 13 * helper::from_rval_const); CHECK(std::move(a).or_else(fn).value().v == 13 * helper::from_rval); } + + SECTION("constexpr") + { + using T = expected; + constexpr helper c{std::initializer_list(), 7}; + constexpr auto fn + = [](auto &&a) constexpr -> expected { return helper(std::forward(a)).v * 3; }; + + SECTION("lval const") + { + { + constexpr T a(std::in_place, 5); + static_assert(a.or_else(fn).value() == 5); + } + + { + constexpr T a(unexpect, {3.0}, 5); + static_assert(a.or_else(fn).value() == 3 * 3 * 5 * helper::from_lval_const); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{unexpect, {3.0}, 5}.or_else(fn).value() == 3 * 3 * 5 * helper::from_rval); + static_assert(T{std::in_place, 13}.or_else(fn).value() == 13); + + SUCCEED(); + } + } } SECTION("transform") @@ -2271,6 +2494,36 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(std::move(std::as_const(a)).transform(fn).error().v == 11 * helper::from_rval_const); CHECK(std::move(a).transform(fn).error().v == 11 * helper::from_rval); } + + SECTION("constexpr") + { + using T = expected; + constexpr helper c{std::initializer_list(), 7}; + constexpr auto fn = [](auto &&a) constexpr -> int { return helper(std::forward(a)).v * 3; }; + + SECTION("lval const") + { + { + constexpr T a(std::in_place, {3.0}, 5); + static_assert(a.transform(fn).value() == 3 * 3 * 5 * helper::from_lval_const); + } + + { + constexpr T a(unexpect, Error::file_not_found); + static_assert(a.transform(fn).error() == Error::file_not_found); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{std::in_place, {3.0}, 5}.transform(fn) == 3 * 3 * 5 * helper::from_rval); + static_assert(T{unexpect, Error::file_not_found}.transform(fn).error() == Error::file_not_found); + + SUCCEED(); + } + } } SECTION("transform_error") @@ -2298,6 +2551,36 @@ TEST_CASE("expected", "[expected][polyfill]") CHECK(std::move(std::as_const(a)).transform_error(fn).value().v == 13 * helper::from_rval_const); CHECK(std::move(a).transform_error(fn).value().v == 13 * helper::from_rval); } + + SECTION("constexpr") + { + using T = expected; + constexpr helper c{std::initializer_list(), 7}; + constexpr auto fn = [](auto &&a) constexpr -> int { return helper(std::forward(a)).v * 3; }; + + SECTION("lval const") + { + { + constexpr T a(std::in_place, 5); + static_assert(a.transform_error(fn).value() == 5); + } + + { + constexpr T a(unexpect, {3.0}, 5); + static_assert(a.transform_error(fn).error() == 3 * 3 * 5 * helper::from_lval_const); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{unexpect, {3.0}, 5}.transform_error(fn).error() == 3 * 3 * 5 * helper::from_rval); + static_assert(T{std::in_place, 13}.transform_error(fn).value() == 13); + + SUCCEED(); + } + } } } } diff --git a/tests/util/helper_types.hpp b/tests/util/helper_types.hpp index 244482a4..6667d152 100644 --- a/tests/util/helper_types.hpp +++ b/tests/util/helper_types.hpp @@ -5,7 +5,6 @@ #include #include -#include #include #include #include From 16660ae36f395b57902a2b361d67e32f76dfc1f9 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 5 Jul 2025 17:32:37 +0100 Subject: [PATCH 36/46] Add unit tests for equality --- include/pfn/expected.hpp | 11 +- tests/pfn/expected.cpp | 240 ++++++++++++++++++++++++++++-------- tests/util/helper_types.hpp | 8 +- 3 files changed, 196 insertions(+), 63 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 9a87db9c..173f714b 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -882,10 +882,6 @@ template class expected { requires(not ::std::is_void_v) constexpr friend bool operator==(expected const &x, expected const &y) // noexcept(noexcept(*x == *y) && noexcept(x.error() == y.error())) // extension - requires requires { - { *x == *y } -> ::std::convertible_to; - { x.error() == y.error() } -> ::std::convertible_to; - } { if (x.has_value() != y.has_value()) { return false; @@ -898,18 +894,13 @@ template class expected { template constexpr friend bool operator==(expected const &x, const T2 &v) // noexcept(noexcept(*x == v)) // extension - requires(not detail::_is_some_expected) && requires { - { *x == v } -> ::std::convertible_to; - } + requires(not detail::_is_some_expected) { return x.has_value() && static_cast(*x == v); } template constexpr friend bool operator==(expected const &x, unexpected const &e) // noexcept(noexcept(x.error() == e.error())) // extension - requires requires { - { x.error() == e.error() } -> ::std::convertible_to; - } { return (not x.has_value()) && static_cast(x.error() == e.error()); } diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 8fb0c05e..c62465b2 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -289,14 +289,14 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") unexpected c(std::in_place, {3.0, 5.0}); auto const d = 3 * 5; CHECK(c.error().v == d); - static_assert(std::is_nothrow_constructible_v>); + static_assert(std::is_nothrow_constructible_v); } SECTION("no forwarded args") { unexpected c(std::in_place, {2.0, 2.5}); CHECK(c.error().v == 5); - static_assert(std::is_nothrow_constructible_v>); + static_assert(std::is_nothrow_constructible_v); } SECTION("exception thrown") @@ -545,10 +545,9 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v>); - static_assert(std::is_constructible_v, Error>>); + static_assert(std::is_constructible_v>); static_assert(not extension || not std::is_nothrow_constructible_v>); - static_assert(not extension - || std::is_nothrow_constructible_v, Error>>); + static_assert(not extension || std::is_nothrow_constructible_v>); constexpr T b(expected(unexpect, Error::unknown)); static_assert(b.error() == Error::unknown); @@ -571,8 +570,7 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_constructible_v const &>); static_assert(not extension || not std::is_nothrow_constructible_v const &>); - static_assert(not extension - || std::is_nothrow_constructible_v, Error> const &>); + static_assert(not extension || std::is_nothrow_constructible_v const &>); constexpr expected v(5); constexpr expected e(unexpect, Error::file_not_found); @@ -602,8 +600,8 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not extension || not std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); static_assert(not extension || std::is_nothrow_constructible_v); - static_assert(std::is_constructible_v>); - static_assert(not extension || std::is_nothrow_constructible_v>); + static_assert(std::is_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); T const b(11); CHECK(b.value().v == 11); @@ -617,8 +615,8 @@ TEST_CASE("expected", "[expected][polyfill]") using T = expected; static_assert(std::is_constructible_v>); static_assert(not extension || not std::is_nothrow_constructible_v>); - static_assert(std::is_constructible_v>>); - static_assert(not extension || std::is_nothrow_constructible_v>>); + static_assert(std::is_constructible_v>); + static_assert(not extension || std::is_nothrow_constructible_v>); constexpr expected a(unexpected(true)); static_assert(a.error() == 1); @@ -645,9 +643,8 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not extension || not std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); static_assert(not extension || std::is_nothrow_constructible_v); - static_assert(std::is_constructible_v>); - static_assert(not extension - || std::is_nothrow_constructible_v>); + static_assert(std::is_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); T const b(std::in_place, 11, 13); CHECK(b.value().v == 11 * 13); @@ -670,8 +667,8 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not extension || not std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); static_assert(not extension || std::is_nothrow_constructible_v); - static_assert(std::is_constructible_v>); - static_assert(not extension || std::is_nothrow_constructible_v>); + static_assert(std::is_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); T const b(unexpect, 11, 13); CHECK(b.error().v == 11 * 13); @@ -1253,7 +1250,7 @@ TEST_CASE("expected", "[expected][polyfill]") constexpr T a = fn(T(unexpect, Error::file_not_found)); static_assert(a.error() == Error::file_not_found); - constexpr T b = fn(T(std::in_place, std::initializer_list(), 7)); + constexpr T b = fn(T(std::in_place, helper::list_t(), 7)); static_assert(b.value().v == 7 * helper::from_rval * helper::from_rval); SUCCEED(); @@ -1263,7 +1260,7 @@ TEST_CASE("expected", "[expected][polyfill]") { { constexpr auto fn = [](T &&v) constexpr -> T { - T tmp{std::in_place, std::initializer_list(), 13}; + T tmp{std::in_place, helper::list_t(), 13}; tmp = std::move(v); return tmp; }; @@ -1271,7 +1268,7 @@ TEST_CASE("expected", "[expected][polyfill]") constexpr T a = fn(T(unexpect, Error::file_not_found)); static_assert(a.error() == Error::file_not_found); - constexpr T b = fn(T(std::in_place, std::initializer_list(), 11)); + constexpr T b = fn(T(std::in_place, helper::list_t(), 11)); static_assert(b.value().v == 11 * helper::from_rval * helper::from_rval); SUCCEED(); @@ -1612,7 +1609,7 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; constexpr T c{unexpect, Error::file_not_found}; - constexpr T d{std::in_place, std::initializer_list(), 5}; + constexpr T d{std::in_place, helper::list_t(), 5}; SECTION("from error") { @@ -1635,7 +1632,7 @@ TEST_CASE("expected", "[expected][polyfill]") { { constexpr auto fn = [](T const &v) constexpr -> T { - T tmp{std::in_place, std::initializer_list(), 13}; + T tmp{std::in_place, helper::list_t(), 13}; tmp = v; return tmp; }; @@ -1688,7 +1685,7 @@ TEST_CASE("expected", "[expected][polyfill]") SECTION("constexpr") { using T = expected; - constexpr helper c{std::initializer_list(), 5}; + constexpr helper c{helper::list_t(), 5}; SECTION("from error") { @@ -1701,7 +1698,7 @@ TEST_CASE("expected", "[expected][polyfill]") constexpr T a = fn(c); static_assert(a.value().v == 5 * helper::from_lval_const * helper::from_rval); - constexpr T b = fn(std::initializer_list{3.0, 11.0}, 7); + constexpr T b = fn(helper::list_t{3.0, 11.0}, 7); static_assert(b.value().v == 3 * 11 * 7 * helper::from_rval); SUCCEED(); @@ -1711,7 +1708,7 @@ TEST_CASE("expected", "[expected][polyfill]") { { constexpr auto fn = [](auto &&...args) constexpr -> T { - T tmp{std::in_place, std::initializer_list(), 13}; + T tmp{std::in_place, helper::list_t(), 13}; tmp.emplace(std::forward(args)...); return tmp; }; @@ -1719,7 +1716,7 @@ TEST_CASE("expected", "[expected][polyfill]") constexpr T a = fn(c); static_assert(a.value().v == 5 * helper::from_lval_const * helper::from_rval); - constexpr T b = fn(std::initializer_list{3.0, 11.0}, 7); + constexpr T b = fn(helper::list_t{3.0, 11.0}, 7); static_assert(b.value().v == 3 * 11 * 7 * helper::from_rval); SUCCEED(); @@ -2155,9 +2152,9 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(noexcept(std::declval().value_or(std::declval>()))); + static_assert(noexcept(std::declval().value_or(std::declval()))); static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(noexcept(std::declval().value_or(std::declval>()))); + static_assert(noexcept(std::declval().value_or(std::declval()))); SECTION("value") { @@ -2193,12 +2190,12 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not std::is_nothrow_copy_constructible_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(not std::is_nothrow_convertible_v); - static_assert(std::is_nothrow_convertible_v, T::value_type>); + static_assert(std::is_nothrow_convertible_v); static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(noexcept(std::declval().value_or(std::declval>()))); + static_assert(noexcept(std::declval().value_or(std::declval()))); static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(not noexcept(std::declval().value_or(std::declval>()))); + static_assert(not noexcept(std::declval().value_or(std::declval()))); } { @@ -2206,12 +2203,12 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(std::is_nothrow_copy_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); static_assert(not std::is_nothrow_convertible_v); - static_assert(std::is_nothrow_convertible_v, T::value_type>); + static_assert(std::is_nothrow_convertible_v); static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(not noexcept(std::declval().value_or(std::declval>()))); + static_assert(not noexcept(std::declval().value_or(std::declval()))); static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(noexcept(std::declval().value_or(std::declval>()))); + static_assert(noexcept(std::declval().value_or(std::declval()))); } SUCCEED(); @@ -2220,7 +2217,7 @@ TEST_CASE("expected", "[expected][polyfill]") SECTION("constexpr") { using T = expected; - constexpr helper c{std::initializer_list(), 7}; + constexpr helper c{helper::list_t(), 7}; SECTION("lval const") { @@ -2241,7 +2238,7 @@ TEST_CASE("expected", "[expected][polyfill]") { static_assert(T{std::in_place, {3.0}, 5}.value_or(c).v == 3 * 5 * helper::from_rval); static_assert(T{unexpect, Error::unknown}.value_or(c).v == 7 * helper::from_lval_const); - static_assert(T{unexpect, Error::unknown}.value_or(helper(std::initializer_list{7.0}, 3)).v + static_assert(T{unexpect, Error::unknown}.value_or(helper(helper::list_t{7.0}, 3)).v == 7 * 3 * helper::from_rval); SUCCEED(); @@ -2253,9 +2250,9 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(noexcept(std::declval().error_or(std::declval>()))); + static_assert(noexcept(std::declval().error_or(std::declval()))); static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(noexcept(std::declval().error_or(std::declval>()))); + static_assert(noexcept(std::declval().error_or(std::declval()))); SECTION("error") { @@ -2291,12 +2288,12 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not std::is_nothrow_copy_constructible_v); static_assert(std::is_nothrow_move_constructible_v); static_assert(not std::is_nothrow_convertible_v); - static_assert(std::is_nothrow_convertible_v, T::error_type>); + static_assert(std::is_nothrow_convertible_v); static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(noexcept(std::declval().error_or(std::declval>()))); + static_assert(noexcept(std::declval().error_or(std::declval()))); static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(not noexcept(std::declval().error_or(std::declval>()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); } { @@ -2304,12 +2301,12 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(std::is_nothrow_copy_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); static_assert(not std::is_nothrow_convertible_v); - static_assert(std::is_nothrow_convertible_v, T::error_type>); + static_assert(std::is_nothrow_convertible_v); static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(not noexcept(std::declval().error_or(std::declval>()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(noexcept(std::declval().error_or(std::declval>()))); + static_assert(noexcept(std::declval().error_or(std::declval()))); } SUCCEED(); @@ -2318,7 +2315,7 @@ TEST_CASE("expected", "[expected][polyfill]") SECTION("constexpr") { using T = expected; - constexpr helper c{std::initializer_list(), 7}; + constexpr helper c{helper::list_t(), 7}; SECTION("lval const") { @@ -2339,8 +2336,7 @@ TEST_CASE("expected", "[expected][polyfill]") { static_assert(T{unexpect, {3.0}, 5}.error_or(c).v == 3 * 5 * helper::from_rval); static_assert(T{std::in_place, 13}.error_or(c).v == 7 * helper::from_lval_const); - static_assert(T{std::in_place, 13}.error_or(helper(std::initializer_list{7.0}, 3)).v - == 7 * 3 * helper::from_rval); + static_assert(T{std::in_place, 13}.error_or(helper(helper::list_t{7.0}, 3)).v == 7 * 3 * helper::from_rval); SUCCEED(); } @@ -2380,7 +2376,7 @@ TEST_CASE("expected", "[expected][polyfill]") SECTION("constexpr") { using T = expected; - constexpr helper c{std::initializer_list(), 7}; + constexpr helper c{helper::list_t(), 7}; constexpr auto fn = [](auto &&a) constexpr -> expected { return helper(std::forward(a)).v * 3; }; @@ -2440,7 +2436,7 @@ TEST_CASE("expected", "[expected][polyfill]") SECTION("constexpr") { using T = expected; - constexpr helper c{std::initializer_list(), 7}; + constexpr helper c{helper::list_t(), 7}; constexpr auto fn = [](auto &&a) constexpr -> expected { return helper(std::forward(a)).v * 3; }; @@ -2498,7 +2494,7 @@ TEST_CASE("expected", "[expected][polyfill]") SECTION("constexpr") { using T = expected; - constexpr helper c{std::initializer_list(), 7}; + constexpr helper c{helper::list_t(), 7}; constexpr auto fn = [](auto &&a) constexpr -> int { return helper(std::forward(a)).v * 3; }; SECTION("lval const") @@ -2555,7 +2551,7 @@ TEST_CASE("expected", "[expected][polyfill]") SECTION("constexpr") { using T = expected; - constexpr helper c{std::initializer_list(), 7}; + constexpr helper c{helper::list_t(), 7}; constexpr auto fn = [](auto &&a) constexpr -> int { return helper(std::forward(a)).v * 3; }; SECTION("lval const") @@ -2583,4 +2579,148 @@ TEST_CASE("expected", "[expected][polyfill]") } } } + + SECTION("equality operators") + { + SECTION("operand expected") + { + using T = expected; + using U = expected; + + SECTION("value and error") + { + T const t1{std::in_place, {12.0}}; + U const u1{unexpect, false}; + CHECK(not(t1 == u1)); + CHECK(t1 != u1); + + constexpr T t2{unexpect, 12}; + constexpr U u2{std::in_place, helper::list_t(), 13}; + static_assert(not(t2 == u2)); + static_assert(t2 != u2); + } + + SECTION("value") + { + SECTION("same type") + { + T const t1{std::in_place, {12.0}}; + U const u1{std::in_place, {3.0}}; + CHECK(not(t1 == u1)); + CHECK(t1 != u1); + + constexpr T t2{std::in_place, {3.0}, 4}; + constexpr U u2{std::in_place, {3.0}, 2, 2}; + static_assert(t2 == u2); + static_assert(not(t2 != u2)); + CHECK(t1 == t2); + } + + SECTION("different types") + { + using V = expected; + using W = expected; + static_assert(V{12} == W{12.0}); + static_assert(V{15} != W{12.0}); + + SUCCEED(); + } + } + + SECTION("error") + { + SECTION("same type") + { + using V = expected; + using W = expected; + + V const v1{unexpect, {12.0}}; + W const w1{unexpect, {3.0}}; + CHECK(not(v1 == w1)); + CHECK(v1 != w1); + + constexpr V v2{unexpect, {3.0}, 4}; + constexpr W w2{unexpect, {3.0}, 2, 2}; + static_assert(v2 == w2); + static_assert(not(v2 != w2)); + CHECK(v1 == v2); + } + + SECTION("different types") + { + using V = expected; + using W = expected; + static_assert(V{unexpect, 12} == W{unexpect, 12.0}); + static_assert(V{unexpect, 15} != W{unexpect, 12.0}); + + SUCCEED(); + } + } + } + + SECTION("operand value") + { + SECTION("same type") + { + using T = expected; + T const t1{std::in_place, {12.0}}; + helper const u1{3.0}; + helper const v1{3, 4}; + CHECK(not(t1 == u1)); + CHECK(t1 != u1); + CHECK(t1 == v1); + + constexpr T t2{std::in_place, {3.0}, 4}; + constexpr helper u2{helper::list_t(), 3, 2, 2}; + static_assert(t2 == u2); + static_assert(not(t2 != u2)); + CHECK(t1 == t2); + } + + SECTION("different types") + { + using T = expected; + constexpr T t1{std::in_place, 12}; + constexpr double u1 = 12.0; + static_assert(t1 == u1); + static_assert(T{15.0} != u1); + + SUCCEED(); + }; + + SUCCEED(); + } + + SECTION("operand unexpected") + { + SECTION("same type") + { + using T = expected; + using U = unexpected; + T const t1{unexpect, {12.0}}; + U const u1{std::in_place, {3.0}}; + U const v1{std::in_place, {3.0, 4.0}}; + CHECK(not(t1 == u1)); + CHECK(t1 != u1); + CHECK(t1 == v1); + + constexpr T t2{unexpect, {3.0}, 4}; + constexpr U u2{std::in_place, helper::list_t(), 3, 2, 2}; + static_assert(t2 == u2); + static_assert(not(t2 != u2)); + CHECK(t1 == t2); + } + + SECTION("different types") + { + using T = expected; + using U = unexpected; + constexpr T t1{unexpect, 1}; + static_assert(t1 == U{Error::unknown}); + static_assert(t1 != U{Error::file_not_found}); + + SUCCEED(); + } + } + } } diff --git a/tests/util/helper_types.hpp b/tests/util/helper_types.hpp index 6667d152..2ff60899 100644 --- a/tests/util/helper_types.hpp +++ b/tests/util/helper_types.hpp @@ -106,17 +106,19 @@ template struct helper_t { state += v; } - helper_t(std::initializer_list list) noexcept(true) : v(init(list)) { state += v; } + using list_t = std::initializer_list; + + helper_t(list_t list) noexcept(true) : v(init(list)) { state += v; } // Potentially throwing constructor - constexpr helper_t(std::initializer_list list, std::integral auto... a) noexcept(true) + constexpr helper_t(list_t list, std::integral auto... a) noexcept(true) requires(sizeof...(a) > 0) : v(init(list, a...)) // { } // ... and the actual exception being thrown - static constexpr int init(std::initializer_list l, auto &&...a) noexcept + static constexpr int init(list_t l, auto &&...a) noexcept { double ret = (1 * ... * a); for (auto d : l) { From da0c3f4e480fd0b880063a93c53f0202b709d6de Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 20 Jul 2025 12:55:15 +0100 Subject: [PATCH 37/46] Validate expected unit tests against std::expected in C++23 mode --- include/pfn/expected.hpp | 16 +++++- tests/pfn/expected.cpp | 102 +++++++++++++++++++++++---------------- 2 files changed, 75 insertions(+), 43 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 173f714b..88095aee 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -7,6 +7,7 @@ #define INCLUDE_PFN_EXPECTED #include +#include #include #include #include @@ -882,6 +883,13 @@ template class expected { requires(not ::std::is_void_v) constexpr friend bool operator==(expected const &x, expected const &y) // noexcept(noexcept(*x == *y) && noexcept(x.error() == y.error())) // extension + requires ( // +requires { + { *x == *y } -> std::convertible_to; + } && +requires { + { x.error() == y.error() } -> std::convertible_to; + }) { if (x.has_value() != y.has_value()) { return false; @@ -892,15 +900,21 @@ template class expected { } } template + requires(not detail::_is_some_expected) constexpr friend bool operator==(expected const &x, const T2 &v) // noexcept(noexcept(*x == v)) // extension - requires(not detail::_is_some_expected) + requires requires { + { *x == v } -> std::convertible_to; + } { return x.has_value() && static_cast(*x == v); } template constexpr friend bool operator==(expected const &x, unexpected const &e) // noexcept(noexcept(x.error() == e.error())) // extension + requires requires { + { x.error() == e.error() } -> std::convertible_to; + } { return (not x.has_value()) && static_cast(x.error() == e.error()); } diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index c62465b2..cc7ce61f 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -6,7 +6,23 @@ // TODO: Add death tests. Until then, empty definition to avoid false "no coverage" reports #define LIBFN_ASSERT(...) +#if LIBFN_MODE < 23 #include +using pfn::bad_expected_access; +using pfn::expected; +using pfn::unexpect; +using pfn::unexpect_t; +using pfn::unexpected; +constexpr char unexpected_what[] = "bad access to expected without expected value"; +#else +#include +using std::bad_expected_access; +using std::expected; +using std::unexpect; +using std::unexpect_t; +using std::unexpected; +constexpr char unexpected_what[] = "bad access to std::expected without expected value"; +#endif #include @@ -23,7 +39,7 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") { SECTION("bad_expected_access") { - struct T : pfn::bad_expected_access {}; + struct T : bad_expected_access {}; static_assert(noexcept(T{})); T a; @@ -51,7 +67,7 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") a = [&]() -> T const && { return std::move(a); }(); CHECK(T{}.what() == a.what()); } - CHECK(std::strcmp(a.what(), "bad access to expected without expected value") == 0); + CHECK(std::strcmp(a.what(), unexpected_what) == 0); T const b; CHECK(&decltype(a)::what == &decltype(b)::what); @@ -65,8 +81,8 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") SECTION("bad_expected_access") { - using T = pfn::bad_expected_access; - static_assert(std::is_base_of_v, T>); + using T = bad_expected_access; + static_assert(std::is_base_of_v, T>); SECTION("type and noexcept") { @@ -183,9 +199,9 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") SECTION("bad_expected_access") { T a{12}; - CHECK(std::strcmp(a.what(), "bad access to expected without expected value") == 0); + CHECK(std::strcmp(a.what(), unexpected_what) == 0); auto const c = []() { - struct C : pfn::bad_expected_access {}; + struct C : bad_expected_access {}; return C{}; }(); CHECK(a.what() == c.what()); @@ -199,24 +215,23 @@ template struct dummy final { TEST_CASE("unexpect", "[expected][polyfill][unexpect]") { - static_assert(std::is_empty_v); - static_assert(noexcept(pfn::unexpect_t{})); - static_assert(std::is_same_v); + static_assert(std::is_empty_v); + static_assert(noexcept(unexpect_t{})); + static_assert(std::is_same_v); - // pfn::unexpect can be used as a NTTP - static_assert(not std::is_empty_v>); - static constexpr auto a = pfn::unexpect; + // unexpect can be used as a NTTP + static_assert(not std::is_empty_v>); + static constexpr auto a = unexpect; static_assert(not std::is_empty_v>); - static_assert(std::is_same_v); - static_assert(std::is_same_v, dummy>); + static_assert(std::is_same_v); + static_assert(std::is_same_v, dummy>); SUCCEED(); } TEST_CASE("unexpected", "[expected][polyfill][unexpected]") { - using pfn::unexpected; - +#if LIBFN_MODE < 23 SECTION("is_valid_unexpected") { using pfn::detail::_is_valid_unexpected; @@ -237,6 +252,7 @@ TEST_CASE("unexpected", "[expected][polyfill][unexpected]") static_assert(_is_valid_unexpected>); SUCCEED(); } +#endif SECTION("constructors") { @@ -438,12 +454,11 @@ concept is_swappable = requires { swap(std::declval(), std::declval()) TEST_CASE("expected", "[expected][polyfill]") { - using pfn::bad_expected_access; - using pfn::expected; - using pfn::unexpect; - using pfn::unexpect_t; - using pfn::unexpected; +#if LIBFN_MODE < 23 constexpr bool extension = true; +#else + constexpr bool extension = false; +#endif SECTION("constructors") { @@ -901,7 +916,7 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not std::is_trivially_move_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); - static_assert(not std::is_nothrow_destructible_v); + static_assert(not extension || not std::is_nothrow_destructible_v); constexpr T a(std::in_place); constexpr T b = a; @@ -924,7 +939,7 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not std::is_trivially_move_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); - static_assert(not std::is_nothrow_destructible_v); + static_assert(not extension || not std::is_nothrow_destructible_v); constexpr T a(unexpect); constexpr T b = a; @@ -1745,8 +1760,8 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(std::is_move_constructible_v); static_assert(std::is_nothrow_move_constructible_v); - static_assert(not is_swappable>); - static_assert(not is_swappable>); + static_assert(not extension || not is_swappable>); + static_assert(not extension || not is_swappable>); SUCCEED(); } @@ -2249,10 +2264,13 @@ TEST_CASE("expected", "[expected][polyfill]") SECTION("error_or") { using T = expected; - static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(noexcept(std::declval().error_or(std::declval()))); - static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(noexcept(std::declval().error_or(std::declval()))); + SECTION("noexcept extension") + { + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().error_or(std::declval()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().error_or(std::declval()))); + } SECTION("error") { @@ -2291,7 +2309,7 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(std::is_nothrow_convertible_v); static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(noexcept(std::declval().error_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().error_or(std::declval()))); static_assert(not noexcept(std::declval().error_or(std::declval()))); static_assert(not noexcept(std::declval().error_or(std::declval()))); } @@ -2306,7 +2324,7 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not noexcept(std::declval().error_or(std::declval()))); static_assert(not noexcept(std::declval().error_or(std::declval()))); static_assert(not noexcept(std::declval().error_or(std::declval()))); - static_assert(noexcept(std::declval().error_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().error_or(std::declval()))); } SUCCEED(); @@ -2592,7 +2610,7 @@ TEST_CASE("expected", "[expected][polyfill]") T const t1{std::in_place, {12.0}}; U const u1{unexpect, false}; CHECK(not(t1 == u1)); - CHECK(t1 != u1); + CHECK((t1 != u1)); constexpr T t2{unexpect, 12}; constexpr U u2{std::in_place, helper::list_t(), 13}; @@ -2607,13 +2625,13 @@ TEST_CASE("expected", "[expected][polyfill]") T const t1{std::in_place, {12.0}}; U const u1{std::in_place, {3.0}}; CHECK(not(t1 == u1)); - CHECK(t1 != u1); + CHECK((t1 != u1)); constexpr T t2{std::in_place, {3.0}, 4}; constexpr U u2{std::in_place, {3.0}, 2, 2}; static_assert(t2 == u2); static_assert(not(t2 != u2)); - CHECK(t1 == t2); + CHECK((t1 == t2)); } SECTION("different types") @@ -2637,13 +2655,13 @@ TEST_CASE("expected", "[expected][polyfill]") V const v1{unexpect, {12.0}}; W const w1{unexpect, {3.0}}; CHECK(not(v1 == w1)); - CHECK(v1 != w1); + CHECK((v1 != w1)); constexpr V v2{unexpect, {3.0}, 4}; constexpr W w2{unexpect, {3.0}, 2, 2}; static_assert(v2 == w2); static_assert(not(v2 != w2)); - CHECK(v1 == v2); + CHECK((v1 == v2)); } SECTION("different types") @@ -2667,14 +2685,14 @@ TEST_CASE("expected", "[expected][polyfill]") helper const u1{3.0}; helper const v1{3, 4}; CHECK(not(t1 == u1)); - CHECK(t1 != u1); - CHECK(t1 == v1); + CHECK((t1 != u1)); + CHECK((t1 == v1)); constexpr T t2{std::in_place, {3.0}, 4}; constexpr helper u2{helper::list_t(), 3, 2, 2}; static_assert(t2 == u2); static_assert(not(t2 != u2)); - CHECK(t1 == t2); + CHECK((t1 == t2)); } SECTION("different types") @@ -2701,14 +2719,14 @@ TEST_CASE("expected", "[expected][polyfill]") U const u1{std::in_place, {3.0}}; U const v1{std::in_place, {3.0, 4.0}}; CHECK(not(t1 == u1)); - CHECK(t1 != u1); - CHECK(t1 == v1); + CHECK((t1 == v1)); + CHECK((t1 != u1)); constexpr T t2{unexpect, {3.0}, 4}; constexpr U u2{std::in_place, helper::list_t(), 3, 2, 2}; static_assert(t2 == u2); static_assert(not(t2 != u2)); - CHECK(t1 == t2); + CHECK((t1 == t2)); } SECTION("different types") From ee5158d7d7e72150418ffa63f4ad8a989859ca37 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 6 Jul 2025 12:12:22 +0100 Subject: [PATCH 38/46] Add expected with complete unit tests --- include/pfn/expected.hpp | 566 +++++++++++++-- tests/pfn/expected.cpp | 1465 +++++++++++++++++++++++++++++++++++++- 2 files changed, 1967 insertions(+), 64 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 88095aee..1e6a1140 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -324,7 +324,7 @@ template class expected { return result_t(::std::in_place, ::std::invoke(FWD(fn), FWD(self).value())); } else { ::std::invoke(FWD(fn), FWD(self).value()); - return result_t(); + return result_t(::std::in_place); } } return result_t(unexpect, FWD(self).error()); @@ -601,7 +601,7 @@ template class expected { } template - constexpr T &emplace(Args &&...args) noexcept(true) + constexpr T &emplace(Args &&...args) noexcept requires(::std::is_nothrow_constructible_v) { if (set_) { @@ -614,7 +614,7 @@ template class expected { } template - constexpr T &emplace(::std::initializer_list il, Args &&...args) noexcept(true) + constexpr T &emplace(::std::initializer_list il, Args &&...args) noexcept requires(::std::is_nothrow_constructible_v &, Args...>) { if (set_) { @@ -678,26 +678,32 @@ template class expected { constexpr bool has_value() const noexcept { return set_; } constexpr T const &value() const & { + static_assert(::std::is_copy_constructible_v); if (not set_) - throw bad_expected_access(::std::as_const(e_)); + throw bad_expected_access(e_); return v_; } constexpr T &value() & { + static_assert(::std::is_copy_constructible_v); if (not set_) throw bad_expected_access(::std::as_const(e_)); return v_; } constexpr T const &&value() const && { + static_assert(::std::is_copy_constructible_v); + static_assert(::std::is_constructible_v); if (not set_) - throw bad_expected_access(::std::as_const(e_)); + throw bad_expected_access(::std::move(e_)); return ::std::move(v_); } constexpr T &&value() && { + static_assert(::std::is_copy_constructible_v); + static_assert(::std::is_constructible_v); if (not set_) - throw bad_expected_access(::std::as_const(e_)); + throw bad_expected_access(::std::move(e_)); return ::std::move(v_); } constexpr E const &error() const & noexcept @@ -930,6 +936,97 @@ requires { template requires ::std::is_void_v class expected { + static_assert(detail::_is_valid_unexpected); + + template + using _can_convert_detail = ::std::bool_constant< // + ::std::is_void_v && ::std::is_constructible_v // + && not ::std::is_constructible_v, expected &> // + && not ::std::is_constructible_v, expected> // + && not ::std::is_constructible_v, expected const &> // + && not ::std::is_constructible_v, expected const>>; + + template using _can_copy_convert = _can_convert_detail; + template using _can_move_convert = _can_convert_detail; + template friend class expected; + + template + using _can_convert = ::std::bool_constant< // + not ::std::is_same_v<::std::remove_cvref_t, ::std::in_place_t> // + && not ::std::is_same_v<::std::remove_cvref_t, unexpect_t> // LWG4222 + && not ::std::is_same_v> // + && not detail::_is_some_unexpected<::std::remove_cvref_t> // + && ::std::is_constructible_v // + && (not ::std::is_same_v> // + || not detail::_is_some_expected<::std::remove_cvref_t>)>; + + template + static constexpr auto _and_then(Self &&self, Fn &&fn) // + noexcept(::std::is_nothrow_invocable_v && ::std::is_nothrow_constructible_v) + requires(::std::is_constructible_v) + { + using result_t = ::std::remove_cvref_t<::std::invoke_result_t>; + static_assert(detail::_is_some_expected); + static_assert(::std::is_same_v::error_type>); + if (self.has_value()) { + return ::std::invoke(FWD(fn)); + } + return result_t(unexpect, FWD(self).error()); + } + + template + static constexpr auto _or_else(Self &&self, Fn &&fn) // + noexcept(::std::is_nothrow_invocable_v) + { + using result_t = ::std::remove_cvref_t<::std::invoke_result_t>; + static_assert(detail::_is_some_expected); + static_assert(::std::is_same_v::value_type>); + if (self.has_value()) { + return result_t(::std::in_place); + } + return ::std::invoke(FWD(fn), FWD(self).error()); + } + + template + static constexpr auto _transform(Self &&self, Fn &&fn) // + noexcept(::std::is_nothrow_invocable_v && ::std::is_nothrow_constructible_v + && (::std::is_void_v<::std::invoke_result_t> + || ::std::is_nothrow_constructible_v<::std::remove_cv_t<::std::invoke_result_t>, + ::std::invoke_result_t>)) + requires(::std::is_constructible_v) + { + using value_t = ::std::remove_cv_t<::std::invoke_result_t>; + static_assert(detail::_is_valid_expected); + using result_t = expected; + if (self.has_value()) { + if constexpr (not ::std::is_void_v) { + static_assert(::std::is_constructible_v>); + return result_t(::std::in_place, ::std::invoke(FWD(fn))); + } else { + ::std::invoke(FWD(fn)); + return result_t(::std::in_place); + } + } + return result_t(unexpect, FWD(self).error()); + } + + template + static constexpr auto _transform_error(Self &&self, Fn &&fn) // + noexcept(::std::is_nothrow_invocable_v + && ::std::is_nothrow_constructible_v< + ::std::remove_cv_t<::std::invoke_result_t>, + ::std::invoke_result_t>) + { + using error_t = ::std::remove_cv_t<::std::invoke_result_t>; + static_assert(detail::_is_valid_unexpected); + static_assert(::std::is_constructible_v>); + using result_t = expected; + if (not self.has_value()) { + return result_t(unexpect, ::std::invoke(FWD(fn), FWD(self).error())); + } + return result_t(::std::in_place); + } + public: using value_type = T; using error_type = E; @@ -938,73 +1035,432 @@ class expected { template using rebind = expected; // [expected.void.cons], constructors - constexpr expected() noexcept; - constexpr expected(expected const &); - constexpr expected(expected &&) noexcept(/* TODO */ false); - template constexpr explicit(/* TODO */ false) expected(expected const &); - template constexpr explicit(/* TODO */ false) expected(expected &&); + constexpr expected() noexcept : d_(), set_(true) {} + constexpr expected(expected const &) = delete; + constexpr expected(expected const &) // + requires(::std::is_copy_constructible_v && ::std::is_trivially_copy_constructible_v) // + = default; + constexpr expected(expected const &s) // + noexcept(::std::is_nothrow_copy_constructible_v) // extension + requires(::std::is_copy_constructible_v && not ::std::is_trivially_copy_constructible_v) // + : d_(), set_(s.set_) + { + if (not set_) { + ::std::destroy_at(::std::addressof(d_)); + ::std::construct_at(::std::addressof(e_), s.e_); + } + } + + constexpr expected(expected &&s) // + requires(::std::is_move_constructible_v && ::std::is_trivially_move_constructible_v) // + = default; + constexpr expected(expected &&s) // + noexcept(::std::is_nothrow_move_constructible_v) // required + requires(::std::is_move_constructible_v && not ::std::is_trivially_move_constructible_v) + : d_(), set_(s.set_) + { + if (not set_) { + ::std::destroy_at(::std::addressof(d_)); + ::std::construct_at(::std::addressof(e_), ::std::move(s.e_)); + } + } + + template + constexpr explicit(not ::std::is_convertible_v) expected(expected const &s) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires(_can_copy_convert::value) + : d_(), set_(s.set_) + { + if (not set_) { + ::std::destroy_at(::std::addressof(d_)); + ::std::construct_at(::std::addressof(e_), s.e_); + } + } + template + constexpr explicit(not ::std::is_convertible_v) expected(expected &&s) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires(_can_move_convert::value) + : d_(), set_(s.set_) + { + if (not set_) { + ::std::destroy_at(::std::addressof(d_)); + ::std::construct_at(::std::addressof(e_), ::std::move(s.e_)); + } + } + + template + constexpr explicit(!::std::is_convertible_v) expected(unexpected const &g) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires(::std::is_constructible_v) + : e_(::std::forward(g.error())), set_(false) + { + } + template + constexpr explicit(!::std::is_convertible_v) expected(unexpected &&g) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires(::std::is_constructible_v) + : e_(::std::forward(g.error())), set_(false) + { + } - template constexpr explicit(/* TODO */ false) expected(unexpected const &); - template constexpr explicit(/* TODO */ false) expected(unexpected &&); + constexpr explicit expected(::std::in_place_t) noexcept : d_(), set_(true) {} - constexpr explicit expected(::std::in_place_t) noexcept; - template constexpr explicit expected(unexpect_t, Args &&...); - template constexpr explicit expected(unexpect_t, ::std::initializer_list, Args &&...); + template + constexpr explicit expected(unexpect_t, Args &&...a) // + noexcept(::std::is_nothrow_constructible_v) // extension + requires ::std::is_constructible_v + : e_(FWD(a)...), set_(false) + { + } + template + constexpr explicit expected(unexpect_t, ::std::initializer_list il, Args &&...a) // + noexcept(::std::is_nothrow_constructible_v &, Args...>) // extension + requires ::std::is_constructible_v &, Args...> + : e_(il, FWD(a)...), set_(false) + { + } // [expected.void.dtor], destructor - constexpr ~expected(); + constexpr ~expected() noexcept + requires(::std::is_trivially_destructible_v) + = default; + constexpr ~expected() // + noexcept(::std::is_nothrow_destructible_v) // extension + requires(not ::std::is_trivially_destructible_v) + { + if (not set_) + ::std::destroy_at(::std::addressof(e_)); + else + ::std::destroy_at(::std::addressof(d_)); + } // [expected.void.assign], assignment - constexpr expected &operator=(expected const &); - constexpr expected &operator=(expected &&) noexcept(/* TODO */ false); - template constexpr expected &operator=(unexpected const &); - template constexpr expected &operator=(unexpected &&); - constexpr void emplace() noexcept; + constexpr expected &operator=(expected const &) = delete; + constexpr expected &operator=(expected const &s) // + noexcept(::std::is_nothrow_copy_assignable_v && ::std::is_nothrow_copy_constructible_v) // extension + requires(::std::is_copy_assignable_v && ::std::is_copy_constructible_v) + { + if (set_ && s.set_) { + ; + } else if (set_) { + ::std::destroy_at(::std::addressof(d_)); + ::std::construct_at(::std::addressof(e_), s.e_); + set_ = false; + } else if (s.set_) { + ::std::destroy_at(::std::addressof(e_)); + ::std::construct_at(::std::addressof(d_)); + set_ = true; + } else { + e_ = s.e_; + } + return *this; + } + + constexpr expected &operator=(expected &&s) // + noexcept(::std::is_nothrow_move_assignable_v && ::std::is_nothrow_move_constructible_v) // required + requires(::std::is_move_constructible_v && ::std::is_move_assignable_v) + { + if (set_ && s.set_) { + ; + } else if (set_) { + ::std::destroy_at(::std::addressof(d_)); + ::std::construct_at(::std::addressof(e_), ::std::move(s.e_)); + set_ = false; + } else if (s.set_) { + ::std::destroy_at(::std::addressof(e_)); + ::std::construct_at(::std::addressof(d_)); + set_ = true; + } else { + e_ = ::std::move(s.e_); + } + return *this; + } + + template + constexpr expected &operator=(unexpected const &s) // + noexcept(::std::is_nothrow_assignable_v + && ::std::is_nothrow_constructible_v) // extension + requires(::std::is_constructible_v && ::std::is_assignable_v) + { + if (set_) { + ::std::destroy_at(::std::addressof(d_)); + ::std::construct_at(::std::addressof(e_), ::std::forward(s.error())); + set_ = false; + } else { + e_ = ::std::forward(s.error()); + } + return *this; + } + + template + constexpr expected &operator=(unexpected &&s) // + noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v) // extension + requires(::std::is_constructible_v && ::std::is_assignable_v) + { + if (set_) { + ::std::destroy_at(::std::addressof(d_)); + ::std::construct_at(::std::addressof(e_), ::std::forward(s.error())); + set_ = false; + } else { + e_ = ::std::forward(s.error()); + } + return *this; + } + + constexpr void emplace() noexcept + { + if (not set_) { + ::std::destroy_at(::std::addressof(e_)); + ::std::construct_at(::std::addressof(d_)); + set_ = true; + } + } // [expected.void.swap], swap - constexpr void swap(expected &) noexcept(/* TODO */ false); - constexpr friend void swap(expected &x, expected &y) noexcept(noexcept(x.swap(y))); + constexpr void swap(expected &rhs) // + noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_swappable_v) + requires(::std::is_swappable_v && ::std::is_move_constructible_v) + { + bool const lhset = has_value(); + bool const rhset = rhs.has_value(); + if (lhset == rhset) { + if (not lhset) { + using ::std::swap; + swap(e_, rhs.e_); + } + } else { + if (lhset) { + ::std::destroy_at(::std::addressof(d_)); + ::std::construct_at(::std::addressof(e_), ::std::move(rhs.e_)); + ::std::destroy_at(::std::addressof(rhs.e_)); + ::std::construct_at(::std::addressof(rhs.d_)); + set_ = false; + rhs.set_ = true; + } else { + rhs.swap(*this); + } + } + } + + constexpr friend void swap(expected &x, expected &y) noexcept(noexcept(x.swap(y))) + requires requires { x.swap(y); } + { + x.swap(y); + } // [expected.void.obs], observers - constexpr explicit operator bool() const noexcept; - constexpr bool has_value() const noexcept; - constexpr void operator*() const noexcept; - constexpr void value() const &; // freestanding-deleted - constexpr void value() &&; // freestanding-deleted - constexpr E const &error() const & noexcept; - constexpr E &error() & noexcept; - constexpr E const &&error() const && noexcept; - constexpr E &&error() && noexcept; - template constexpr E error_or(G &&) const &; - template constexpr E error_or(G &&) &&; + constexpr explicit operator bool() const noexcept { return set_; } + constexpr bool has_value() const noexcept { return set_; } + constexpr void operator*() const noexcept { ASSERT(set_); } + constexpr void value() const & + { + static_assert(::std::is_copy_constructible_v); + if (not set_) + throw bad_expected_access(e_); + } + constexpr void value() && + { + static_assert(::std::is_copy_constructible_v); + static_assert(::std::is_move_constructible_v); + if (not set_) + throw bad_expected_access(::std::move(e_)); + } + constexpr E const &error() const & noexcept + { + ASSERT(not set_); + return e_; + } + constexpr E &error() & noexcept + { + ASSERT(not set_); + return e_; + } + constexpr E const &&error() const && noexcept + { + ASSERT(not set_); + return ::std::move(e_); + } + constexpr E &&error() && noexcept + { + ASSERT(not set_); + return ::std::move(e_); + } + + template + constexpr E error_or(G &&e) const & // + noexcept(::std::is_nothrow_copy_constructible_v && ::std::is_nothrow_convertible_v) // extension + { + static_assert(::std::is_copy_constructible_v); + static_assert(::std::is_convertible_v); + if (set_) { + return FWD(e); + } + return e_; + } + template + constexpr E error_or(G &&e) && // + noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_convertible_v) // extension + { + static_assert(::std::is_move_constructible_v); + static_assert(::std::is_convertible_v); + if (set_) { + return FWD(e); + } + return ::std::move(e_); + } // [expected.void.monadic], monadic operations - template constexpr auto and_then(F &&f) &; - template constexpr auto and_then(F &&f) &&; - template constexpr auto and_then(F &&f) const &; - template constexpr auto and_then(F &&f) const &&; - template constexpr auto or_else(F &&f) &; - template constexpr auto or_else(F &&f) &&; - template constexpr auto or_else(F &&f) const &; - template constexpr auto or_else(F &&f) const &&; - template constexpr auto transform(F &&f) &; - template constexpr auto transform(F &&f) &&; - template constexpr auto transform(F &&f) const &; - template constexpr auto transform(F &&f) const &&; - template constexpr auto transform_error(F &&f) &; - template constexpr auto transform_error(F &&f) &&; - template constexpr auto transform_error(F &&f) const &; - template constexpr auto transform_error(F &&f) const &&; + template + constexpr auto and_then(F &&f) & // + noexcept(noexcept(_and_then)) // extension + -> decltype(_and_then(*this, FWD(f))) + { + return _and_then(*this, FWD(f)); + } + template + constexpr auto and_then(F &&f) && // + noexcept(noexcept(_and_then)) // extension + -> decltype(_and_then(::std::move(*this), FWD(f))) + { + return _and_then(::std::move(*this), FWD(f)); + } + template + constexpr auto and_then(F &&f) const & // + noexcept(noexcept(_and_then)) // extension + -> decltype(_and_then(*this, FWD(f))) + { + return _and_then(*this, FWD(f)); + } + template + constexpr auto and_then(F &&f) const && // + noexcept(noexcept(_and_then)) // extension + -> decltype(_and_then(::std::move(*this), FWD(f))) + { + return _and_then(::std::move(*this), FWD(f)); + } + + template + constexpr auto or_else(F &&f) & // + noexcept(noexcept(_or_else)) // extension + -> decltype(_or_else(*this, FWD(f))) + { + return _or_else(*this, FWD(f)); + } + template + constexpr auto or_else(F &&f) && // + noexcept(noexcept(_or_else)) // extension + -> decltype(_or_else(::std::move(*this), FWD(f))) + { + return _or_else(::std::move(*this), FWD(f)); + } + template + constexpr auto or_else(F &&f) const & // + noexcept(noexcept(_or_else)) // extension + -> decltype(_or_else(*this, FWD(f))) + { + return _or_else(*this, FWD(f)); + } + template + constexpr auto or_else(F &&f) const && // + noexcept(noexcept(_or_else)) // extension + -> decltype(_or_else(::std::move(*this), FWD(f))) + { + return _or_else(::std::move(*this), FWD(f)); + } + + template + constexpr auto transform(F &&f) & // + noexcept(noexcept(_transform)) // extension + -> decltype(_transform(*this, FWD(f))) + { + return _transform(*this, FWD(f)); + } + template + constexpr auto transform(F &&f) && // + noexcept(noexcept(_transform)) // extension + -> decltype(_transform(::std::move(*this), FWD(f))) + { + return _transform(::std::move(*this), FWD(f)); + } + template + constexpr auto transform(F &&f) const & // + noexcept(noexcept(_transform)) // extension + -> decltype(_transform(*this, FWD(f))) + { + return _transform(*this, FWD(f)); + } + template + constexpr auto transform(F &&f) const && // + noexcept(noexcept(_transform)) // extension + -> decltype(_transform(::std::move(*this), FWD(f))) + { + return _transform(::std::move(*this), FWD(f)); + } + + template + constexpr auto transform_error(F &&f) & // + noexcept(noexcept(_transform_error)) // extension + -> decltype(_transform_error(*this, FWD(f))) + { + return _transform_error(*this, FWD(f)); + } + template + constexpr auto transform_error(F &&f) && // + noexcept(noexcept(_transform_error)) // extension + -> decltype(_transform_error(::std::move(*this), FWD(f))) + { + return _transform_error(::std::move(*this), FWD(f)); + } + template + constexpr auto transform_error(F &&f) const & // + noexcept(noexcept(_transform_error)) // extension + -> decltype(_transform_error(*this, FWD(f))) + { + return _transform_error(*this, FWD(f)); + } + template + constexpr auto transform_error(F &&f) const && // + noexcept(noexcept(_transform_error)) // extension + -> decltype(_transform_error(::std::move(*this), FWD(f))) + { + return _transform_error(::std::move(*this), FWD(f)); + } // [expected.void.eq], equality operators template - requires ::std::is_void_v - constexpr friend bool operator==(expected const &x, expected const &y); - template constexpr friend bool operator==(expected const &, unexpected const &); + requires(::std::is_void_v) + constexpr friend bool operator==(expected const &x, expected const &y) // + noexcept(noexcept(x.error() == y.error())) // extension + requires requires { + { x.error() == y.error() } -> std::convertible_to; + } + { + if (x.has_value() != y.has_value()) { + return false; + } else if (x.has_value()) { + return true; + } else { + return x.error() == y.error(); + } + } + template + constexpr friend bool operator==(expected const &x, unexpected const &e) // + noexcept(noexcept(x.error() == e.error())) // extension + requires requires { + { x.error() == e.error() } -> std::convertible_to; + } + { + return (not x.has_value()) && static_cast(x.error() == e.error()); + } private: + struct dummy final { + constexpr dummy() noexcept = default; + constexpr ~dummy() noexcept = default; + }; + union { - unsigned char dummy_; + [[no_unique_address]] dummy d_; E e_; }; bool set_; diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index cc7ce61f..065dd469 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -13,7 +13,6 @@ using pfn::expected; using pfn::unexpect; using pfn::unexpect_t; using pfn::unexpected; -constexpr char unexpected_what[] = "bad access to expected without expected value"; #else #include using std::bad_expected_access; @@ -21,7 +20,6 @@ using std::expected; using std::unexpect; using std::unexpect_t; using std::unexpected; -constexpr char unexpected_what[] = "bad access to std::expected without expected value"; #endif #include @@ -30,6 +28,7 @@ constexpr char unexpected_what[] = "bad access to std::expected without expected #include #include +#include #include #include @@ -37,6 +36,9 @@ enum Error { unknown = 1, file_not_found = 5 }; TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") { + std::string const e1 = "bad access to expected"; + std::string const e2 = "bad access to std::expected"; + SECTION("bad_expected_access") { struct T : bad_expected_access {}; @@ -67,7 +69,8 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") a = [&]() -> T const && { return std::move(a); }(); CHECK(T{}.what() == a.what()); } - CHECK(std::strcmp(a.what(), unexpected_what) == 0); + std::string const tmp = a.what(); + CHECK(((tmp.substr(0, e1.size()) == e1) || (tmp.substr(0, e2.size()) == e2))); T const b; CHECK(&decltype(a)::what == &decltype(b)::what); @@ -199,7 +202,8 @@ TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]") SECTION("bad_expected_access") { T a{12}; - CHECK(std::strcmp(a.what(), unexpected_what) == 0); + std::string const tmp = a.what(); + CHECK(((tmp.substr(0, e1.size()) == e1) || (tmp.substr(0, e2.size()) == e2))); auto const c = []() { struct C : bad_expected_access {}; return C{}; @@ -452,7 +456,7 @@ concept is_swappable = requires { swap(std::declval(), std::declval()) } // namespace -TEST_CASE("expected", "[expected][polyfill]") +TEST_CASE("expected non void", "[expected][polyfill]") { #if LIBFN_MODE < 23 constexpr bool extension = true; @@ -2167,9 +2171,9 @@ TEST_CASE("expected", "[expected][polyfill]") { using T = expected; static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(noexcept(std::declval().value_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().value_or(std::declval()))); static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(noexcept(std::declval().value_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().value_or(std::declval()))); SECTION("value") { @@ -2208,7 +2212,7 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(std::is_nothrow_convertible_v); static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(noexcept(std::declval().value_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().value_or(std::declval()))); static_assert(not noexcept(std::declval().value_or(std::declval()))); static_assert(not noexcept(std::declval().value_or(std::declval()))); } @@ -2223,7 +2227,7 @@ TEST_CASE("expected", "[expected][polyfill]") static_assert(not noexcept(std::declval().value_or(std::declval()))); static_assert(not noexcept(std::declval().value_or(std::declval()))); static_assert(not noexcept(std::declval().value_or(std::declval()))); - static_assert(noexcept(std::declval().value_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().value_or(std::declval()))); } SUCCEED(); @@ -2742,3 +2746,1446 @@ TEST_CASE("expected", "[expected][polyfill]") } } } + +TEST_CASE("expected void", "[expected_void][polyfill]") +{ +#if LIBFN_MODE < 23 + constexpr bool extension = true; +#else + constexpr bool extension = false; +#endif + + SECTION("constructors") + { + SECTION("default unavailable") + { + static_assert(not std::is_default_constructible_v); // prerequisite + static_assert(not std::is_default_constructible_v>); + static_assert(std::is_default_constructible_v>); + SUCCEED(); + } + + SECTION("default trivial") + { + using T = expected; + static_assert(std::is_default_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); + + constexpr T a; + static_assert(a.has_value()); + SUCCEED(); + } + + struct A { + constexpr A() noexcept(true) {} + constexpr bool operator==(A const &) const = default; + + private: + int v = 12; + }; + + SECTION("default noexcept(true) from value type") + { + using T = expected; + static_assert(std::is_default_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); + + constexpr T a; + static_assert(a.has_value()); + + T b; + CHECK(b.has_value()); + } + + struct B { + constexpr B() noexcept(false) {} + int v = 42; + }; + + SECTION("default ignore noexcept from error type") + { + using T = expected; + static_assert(std::is_default_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); + SUCCEED(); + } + + struct C { + C() noexcept(false) { throw 7; } + }; + + SECTION("default exception thrown") + { + // not a problem if error type ctor is throwing + using T = expected; + static_assert(std::is_default_constructible_v); + static_assert(not extension || std::is_nothrow_constructible_v); + + T b; + CHECK(b.has_value()); + } + + SECTION("from unexpected rval") + { + using T = expected; + static_assert(std::is_constructible_v>); + static_assert(not extension || not std::is_nothrow_constructible_v>); + static_assert(std::is_constructible_v>); + static_assert(not extension || std::is_nothrow_constructible_v>); + + constexpr expected a(unexpected(true)); + static_assert(a.error() == 1); + + T const b(unexpected(5)); + CHECK(b.error().v == 5); + } + + SECTION("from unexpected lval const") + { + using T = expected; + constexpr auto g1 = unexpected(5); + constexpr expected a(g1); + static_assert(a.error() == 5); + + T const b(g1); + CHECK(b.error().v == 5); + } + + SECTION("with in_place") + { + using T = expected; + static_assert(std::is_constructible_v); + static_assert(std::is_nothrow_constructible_v); + + T const b(std::in_place); + CHECK(b.has_value()); + } + } + + SECTION("copy, move and dtor") + { + SECTION("trivial") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(std::is_trivially_copy_constructible_v); + static_assert(not extension || std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_trivially_move_constructible_v); + static_assert(not extension || std::is_nothrow_move_constructible_v); + static_assert(std::is_trivially_destructible_v); + static_assert(std::is_nothrow_destructible_v); + + constexpr T a; + constexpr T b = a; + static_assert(b.has_value()); + + { + T a(std::in_place); + T b = a; + CHECK(b.has_value()); + + T c = std::move(a); + CHECK(c.has_value()); + } + } + + SECTION("non-trivial error type") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); + static_assert(not extension || std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); // TODO required + static_assert(not std::is_trivially_destructible_v); + static_assert(std::is_nothrow_destructible_v); + + { + T a(unexpect, 33); + T b = a; // no overload for lval + CHECK(not b.has_value()); + CHECK(b.error().v == 33 * helper::from_lval_const); + + T c = std::as_const(a); + CHECK(not b.has_value()); + CHECK(c.error().v == 33 * helper::from_lval_const); + + T d = std::move(std::as_const(a)); // no overload for lval const + CHECK(not b.has_value()); + CHECK(d.error().v == 33 * helper::from_lval_const); + + T e = std::move(a); + CHECK(not b.has_value()); + CHECK(e.error().v == 33 * helper::from_rval); + } + } + + struct B { + int v; + constexpr B(int v) : v(v) {} + constexpr B(B const &s) noexcept(false) : v(s.v) {}; + constexpr B(B &&s) noexcept(false) : v(s.v) {}; + }; + + SECTION("noexcept(false) from error type") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); + static_assert(not extension || not std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); // required + static_assert(std::is_trivially_destructible_v); + static_assert(std::is_nothrow_destructible_v); + + constexpr T a(unexpect, 23); + constexpr T b = a; + static_assert(not b.has_value() && a.error().v == b.error().v); + + { + T const a(unexpect, 29); + T b = a; + CHECK(not b.has_value()); + CHECK(b.error().v == 29); + + T c = std::move(a); + CHECK(not c.has_value()); + CHECK(c.error().v == 29); + } + } + + struct C { + constexpr C() noexcept(true) {}; + constexpr C(C const &) noexcept(true) {}; // WORKAROUND:MSVC + constexpr ~C() noexcept(false) {}; + }; + + SECTION("noexcept(false) dtor error type") + { + using T = expected; + static_assert(std::is_copy_constructible_v); + static_assert(not std::is_trivially_copy_constructible_v); + static_assert(not extension || not std::is_nothrow_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_trivially_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); // required + static_assert(not std::is_trivially_destructible_v); + static_assert(not extension || not std::is_nothrow_destructible_v); + + constexpr T a(unexpect); + constexpr T b = a; + static_assert(not b.has_value()); + + { + T const a(unexpect); + T b = a; + CHECK(not b.has_value()); + } + } + } + + SECTION("assignment") + { + using M = helper_t<2>; // nothrow move constructible + using E = helper_t<3>; // may throw on move and copy + using C = helper_t<4>; // nothrow copy constructible + static_assert(not std::is_nothrow_copy_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + SECTION("from rval") + { + SECTION("value to value") + { + using T = expected; + + { + static_assert(std::is_nothrow_assignable_v); // required + + T a(std::in_place); + a = T(std::in_place); + CHECK(a.has_value()); + } + } + + SECTION("value to error") + { + SECTION("nothrow move") + { + using T = expected; + static_assert(std::is_nothrow_assignable_v); // required + + { + T a(std::in_place); + a = T(unexpect, 5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + static_assert(not extension || std::is_nothrow_assignable_v &&>); + T a(std::in_place); + a = unexpected(5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place); + try { + a = unexpected({0.0}); + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + } + + SECTION("throwing") + { + using T = expected; + static_assert(not std::is_nothrow_assignable_v); // required + + { + T a(std::in_place); + a = T(unexpect, 5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place); + try { + a = T(unexpect, {0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.has_value()); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v &&>); + T a(std::in_place); + a = unexpected(5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place); + try { + a = unexpected({0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.has_value()); + } + } + } + + SECTION("nothrow copy") + { + using T = expected; + static_assert(not std::is_nothrow_assignable_v); // required + + { + T a(std::in_place); + a = T(unexpect, 5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place); + try { + a = T(unexpect, {0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.has_value()); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v &&>); + T a(std::in_place); + a = unexpected(5); + CHECK(a.error().v == 5 * helper::from_rval); + } + + { + T a(std::in_place); + try { + a = unexpected({0.0}); + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.has_value()); + } + } + } + } + + SECTION("error to value") + { + SECTION("nothrow move") + { + using T = expected; + static_assert(std::is_nothrow_assignable_v); // required + + { + T a(unexpect, Error::file_not_found); + a = T(std::in_place); + CHECK(a.has_value()); + } + + { + T a(unexpect, Error::file_not_found); + try { + a = T(std::in_place); + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + } + } + + SECTION("error to error") + { + using T = expected; + static_assert(std::is_nothrow_assignable_v); // required + + T a(unexpect, 3); + a = T(unexpect, 5); + CHECK(a.error().v == 5 * helper::from_rval); + + a = unexpected(7); + CHECK(a.error().v == 7 * helper::from_rval); + } + + SECTION("constexpr") + { + using T = expected; + + SECTION("from error") + { + constexpr auto fn = [](T &&v) constexpr -> T { + T tmp{unexpect, Error::unknown}; + tmp = std::move(v); + return tmp; + }; + + constexpr T a = fn(T(unexpect, Error::file_not_found)); + static_assert(a.error() == Error::file_not_found); + + constexpr T b = fn(T(std::in_place)); // TODO + static_assert(b.has_value()); + + SUCCEED(); + } + + SECTION("from value") + { + { + constexpr auto fn = [](T &&v) constexpr -> T { + T tmp{std::in_place}; + tmp = std::move(v); + return tmp; + }; + + constexpr T a = fn(T(unexpect, Error::file_not_found)); + static_assert(a.error() == Error::file_not_found); + + constexpr T b = fn(T(std::in_place)); + static_assert(b.has_value()); + + SUCCEED(); + } + } + } + } + + SECTION("from lval const") + { + SECTION("value to error") + { + SECTION("nothrow move") + { + using T = expected; + static_assert(not extension || not std::is_nothrow_assignable_v); + + { + T a(std::in_place); + T const b(unexpect, 5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place); + try { + T const b(unexpect, {0.0}); + a = b; // copy construction on a side of `New tmp` will throw + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.has_value()); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v const &>); + T a(std::in_place); + unexpected const b(5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place); + try { + unexpected const b({0.0}); + a = b; + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.has_value()); + } + } + } + + SECTION("throwing") + { + using T = expected; + static_assert(not extension || not std::is_nothrow_assignable_v); + + { + T a(std::in_place); + T const b(unexpect, 5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place); + try { + T const b(unexpect, {0.0}); + a = b; + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.has_value()); + } + } + + { + static_assert(not extension || not std::is_nothrow_assignable_v const &>); + T a(std::in_place); + unexpected const b(5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place); + try { + unexpected const b({0.0}); + a = b; + FAIL(); + } catch (std::runtime_error const &e) { + CHECK(std::strcmp(e.what(), "invalid input") == 0); + CHECK(a.has_value()); + } + } + } + + SECTION("nothrow copy") + { + using T = expected; + static_assert(not extension || std::is_nothrow_assignable_v); + + { + T a(std::in_place); + T const b(unexpect, 5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place); + try { + T const b(unexpect, {0.0}); + a = b; + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + + { + static_assert(not extension || std::is_nothrow_assignable_v const &>); + T a(std::in_place); + unexpected const b(5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + } + + { + T a(std::in_place); + try { + unexpected b(std::in_place, {0.0}); + a = b; + SUCCEED(); + } catch (std::runtime_error const &e) { + FAIL(); + } + } + } + } + + SECTION("error to value") + { + using T = expected; + static_assert(std::is_nothrow_assignable_v); + + { + T a(unexpect, Error::file_not_found); + T const b(std::in_place); + a = b; + CHECK(a.has_value()); + } + } + + SECTION("error to error") + { + using T = expected; + static_assert(not extension || std::is_nothrow_assignable_v); + + T a(unexpect, 3); + T const b(unexpect, 5); + a = b; + CHECK(a.error().v == 5 * helper::from_lval_const); + + unexpected const c(7); + a = c; + CHECK(a.error().v == 7 * helper::from_lval_const); + } + } + + SECTION("constexpr") + { + using T = expected; + constexpr T c{unexpect, Error::file_not_found}; + constexpr T d{std::in_place}; + + SECTION("from error") + { + constexpr auto fn = [](T const &v) constexpr -> T { + T tmp{unexpect, Error::unknown}; + tmp = v; + return tmp; + }; + + constexpr T a = fn(c); + static_assert(a.error() == Error::file_not_found); + + constexpr T b = fn(d); // TODO + static_assert(b.has_value()); + + SUCCEED(); + } + + SECTION("from value") + { + { + constexpr auto fn = [](T const &v) constexpr -> T { + T tmp{std::in_place}; + tmp = v; + return tmp; + }; + + constexpr T a = fn(c); + static_assert(a.error() == Error::file_not_found); + + constexpr T b = fn(d); + static_assert(b.has_value()); + + SUCCEED(); + } + } + } + } + + SECTION("emplace") + { + using T = expected; + SECTION("value to value") + { + T a(std::in_place); + a.emplace(); + CHECK(a.has_value()); + } + + SECTION("error to value") + { + T a(unexpect, Error::file_not_found); + a.emplace(); + CHECK(a.has_value()); + } + + SECTION("constexpr") + { + using T = expected; + + SECTION("from error") + { + constexpr auto fn = []() constexpr -> T { + T tmp{unexpect, Error::unknown}; + tmp.emplace(); + return tmp; + }; + + constexpr T a = fn(); // TODO + static_assert(a.has_value()); + + SUCCEED(); + } + + SECTION("from value") + { + { + constexpr auto fn = []() constexpr -> T { + T tmp{std::in_place}; + tmp.emplace(); + return tmp; + }; + + constexpr T a = fn(); + static_assert(a.has_value()); + + SUCCEED(); + } + } + + SECTION("throwing constructor") + { + constexpr auto fn = [](auto &&...args) constexpr -> bool { + return requires { std::declval().emplace(std::forward(args)...); }; + }; + + static_assert(not fn(1, 2)); + + SUCCEED(); + } + } + } + + SECTION("swap") + { + SECTION("non-swappable") + { + struct A : non_swappable {}; + static_assert(not std::is_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + + static_assert(not extension || not is_swappable>); + + SUCCEED(); + } + + SECTION("non-move-constructible") + { + struct A : swappable { + A(A &&) = delete; + }; + static_assert(std::is_swappable_v); + static_assert(not std::is_nothrow_swappable_v); + static_assert(not std::is_move_constructible_v); + + static_assert(not is_swappable>); + + SUCCEED(); + } + + SECTION("non-nothrow-move-constructible") + { + struct A : swappable { + A(A &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(not std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + + SUCCEED(); + } + + SECTION("nothrow-swappable non-nothrow-move-constructible") + { + struct A : nothrow_swappable { + A(A &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + + SUCCEED(); + } + + SECTION("swappabla, non-nothrow-move-constructible") + { + struct A : swappable { + A(A &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(not std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + + SUCCEED(); + } + + SECTION("nothrow-swappabla, non-nothrow-move-constructible") + { + struct A : nothrow_swappable { + A(A &&) noexcept(false) {} + }; + static_assert(std::is_swappable_v); + static_assert(std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + + SUCCEED(); + } + + SECTION("nothrow-swappabla, nothrow-move-constructible") + { + struct A : nothrow_swappable { + A(A &&) noexcept(true) = default; + }; + static_assert(std::is_swappable_v); + static_assert(std::is_nothrow_swappable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + + static_assert(is_swappable>); + + SUCCEED(); + } + + SECTION("swap same") + { + SECTION("value") + { + using T = expected; + T a; + T b; + swap(a, b); + CHECK(a.has_value()); + CHECK(b.has_value()); + } + + SECTION("error") + { + using T = expected; + T a(unexpect, 17); + T b(unexpect, 23); + swap(a, b); + CHECK(a.error().v == 23 * helper::swapped); + CHECK(b.error().v == 17 * helper::swapped); + } + } + + SECTION("swap error/value") + { + using T = expected>; + T a(unexpect, 19); + T b; + swap(a, b); + CHECK(a.has_value()); + CHECK(b.error().v == 19 * helper::from_rval); + } + + SECTION("swap value/error") + { + SECTION("nothrow") + { + using T = expected>; + T a; + T b(unexpect, 29); + swap(a, b); + CHECK(a.error().v == 29 * helper::from_rval); + CHECK(b.has_value()); + } + + static_assert(not std::is_nothrow_move_constructible_v>); + static_assert(std::is_nothrow_move_constructible_v>); + + SECTION("throw error") + { + using T = expected>; + SECTION("happy path") + { + T a; + T b(unexpect, 11); + swap(a, b); + CHECK(a.error().v == 11 * helper::from_rval); + CHECK(b.has_value()); + } + + SECTION("exception") + { + T a(std::in_place); + T b(unexpect, {0.0}); + try { + swap(a, b); + FAIL(); + } catch (std::runtime_error const &) { + CHECK(a.has_value()); + CHECK(b.error().v == 0); + } + } + } + } + + SECTION("constexpr") + { + using T = expected; + + SECTION("to error") + { + constexpr auto fn = [](T &&v) constexpr -> T { + T tmp{unexpect, Error::unknown}; + swap(tmp, v); + return v; + }; + + constexpr T a = fn(T(unexpect, Error::file_not_found)); + static_assert(a.error() == Error::unknown); + + constexpr T b = fn(T()); + static_assert(b.error() == Error::unknown); + + SUCCEED(); + } + + SECTION("to value") + { + constexpr auto fn = [](T &&v) constexpr -> T { + T tmp{std::in_place}; + swap(tmp, v); + return v; + }; + + constexpr T a = fn(T(unexpect, Error::file_not_found)); // TODO + static_assert(a.has_value()); + + constexpr T b = fn(T()); + static_assert(b.has_value()); + + SUCCEED(); + } + } + } + + SECTION("accessors") + { + SECTION("value") + { + using T = expected; + + T a; + static_assert(std::is_same_v); + SUCCEED(); + + { + T a{unexpect, Error::file_not_found}; + CHECK(!a); + + try { + a.value(); + FAIL(); + } catch (bad_expected_access const &e) { + CHECK(e.error() == Error::file_not_found); + } + + try { + std::as_const(a).value(); + FAIL(); + } catch (bad_expected_access const &e) { + CHECK(e.error() == Error::file_not_found); + } + + try { + std::move(std::as_const(a)).value(); + FAIL(); + } catch (bad_expected_access const &e) { + CHECK(e.error() == Error::file_not_found); + } + + try { + std::move(a).value(); + FAIL(); + } catch (bad_expected_access const &e) { + CHECK(e.error() == Error::file_not_found); + } + } + } + + SECTION("error") + { + using T = expected; + + T a{unexpect, 17}; + CHECK(a.error().v == 17); + CHECK(std::as_const(a).error().v == 17); + CHECK(std::move(std::as_const(a)).error().v == 17); + CHECK(std::move(a).error().v == 17); + + { + helper b{1}; + CHECK((b = a.error()).v == 17 * helper::from_lval); + CHECK((b = std::as_const(a).error()).v == 17 * helper::from_lval_const); + CHECK((b = std::move(std::as_const(a)).error()).v == 17 * helper::from_rval_const); + CHECK((b = std::move(a).error()).v == 17 * helper::from_rval); + } + } + + SECTION("error_or") + { + using T = expected; + SECTION("noexcept extension") + { + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().error_or(std::declval()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().error_or(std::declval()))); + } + + SECTION("error") + { + T a(unexpect, 7); + CHECK(a.error_or(0) == helper(7 * helper::from_lval_const)); + CHECK(std::as_const(a).error_or(0) == helper(7 * helper::from_lval_const)); + CHECK(std::move(std::as_const(a)).error_or(0) == helper(7 * helper::from_lval_const)); + CHECK(std::move(a).error_or(0) == helper(7 * helper::from_rval)); + } + + SECTION("value") + { + { + T a{}; + CHECK(a.error_or(17) == helper(17)); + CHECK(std::move(a).error_or(5) == helper(5)); + } + + { + T const a{}; + helper b(11); + CHECK(a.error_or(b) == helper(11 * helper::from_lval)); + CHECK(a.error_or(std::as_const(b)) == helper(11 * helper::from_lval_const)); + CHECK(a.error_or(std::move(std::as_const(b))) == helper(11 * helper::from_rval_const)); + CHECK(a.error_or(std::move(b)) == helper(11 * helper::from_rval)); + } + } + + SECTION("noexcept extension") + { + { + using T = expected>; + static_assert(not std::is_nothrow_copy_constructible_v); + static_assert(std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_convertible_v); + static_assert(std::is_nothrow_convertible_v); + + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().error_or(std::declval()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); + } + + { + using T = expected>; + static_assert(std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_move_constructible_v); + static_assert(not std::is_nothrow_convertible_v); + static_assert(std::is_nothrow_convertible_v); + + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not noexcept(std::declval().error_or(std::declval()))); + static_assert(not extension || noexcept(std::declval().error_or(std::declval()))); + } + + SUCCEED(); + } + + SECTION("constexpr") + { + using T = expected; + constexpr helper c{helper::list_t(), 7}; + + SECTION("lval const") + { + { + constexpr T a(unexpect, {3.0}, 5); + static_assert(a.error_or(c).v == 3 * 5 * helper::from_lval_const); + } + + { + constexpr T a(std::in_place); + static_assert(a.error_or(c).v == 7 * helper::from_lval_const); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{unexpect, {3.0}, 5}.error_or(c).v == 3 * 5 * helper::from_rval); + static_assert(T{std::in_place}.error_or(c).v == 7 * helper::from_lval_const); + static_assert(T{std::in_place}.error_or(helper(helper::list_t{7.0}, 3)).v == 7 * 3 * helper::from_rval); + + SUCCEED(); + } + } + } + } + + SECTION("monadic functions") + { + SECTION("and_then") + { + SECTION("value") + { + using T = expected; + constexpr auto fn = []() constexpr -> expected { return {2}; }; + + T a; + CHECK(a.and_then(fn).value() == 2); + CHECK(std::as_const(a).and_then(fn).value() == 2); + CHECK(std::move(std::as_const(a)).and_then(fn).value() == 2); + CHECK(std::move(a).and_then(fn).value() == 2); + } + + SECTION("error") + { + using T = expected; + constexpr auto fn = []() constexpr -> expected { return {0}; }; + + T a(unexpect, 11); + CHECK(a.and_then(fn).error().v == 11 * helper::from_lval); + CHECK(std::as_const(a).and_then(fn).error().v == 11 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).and_then(fn).error().v == 11 * helper::from_rval_const); + CHECK(std::move(a).and_then(fn).error().v == 11 * helper::from_rval); + } + + SECTION("constexpr") + { + using T = expected; + constexpr auto fn = []() constexpr -> expected { return {3}; }; + + SECTION("lval const") + { + { + constexpr T a(std::in_place); + static_assert(a.and_then(fn).value() == 3); + } + + { + constexpr T a(unexpect, Error::file_not_found); + static_assert(a.and_then(fn).error() == Error::file_not_found); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{std::in_place}.and_then(fn) == 3); + static_assert(T{unexpect, Error::file_not_found}.and_then(fn).error() == Error::file_not_found); + + SUCCEED(); + } + } + } + + SECTION("or_else") + { + SECTION("error") + { + using T = expected; + constexpr auto fn = [](auto &&a) constexpr -> expected { + return expected{unexpect, helper(std::forward(a)).v * 3}; + }; + + T a(unexpect, 5); + CHECK(a.or_else(fn).error() == 5 * 3 * helper::from_lval); + CHECK(std::as_const(a).or_else(fn).error() == 5 * 3 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).or_else(fn).error() == 5 * 3 * helper::from_rval_const); + CHECK(std::move(a).or_else(fn).error() == 5 * 3 * helper::from_rval); + } + + SECTION("value") + { + using T = expected; + constexpr auto fn = [](auto &&) constexpr -> expected { return expected{unexpect, 13}; }; + + T a; + CHECK(a.or_else(fn).has_value()); + CHECK(std::as_const(a).or_else(fn).has_value()); + CHECK(std::move(std::as_const(a)).or_else(fn).has_value()); + CHECK(std::move(a).or_else(fn).has_value()); + } + + SECTION("constexpr") + { + using T = expected; + constexpr helper c{helper::list_t(), 7}; + constexpr auto fn = [](auto &&a) constexpr -> expected { + return expected{unexpect, helper(std::forward(a)).v * 3}; + }; + + SECTION("lval const") + { + { + constexpr T a(std::in_place); + static_assert(a.or_else(fn).has_value()); + } + + { + constexpr T a(unexpect, {3.0}, 5); + static_assert(a.or_else(fn).error() == 3 * 3 * 5 * helper::from_lval_const); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{unexpect, {3.0}, 5}.or_else(fn).error() == 3 * 3 * 5 * helper::from_rval); + static_assert(T{std::in_place}.or_else(fn).has_value()); + + SUCCEED(); + } + } + } + + SECTION("transform") + { + SECTION("value") + { + using T = expected; + constexpr auto fn = []() constexpr -> int { return 2; }; + + T a; + CHECK(a.transform(fn).value() == 2); + CHECK(std::as_const(a).transform(fn).value() == 2); + CHECK(std::move(std::as_const(a)).transform(fn).value() == 2); + CHECK(std::move(a).transform(fn).value() == 2); + } + + SECTION("error") + { + using T = expected; + constexpr auto fn = []() constexpr -> int { return 0; }; + + T a(unexpect, 11); + CHECK(a.transform(fn).error().v == 11 * helper::from_lval); + CHECK(std::as_const(a).transform(fn).error().v == 11 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).transform(fn).error().v == 11 * helper::from_rval_const); + CHECK(std::move(a).transform(fn).error().v == 11 * helper::from_rval); + } + + SECTION("constexpr") + { + using T = expected; + constexpr auto fn = []() constexpr -> int { return 3; }; + + SECTION("lval const") + { + { + constexpr T a(std::in_place); + static_assert(a.transform(fn).value() == 3); + } + + { + constexpr T a(unexpect, Error::file_not_found); + static_assert(a.transform(fn).error() == Error::file_not_found); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{std::in_place}.transform(fn) == 3); + static_assert(T{unexpect, Error::file_not_found}.transform(fn).error() == Error::file_not_found); + + SUCCEED(); + } + } + } + + SECTION("transform_error") + { + SECTION("error") + { + using T = expected; + constexpr auto fn = [](auto &&a) constexpr -> int { return helper(std::forward(a)).v * 3; }; + + T a(unexpect, 5); + CHECK(a.transform_error(fn).error() == 5 * 3 * helper::from_lval); + CHECK(std::as_const(a).transform_error(fn).error() == 5 * 3 * helper::from_lval_const); + CHECK(std::move(std::as_const(a)).transform_error(fn).error() == 5 * 3 * helper::from_rval_const); + CHECK(std::move(a).transform_error(fn).error() == 5 * 3 * helper::from_rval); + } + + SECTION("value") + { + using T = expected; + constexpr auto fn = [](auto &&) constexpr -> int { return 0; }; + + T a{}; + CHECK(a.transform_error(fn).has_value()); + CHECK(std::as_const(a).transform_error(fn).has_value()); + CHECK(std::move(std::as_const(a)).transform_error(fn).has_value()); + CHECK(std::move(a).transform_error(fn).has_value()); + } + + SECTION("constexpr") + { + using T = expected; + constexpr helper c{helper::list_t(), 7}; + constexpr auto fn = [](auto &&a) constexpr -> int { return helper(std::forward(a)).v * 3; }; + + SECTION("lval const") + { + { + constexpr T a(std::in_place); + static_assert(a.transform_error(fn).has_value()); + } + + { + constexpr T a(unexpect, {3.0}, 5); + static_assert(a.transform_error(fn).error() == 3 * 3 * 5 * helper::from_lval_const); + } + + SUCCEED(); + } + + SECTION("rval") + { + static_assert(T{unexpect, {3.0}, 5}.transform_error(fn).error() == 3 * 3 * 5 * helper::from_rval); + static_assert(T{std::in_place}.transform_error(fn).has_value()); + + SUCCEED(); + } + } + } + } + + SECTION("equality operators") + { + SECTION("operand expected") + { + using T = expected; + using U = expected; + + SECTION("value and error") + { + T const t1{std::in_place}; + U const u1{unexpect, false}; + CHECK(not(t1 == u1)); + CHECK((t1 != u1)); + + constexpr T t2{unexpect, 12}; + constexpr U u2{std::in_place}; + static_assert(not(t2 == u2)); + static_assert(t2 != u2); + } + + SECTION("value") + { + T const t1{std::in_place}; + U const u1{std::in_place}; + CHECK((t1 == u1)); + } + + SECTION("error") + { + SECTION("same type") + { + using V = expected; + using W = expected; + + V const v1{unexpect, {12.0}}; + W const w1{unexpect, {3.0}}; + CHECK(not(v1 == w1)); + CHECK((v1 != w1)); + + constexpr V v2{unexpect, {3.0}, 4}; + constexpr W w2{unexpect, {3.0}, 2, 2}; + static_assert(v2 == w2); + static_assert(not(v2 != w2)); + CHECK((v1 == v2)); + } + + SECTION("different types") + { + using V = expected; + using W = expected; + static_assert(V{unexpect, 12} == W{unexpect, 12.0}); + static_assert(V{unexpect, 15} != W{unexpect, 12.0}); + + SUCCEED(); + } + } + } + + SECTION("operand unexpected") + { + SECTION("same type") + { + using T = expected; + using U = unexpected; + T const t1{unexpect, {12.0}}; + U const u1{std::in_place, {3.0}}; + U const v1{std::in_place, {3.0, 4.0}}; + CHECK(not(t1 == u1)); + CHECK((t1 == v1)); + CHECK((t1 != u1)); + + constexpr T t2{unexpect, {3.0}, 4}; + constexpr U u2{std::in_place, helper::list_t(), 3, 2, 2}; + static_assert(t2 == u2); + static_assert(not(t2 != u2)); + CHECK((t1 == t2)); + } + + SECTION("different types") + { + using T = expected; + using U = unexpected; + constexpr T t1{unexpect, 1}; + static_assert(t1 == U{Error::unknown}); + static_assert(t1 != U{Error::file_not_found}); + + SUCCEED(); + } + } + } +} From 3bf4228a8a7ecb65cccf63104add948fccfdfc60 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 20 Jul 2025 19:44:24 +0100 Subject: [PATCH 39/46] Decouple test validation from C++23 mode --- tests/CMakeLists.txt | 26 ++++++++++++++++++++++++++ tests/pfn/expected.cpp | 9 ++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 56eb21ac..5534c2b8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -70,6 +70,32 @@ foreach(mode 20 23) set_property(TEST "${target}" PROPERTY LABELS tests_pfn "cxx${mode}" "${root_name}") unset(target) + + # pfn tests have a "validation" mode, where the unit tests intended for polyfill + # are run against the "real" thing. This validates the tests. + if (mode EQUAL 23) + set(target "tests_${root_name}_validation_cxx${mode}") + + create_target_for_file( + NAME "${target}" + SOURCE "${source}" + DEPENDENCIES include_pfn tests_util Catch2::Catch2WithMain + ) + append_compilation_options("${target}" WARNINGS OPTIMIZATION) + add_dependencies("cxx${mode}" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") + target_compile_definitions("${target}" PRIVATE LIBFN_MODE=${mode} PFN_TEST_VALIDATION) + + add_test( + NAME "${target}" + COMMAND "${target}" -r console + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + set_property(TEST "${target}" PROPERTY LABELS tests_pfn "cxx${mode}" "${root_name}") + + unset(target) + endif() + unset(root_name) endforeach() endforeach() diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 065dd469..8bd1940d 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -3,10 +3,9 @@ // Distributed under the ISC License. See accompanying file LICENSE.md // or copy at https://opensource.org/licenses/ISC +#ifndef PFN_TEST_VALIDATION // TODO: Add death tests. Until then, empty definition to avoid false "no coverage" reports #define LIBFN_ASSERT(...) - -#if LIBFN_MODE < 23 #include using pfn::bad_expected_access; using pfn::expected; @@ -235,7 +234,7 @@ TEST_CASE("unexpect", "[expected][polyfill][unexpect]") TEST_CASE("unexpected", "[expected][polyfill][unexpected]") { -#if LIBFN_MODE < 23 +#ifndef PFN_TEST_VALIDATION SECTION("is_valid_unexpected") { using pfn::detail::_is_valid_unexpected; @@ -458,7 +457,7 @@ concept is_swappable = requires { swap(std::declval(), std::declval()) TEST_CASE("expected non void", "[expected][polyfill]") { -#if LIBFN_MODE < 23 +#ifndef PFN_TEST_VALIDATION constexpr bool extension = true; #else constexpr bool extension = false; @@ -2749,7 +2748,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") TEST_CASE("expected void", "[expected_void][polyfill]") { -#if LIBFN_MODE < 23 +#ifndef PFN_TEST_VALIDATION constexpr bool extension = true; #else constexpr bool extension = false; From aae61bc54d58286c2ae4609745d55dc490259089 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 20 Jul 2025 19:48:05 +0100 Subject: [PATCH 40/46] Remove TODO because done --- tests/pfn/expected.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 8bd1940d..dfac746b 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -2897,7 +2897,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") static_assert(not extension || std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); - static_assert(std::is_nothrow_move_constructible_v); // TODO required + static_assert(std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); static_assert(std::is_nothrow_destructible_v); @@ -3180,7 +3180,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") constexpr T a = fn(T(unexpect, Error::file_not_found)); static_assert(a.error() == Error::file_not_found); - constexpr T b = fn(T(std::in_place)); // TODO + constexpr T b = fn(T(std::in_place)); static_assert(b.has_value()); SUCCEED(); @@ -3391,7 +3391,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") constexpr T a = fn(c); static_assert(a.error() == Error::file_not_found); - constexpr T b = fn(d); // TODO + constexpr T b = fn(d); static_assert(b.has_value()); SUCCEED(); @@ -3447,7 +3447,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") return tmp; }; - constexpr T a = fn(); // TODO + constexpr T a = fn(); static_assert(a.has_value()); SUCCEED(); @@ -3689,7 +3689,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") return v; }; - constexpr T a = fn(T(unexpect, Error::file_not_found)); // TODO + constexpr T a = fn(T(unexpect, Error::file_not_found)); static_assert(a.has_value()); constexpr T b = fn(T()); From ce92e34c2a59e3bee847a7c961d129e4d9c2c7c9 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 20 Jul 2025 20:50:31 +0100 Subject: [PATCH 41/46] Remove redundant tests --- tests/pfn/expected.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index dfac746b..3b335106 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -3875,9 +3875,6 @@ TEST_CASE("expected void", "[expected_void][polyfill]") T a; CHECK(a.and_then(fn).value() == 2); - CHECK(std::as_const(a).and_then(fn).value() == 2); - CHECK(std::move(std::as_const(a)).and_then(fn).value() == 2); - CHECK(std::move(a).and_then(fn).value() == 2); } SECTION("error") @@ -3945,9 +3942,6 @@ TEST_CASE("expected void", "[expected_void][polyfill]") T a; CHECK(a.or_else(fn).has_value()); - CHECK(std::as_const(a).or_else(fn).has_value()); - CHECK(std::move(std::as_const(a)).or_else(fn).has_value()); - CHECK(std::move(a).or_else(fn).has_value()); } SECTION("constexpr") @@ -3992,9 +3986,6 @@ TEST_CASE("expected void", "[expected_void][polyfill]") T a; CHECK(a.transform(fn).value() == 2); - CHECK(std::as_const(a).transform(fn).value() == 2); - CHECK(std::move(std::as_const(a)).transform(fn).value() == 2); - CHECK(std::move(a).transform(fn).value() == 2); } SECTION("error") @@ -4060,9 +4051,6 @@ TEST_CASE("expected void", "[expected_void][polyfill]") T a{}; CHECK(a.transform_error(fn).has_value()); - CHECK(std::as_const(a).transform_error(fn).has_value()); - CHECK(std::move(std::as_const(a)).transform_error(fn).has_value()); - CHECK(std::move(a).transform_error(fn).has_value()); } SECTION("constexpr") From 1f88e4d3b53b39796f0bb077252e30e6d0adeb78 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 20 Jul 2025 21:17:44 +0100 Subject: [PATCH 42/46] Add missing tests --- tests/pfn/expected.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 3b335106..48e94330 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -2498,6 +2498,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") CHECK(std::as_const(a).transform(fn).value() == 7 * 2 * helper::from_lval_const); CHECK(std::move(std::as_const(a)).transform(fn).value() == 7 * 2 * helper::from_rval_const); CHECK(std::move(a).transform(fn).value() == 7 * 2 * helper::from_rval); + CHECK(a.transform([](auto &&) {}).has_value()); } SECTION("error") @@ -2510,6 +2511,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") CHECK(std::as_const(a).transform(fn).error().v == 11 * helper::from_lval_const); CHECK(std::move(std::as_const(a)).transform(fn).error().v == 11 * helper::from_rval_const); CHECK(std::move(a).transform(fn).error().v == 11 * helper::from_rval); + CHECK(a.transform([](auto &&) {}).error().v == 11 * helper::from_lval); } SECTION("constexpr") @@ -3986,6 +3988,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") T a; CHECK(a.transform(fn).value() == 2); + CHECK(a.transform([]() {}).has_value()); } SECTION("error") @@ -3998,6 +4001,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") CHECK(std::as_const(a).transform(fn).error().v == 11 * helper::from_lval_const); CHECK(std::move(std::as_const(a)).transform(fn).error().v == 11 * helper::from_rval_const); CHECK(std::move(a).transform(fn).error().v == 11 * helper::from_rval); + CHECK(a.transform([]() {}).error().v == 11 * helper::from_lval); } SECTION("constexpr") From 1538b0724eeb38f1751ac54143a4c59d21ae31d3 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 20 Jul 2025 21:30:05 +0100 Subject: [PATCH 43/46] Workarounds for MSVC errors --- cmake/CompilationOptions.cmake | 10 +++- include/pfn/expected.hpp | 104 ++++++++++++++++----------------- tests/pfn/expected.cpp | 5 +- 3 files changed, 65 insertions(+), 54 deletions(-) diff --git a/cmake/CompilationOptions.cmake b/cmake/CompilationOptions.cmake index ec5116c1..75e7d2df 100644 --- a/cmake/CompilationOptions.cmake +++ b/cmake/CompilationOptions.cmake @@ -21,12 +21,20 @@ function(append_compilation_options) if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") if(Options_WARNINGS) # disable C4456: declaration of 'b' hides previous local declaration - target_compile_options(${Options_NAME} PRIVATE /W4 /wd4456) + # disable C4244: 'initializing': conversion from '_Ty' to '_Ty', possible loss of data + # disable C4101: 'e': unreferenced local variable + target_compile_options(${Options_NAME} PRIVATE /W4 /wd4456 /wd4244 /wd4101 ) endif() if(Options_OPTIMIZATION) target_compile_options(${Options_NAME} PRIVATE $,/Od,/Ox>) endif() + + if(Options_INTERFACE) + target_compile_options(${Options_NAME} INTERFACE /Za /permissive-) + # This will disable `unexpected` in global namespace from eh.h + target_compile_definitions(${Options_NAME} INTERFACE _HAS_CXX23) + endif() elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?Clang") if(Options_WARNINGS) target_compile_options(${Options_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror -Wno-redundant-move) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 1e6a1140..fc0d9d5a 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -770,28 +770,28 @@ template class expected { // [expected.object.monadic], monadic operations template constexpr auto and_then(F &&f) & // - noexcept(noexcept(_and_then)) // extension + noexcept(noexcept(_and_then(*this, FWD(f)))) // extension -> decltype(_and_then(*this, FWD(f))) { return _and_then(*this, FWD(f)); } template - constexpr auto and_then(F &&f) && // - noexcept(noexcept(_and_then)) // extension + constexpr auto and_then(F &&f) && // + noexcept(noexcept(_and_then(::std::move(*this), FWD(f)))) // extension -> decltype(_and_then(::std::move(*this), FWD(f))) { return _and_then(::std::move(*this), FWD(f)); } template - constexpr auto and_then(F &&f) const & // - noexcept(noexcept(_and_then)) // extension + constexpr auto and_then(F &&f) const & // + noexcept(noexcept(_and_then(*this, FWD(f)))) // extension -> decltype(_and_then(*this, FWD(f))) { return _and_then(*this, FWD(f)); } template - constexpr auto and_then(F &&f) const && // - noexcept(noexcept(_and_then)) // extension + constexpr auto and_then(F &&f) const && // + noexcept(noexcept(_and_then(::std::move(*this), FWD(f)))) // extension -> decltype(_and_then(::std::move(*this), FWD(f))) { return _and_then(::std::move(*this), FWD(f)); @@ -799,28 +799,28 @@ template class expected { template constexpr auto or_else(F &&f) & // - noexcept(noexcept(_or_else)) // extension + noexcept(noexcept(_or_else(*this, FWD(f)))) // extension -> decltype(_or_else(*this, FWD(f))) { return _or_else(*this, FWD(f)); } template - constexpr auto or_else(F &&f) && // - noexcept(noexcept(_or_else)) // extension + constexpr auto or_else(F &&f) && // + noexcept(noexcept(_or_else(::std::move(*this), FWD(f)))) // extension -> decltype(_or_else(::std::move(*this), FWD(f))) { return _or_else(::std::move(*this), FWD(f)); } template - constexpr auto or_else(F &&f) const & // - noexcept(noexcept(_or_else)) // extension + constexpr auto or_else(F &&f) const & // + noexcept(noexcept(_or_else(*this, FWD(f)))) // extension -> decltype(_or_else(*this, FWD(f))) { return _or_else(*this, FWD(f)); } template - constexpr auto or_else(F &&f) const && // - noexcept(noexcept(_or_else)) // extension + constexpr auto or_else(F &&f) const && // + noexcept(noexcept(_or_else(::std::move(*this), FWD(f)))) // extension -> decltype(_or_else(::std::move(*this), FWD(f))) { return _or_else(::std::move(*this), FWD(f)); @@ -828,28 +828,28 @@ template class expected { template constexpr auto transform(F &&f) & // - noexcept(noexcept(_transform)) // extension + noexcept(noexcept(_transform(*this, FWD(f)))) // extension -> decltype(_transform(*this, FWD(f))) { return _transform(*this, FWD(f)); } template - constexpr auto transform(F &&f) && // - noexcept(noexcept(_transform)) // extension + constexpr auto transform(F &&f) && // + noexcept(noexcept(_transform(::std::move(*this), FWD(f)))) // extension -> decltype(_transform(::std::move(*this), FWD(f))) { return _transform(::std::move(*this), FWD(f)); } template constexpr auto transform(F &&f) const & // - noexcept(noexcept(_transform)) // extension + noexcept(noexcept(_transform(*this, FWD(f)))) // extension -> decltype(_transform(*this, FWD(f))) { return _transform(*this, FWD(f)); } template - constexpr auto transform(F &&f) const && // - noexcept(noexcept(_transform)) // extension + constexpr auto transform(F &&f) const && // + noexcept(noexcept(_transform(::std::move(*this), FWD(f)))) // extension -> decltype(_transform(::std::move(*this), FWD(f))) { return _transform(::std::move(*this), FWD(f)); @@ -857,28 +857,28 @@ template class expected { template constexpr auto transform_error(F &&f) & // - noexcept(noexcept(_transform_error)) // extension + noexcept(noexcept(_transform_error(*this, FWD(f)))) // extension -> decltype(_transform_error(*this, FWD(f))) { return _transform_error(*this, FWD(f)); } template - constexpr auto transform_error(F &&f) && // - noexcept(noexcept(_transform_error)) // extension + constexpr auto transform_error(F &&f) && // + noexcept(noexcept(_transform_error(::std::move(*this), FWD(f)))) // extension -> decltype(_transform_error(::std::move(*this), FWD(f))) { return _transform_error(::std::move(*this), FWD(f)); } template constexpr auto transform_error(F &&f) const & // - noexcept(noexcept(_transform_error)) // extension + noexcept(noexcept(_transform_error(*this, FWD(f)))) // extension -> decltype(_transform_error(*this, FWD(f))) { return _transform_error(*this, FWD(f)); } template - constexpr auto transform_error(F &&f) const && // - noexcept(noexcept(_transform_error)) // extension + constexpr auto transform_error(F &&f) const && // + noexcept(noexcept(_transform_error(::std::move(*this), FWD(f)))) // extension -> decltype(_transform_error(::std::move(*this), FWD(f))) { return _transform_error(::std::move(*this), FWD(f)); @@ -1312,28 +1312,28 @@ class expected { // [expected.void.monadic], monadic operations template constexpr auto and_then(F &&f) & // - noexcept(noexcept(_and_then)) // extension + noexcept(noexcept(_and_then(*this, FWD(f)))) // extension -> decltype(_and_then(*this, FWD(f))) { return _and_then(*this, FWD(f)); } template - constexpr auto and_then(F &&f) && // - noexcept(noexcept(_and_then)) // extension + constexpr auto and_then(F &&f) && // + noexcept(noexcept(_and_then(::std::move(*this), FWD(f)))) // extension -> decltype(_and_then(::std::move(*this), FWD(f))) { return _and_then(::std::move(*this), FWD(f)); } template - constexpr auto and_then(F &&f) const & // - noexcept(noexcept(_and_then)) // extension + constexpr auto and_then(F &&f) const & // + noexcept(noexcept(_and_then(*this, FWD(f)))) // extension -> decltype(_and_then(*this, FWD(f))) { return _and_then(*this, FWD(f)); } template - constexpr auto and_then(F &&f) const && // - noexcept(noexcept(_and_then)) // extension + constexpr auto and_then(F &&f) const && // + noexcept(noexcept(_and_then(::std::move(*this), FWD(f)))) // extension -> decltype(_and_then(::std::move(*this), FWD(f))) { return _and_then(::std::move(*this), FWD(f)); @@ -1341,28 +1341,28 @@ class expected { template constexpr auto or_else(F &&f) & // - noexcept(noexcept(_or_else)) // extension + noexcept(noexcept(_or_else(*this, FWD(f)))) // extension -> decltype(_or_else(*this, FWD(f))) { return _or_else(*this, FWD(f)); } template - constexpr auto or_else(F &&f) && // - noexcept(noexcept(_or_else)) // extension + constexpr auto or_else(F &&f) && // + noexcept(noexcept(_or_else(::std::move(*this), FWD(f)))) // extension -> decltype(_or_else(::std::move(*this), FWD(f))) { return _or_else(::std::move(*this), FWD(f)); } template - constexpr auto or_else(F &&f) const & // - noexcept(noexcept(_or_else)) // extension + constexpr auto or_else(F &&f) const & // + noexcept(noexcept(_or_else(*this, FWD(f)))) // extension -> decltype(_or_else(*this, FWD(f))) { return _or_else(*this, FWD(f)); } template - constexpr auto or_else(F &&f) const && // - noexcept(noexcept(_or_else)) // extension + constexpr auto or_else(F &&f) const && // + noexcept(noexcept(_or_else(::std::move(*this), FWD(f)))) // extension -> decltype(_or_else(::std::move(*this), FWD(f))) { return _or_else(::std::move(*this), FWD(f)); @@ -1370,28 +1370,28 @@ class expected { template constexpr auto transform(F &&f) & // - noexcept(noexcept(_transform)) // extension + noexcept(noexcept(_transform(*this, FWD(f)))) // extension -> decltype(_transform(*this, FWD(f))) { return _transform(*this, FWD(f)); } template - constexpr auto transform(F &&f) && // - noexcept(noexcept(_transform)) // extension + constexpr auto transform(F &&f) && // + noexcept(noexcept(_transform(::std::move(*this), FWD(f)))) // extension -> decltype(_transform(::std::move(*this), FWD(f))) { return _transform(::std::move(*this), FWD(f)); } template constexpr auto transform(F &&f) const & // - noexcept(noexcept(_transform)) // extension + noexcept(noexcept(_transform(*this, FWD(f)))) // extension -> decltype(_transform(*this, FWD(f))) { return _transform(*this, FWD(f)); } template - constexpr auto transform(F &&f) const && // - noexcept(noexcept(_transform)) // extension + constexpr auto transform(F &&f) const && // + noexcept(noexcept(_transform(::std::move(*this), FWD(f)))) // extension -> decltype(_transform(::std::move(*this), FWD(f))) { return _transform(::std::move(*this), FWD(f)); @@ -1399,28 +1399,28 @@ class expected { template constexpr auto transform_error(F &&f) & // - noexcept(noexcept(_transform_error)) // extension + noexcept(noexcept(_transform_error(*this, FWD(f)))) // extension -> decltype(_transform_error(*this, FWD(f))) { return _transform_error(*this, FWD(f)); } template - constexpr auto transform_error(F &&f) && // - noexcept(noexcept(_transform_error)) // extension + constexpr auto transform_error(F &&f) && // + noexcept(noexcept(_transform_error(::std::move(*this), FWD(f)))) // extension -> decltype(_transform_error(::std::move(*this), FWD(f))) { return _transform_error(::std::move(*this), FWD(f)); } template constexpr auto transform_error(F &&f) const & // - noexcept(noexcept(_transform_error)) // extension + noexcept(noexcept(_transform_error(*this, FWD(f)))) // extension -> decltype(_transform_error(*this, FWD(f))) { return _transform_error(*this, FWD(f)); } template - constexpr auto transform_error(F &&f) const && // - noexcept(noexcept(_transform_error)) // extension + constexpr auto transform_error(F &&f) const && // + noexcept(noexcept(_transform_error(::std::move(*this), FWD(f)))) // extension -> decltype(_transform_error(::std::move(*this), FWD(f))) { return _transform_error(::std::move(*this), FWD(f)); diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index 48e94330..a490b89c 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -2174,6 +2174,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") static_assert(not noexcept(std::declval().value_or(std::declval()))); static_assert(not extension || noexcept(std::declval().value_or(std::declval()))); +#ifndef _MSC_VER SECTION("value") { T a(7); @@ -2200,6 +2201,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") CHECK(a.value_or(std::move(b)) == helper(11 * helper::from_rval)); } } +#endif SECTION("noexcept extension") { @@ -2232,6 +2234,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") SUCCEED(); } +#ifndef _MSC_VER SECTION("constexpr") { using T = expected; @@ -2243,7 +2246,6 @@ TEST_CASE("expected non void", "[expected][polyfill]") constexpr T a(std::in_place, {3.0}, 5); static_assert(a.value_or(c).v == 3 * 5 * helper::from_lval_const); } - { constexpr T a(unexpect, Error::unknown); static_assert(a.value_or(c).v == 7 * helper::from_lval_const); @@ -2262,6 +2264,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") SUCCEED(); } } +#endif } SECTION("error_or") From afdb9dc9fec678f6b364a60ebb2b49a43a6ffee0 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 26 Jul 2025 13:11:00 +0100 Subject: [PATCH 44/46] Replace workflow modes with matrix.exclude --- .github/workflows/build.yml | 192 ++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 105 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf6b9aac..d3d73b75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,38 +33,30 @@ jobs: strategy: fail-fast: false matrix: + mode: + - cxx20 + - cxx23 configuration: - - Debug - - Release - env: - - compiler: gcc - release: 12 - modes: "cxx20" - - compiler: clang - release: 16 - modes: "cxx20" - - compiler: clang - release: 17 - modes: "cxx20" - - compiler: gcc - release: 13 - modes: "cxx20 cxx23" - - compiler: gcc - release: 14 - modes: "cxx20 cxx23" - - compiler: gcc - release: 15 - modes: "cxx20 cxx23" - - compiler: clang - release: 18 - modes: "cxx20 cxx23" - - compiler: clang - release: 19 - modes: "cxx20 cxx23" - - compiler: clang - release: 20 - modes: "cxx20 cxx23" - container: libfn/ci-build-${{ matrix.env.compiler }}:${{ matrix.env.release }} + - Debug + - Release + compiler: + - "gcc:12" + - "clang:16" + - "clang:17" + - "gcc:13" + - "gcc:14" + - "gcc:15" + - "clang:18" + - "clang:19" + - "clang:20" + exclude: + - mode: cxx23 + compiler: "gcc:12" + - mode: cxx23 + compiler: "clang:16" + - mode: cxx23 + compiler: "clang:17" + container: libfn/ci-build-${{ matrix.compiler }} steps: - uses: actions/checkout@v4 @@ -76,31 +68,18 @@ jobs: COMPILER=$( grep -iE "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -n 's/^[^=]*=//p' ) FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS(_${{ matrix.configuration }})?:STRING" CMakeCache.txt | sed -n 's/^[^=]*=//p' | tr '\n' ' ' ) printf "C++ compiler path: %s\n" "$COMPILER" - $COMPILER --version + "$COMPILER" --version printf "C++ compilation options: %s\n" "$FLAGS" - # Separate step for each compilation mode - - name: Build and test C++20 mode - if: ${{ contains( matrix.env.modes, 'cxx20' ) }} - continue-on-error: true - run: | - cd .build - cmake --build . --target clean - cmake --build . --target cxx20 - ctest -L cxx20 --output-on-failure - - - name: Build and test C++23 mode - if: ${{ contains( matrix.env.modes, 'cxx23' ) }} - continue-on-error: true + - name: Build and test ${{ matrix.mode }} run: | cd .build - cmake --build . --target clean - cmake --build . --target cxx23 - ctest -L cxx23 --output-on-failure + cmake --build . --target ${{ matrix.mode }} + ctest -L ${{ matrix.mode }} --output-on-failure # Build and test all for one arbitrary configuration - name: Build and test all - if: ${{ matrix.env.compiler == 'gcc' && matrix.env.release == '14' }} + if: ${{ matrix.compiler == 'gcc:14' && matrix.mode == 'cxx23' }} run: | cd .build cmake --build . --target clean @@ -108,24 +87,35 @@ jobs: ctest --output-on-failure macos: - runs-on: macos-${{ matrix.env.osver }} + runs-on: macos-${{ matrix.osver }} strategy: fail-fast: false matrix: + mode: + - cxx20 + - cxx23 configuration: - - Debug - - Release - env: - - compiler: appleclang - osver: 15 - modes: "cxx20 cxx23" - - compiler: appleclang + - Debug + - Release + compiler: + - appleclang + - clang + osver: + - 14 + - 15 + clangrelease: + - NA + - 18 + exclude: + - compiler: clang osver: 14 - modes: "cxx20" - compiler: clang - release: 18 - osver: 15 - modes: "cxx20 cxx23" + clangrelease: NA + - compiler: appleclang + clangrelease: 18 + - mode: cxx23 + compiler: appleclang + osver: 14 steps: - uses: actions/checkout@v4 @@ -133,79 +123,71 @@ jobs: run: | mkdir .build cd .build - if [[ "${{ matrix.env.compiler }}" == "appleclang" ]]; then + if [[ "${{ matrix.compiler }}" == "appleclang" ]]; then export CXX="$(which clang++)" export CC="$(which clang)" fi - if [[ "${{ matrix.env.compiler }}" == "clang" ]]; then - export CXX="$(brew --prefix llvm@${{ matrix.env.release }})/bin/clang++" - export CC="$(brew --prefix llvm@${{ matrix.env.release }})/bin/clang" + if [[ "${{ matrix.compiler }}" == "clang" ]]; then + export CXX="$(brew --prefix llvm@${{ matrix.clangrelease }})/bin/clang++" + export CC="$(brew --prefix llvm@${{ matrix.clangrelease }})/bin/clang" fi cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. COMPILER=$( grep -iE "^CMAKE_CXX_COMPILER:FILEPATH=" CMakeCache.txt | sed -n 's/^[^=]*=//p' ) FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS(_${{ matrix.configuration }})?:STRING" CMakeCache.txt | sed -n 's/^[^=]*=//p' | tr '\n' ' ' ) printf "C++ compiler path: %s\n" "$COMPILER" - $COMPILER --version + "$COMPILER" --version printf "C++ compilation options: %s\n" "$FLAGS" - # Separate step for each compilation mode - - name: Build and test C++20 mode - if: ${{ contains( matrix.env.modes, 'cxx20' ) }} - continue-on-error: true + - name: Build and test ${{ matrix.mode }} run: | cd .build - cmake --build . --target clean - cmake --build . --target cxx20 - ctest -L cxx20 --output-on-failure + cmake --build . --target ${{ matrix.mode }} + ctest -L ${{ matrix.mode }} --output-on-failure - - name: Build and test C++23 mode - if: ${{ contains( matrix.env.modes, 'cxx23' ) }} - continue-on-error: true + # Build and test all for one arbitrary configuration + - name: Build and test all + if: ${{ matrix.compiler == 'appleclang' && matrix.osver == '15' && matrix.mode == 'cxx23' }} run: | cd .build cmake --build . --target clean - cmake --build . --target cxx23 - ctest -L cxx23 --output-on-failure + cmake --build . + ctest --output-on-failure windows: - runs-on: windows-${{ matrix.env.osver }} + runs-on: windows-${{ matrix.osver }} strategy: fail-fast: false matrix: + mode: + - cxx20 configuration: - - Debug - - Release - env: - - compiler: "Visual Studio 17 2022" - osver: 2025 - modes: "cxx20" + - Debug + - Release + compiler: + - "Visual Studio 17 2022" + osver: + - 2025 steps: - uses: actions/checkout@v4 - name: Prepare build + shell: bash run: | mkdir .build cd .build - cmake -G "${{ matrix.env.compiler }}" -A x64 .. - - # Separate step for each compilation mode - - name: Build and test C++20 mode - if: ${{ contains( matrix.env.modes, 'cxx20' ) }} - continue-on-error: true - run: | - cd .build - cmake --build . --config ${{ matrix.configuration }} --target clean - cmake --build . --config ${{ matrix.configuration }} --target cxx20 - ctest -L cxx20 -C ${{ matrix.configuration }} --output-on-failure - - - name: Build and test C++23 mode - if: ${{ contains( matrix.env.modes, 'cxx23' ) }} - continue-on-error: true + cmake -G "${{ matrix.compiler }}" -A x64 .. + LINKER=$( grep -iE "^CMAKE_LINKER:FILEPATH=" CMakeCache.txt | sed -n 's/^[^=]*=//p' ) + FLAGS=$( grep -iE "^CMAKE_CXX_FLAGS(_${{ matrix.configuration }})?:STRING" CMakeCache.txt | sed -n 's/^[^=]*=//p' | tr '\n' ' ' ) + printf "C++ linker path: %s\n" "$LINKER" + "$LINKER" . || true + printf "C++ compilation options: %s\n" "$FLAGS" + + - name: Build and test ${{ matrix.mode }} + shell: bash run: | cd .build - cmake --build . --config ${{ matrix.configuration }} --target clean - cmake --build . --config ${{ matrix.configuration }} --target cxx23 - ctest -L cxx23 -C ${{ matrix.configuration }} --output-on-failure + cmake --build . --target ${{ matrix.mode }} --config ${{ matrix.configuration }} + ctest -L ${{ matrix.mode }} -C ${{ matrix.configuration }} --output-on-failure nixos: runs-on: @@ -214,8 +196,8 @@ jobs: fail-fast: false matrix: compiler: - - gcc - - clang + - gcc + - clang steps: - uses: actions/checkout@v4 From e1840f6be7f07bb059d7d640e97f186012f02738 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 14 Sep 2025 16:10:42 +0100 Subject: [PATCH 45/46] Remove noexcept(...) from expected::~expected() --- include/pfn/expected.hpp | 12 +++---- tests/pfn/expected.cpp | 69 ++++++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index fc0d9d5a..968c610c 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -481,24 +481,21 @@ template class expected { constexpr ~expected() noexcept requires(::std::is_trivially_destructible_v && ::std::is_trivially_destructible_v) = default; - constexpr ~expected() // - noexcept(::std::is_nothrow_destructible_v) // extension + constexpr ~expected() // requires(::std::is_trivially_destructible_v && not ::std::is_trivially_destructible_v) { if (not set_) ::std::destroy_at(::std::addressof(e_)); // else T is trivially destructible, no need to do anything } - constexpr ~expected() // - noexcept(::std::is_nothrow_destructible_v) // extension + constexpr ~expected() // requires(not ::std::is_trivially_destructible_v && ::std::is_trivially_destructible_v) { if (set_) ::std::destroy_at(::std::addressof(v_)); // else E is trivially destructible, no need to do anything } - constexpr ~expected() // - noexcept(::std::is_nothrow_destructible_v && ::std::is_nothrow_destructible_v) // extension + constexpr ~expected() // requires(not ::std::is_trivially_destructible_v && not ::std::is_trivially_destructible_v) { if (set_) @@ -1124,8 +1121,7 @@ class expected { constexpr ~expected() noexcept requires(::std::is_trivially_destructible_v) = default; - constexpr ~expected() // - noexcept(::std::is_nothrow_destructible_v) // extension + constexpr ~expected() // requires(not ::std::is_trivially_destructible_v) { if (not set_) diff --git a/tests/pfn/expected.cpp b/tests/pfn/expected.cpp index a490b89c..65e98a4a 100644 --- a/tests/pfn/expected.cpp +++ b/tests/pfn/expected.cpp @@ -478,6 +478,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") using T = expected; static_assert(std::is_default_constructible_v); static_assert(not extension || std::is_nothrow_constructible_v); + static_assert(not extension || std::is_trivially_destructible_v); constexpr T a; static_assert(a.has_value()); @@ -498,6 +499,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") using T = expected; static_assert(std::is_default_constructible_v); static_assert(not extension || std::is_nothrow_constructible_v); + static_assert(not extension || std::is_trivially_destructible_v); constexpr T a; static_assert(a.has_value()); @@ -517,7 +519,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") { using T = expected; static_assert(std::is_default_constructible_v); - static_assert(not extension || not std::is_nothrow_constructible_v); + static_assert(not std::is_nothrow_constructible_v); constexpr T a; static_assert(a.has_value()); @@ -564,7 +566,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") using T = expected; static_assert(std::is_constructible_v>); static_assert(std::is_constructible_v>); - static_assert(not extension || not std::is_nothrow_constructible_v>); + static_assert(not std::is_nothrow_constructible_v>); static_assert(not extension || std::is_nothrow_constructible_v>); constexpr T b(expected(unexpect, Error::unknown)); @@ -587,7 +589,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v const &>); - static_assert(not extension || not std::is_nothrow_constructible_v const &>); + static_assert(not std::is_nothrow_constructible_v const &>); static_assert(not extension || std::is_nothrow_constructible_v const &>); constexpr expected v(5); @@ -615,7 +617,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v); - static_assert(not extension || not std::is_nothrow_constructible_v); + static_assert(not std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); static_assert(not extension || std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); @@ -632,7 +634,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v>); - static_assert(not extension || not std::is_nothrow_constructible_v>); + static_assert(not std::is_nothrow_constructible_v>); static_assert(std::is_constructible_v>); static_assert(not extension || std::is_nothrow_constructible_v>); @@ -658,7 +660,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v); - static_assert(not extension || not std::is_nothrow_constructible_v); + static_assert(not std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); static_assert(not extension || std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); @@ -682,7 +684,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") { using T = expected; static_assert(std::is_constructible_v); - static_assert(not extension || not std::is_nothrow_constructible_v); + static_assert(not std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); static_assert(not extension || std::is_nothrow_constructible_v); static_assert(std::is_constructible_v); @@ -852,7 +854,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(not extension || not std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); // required @@ -880,7 +882,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(not extension || not std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); // required @@ -914,12 +916,11 @@ TEST_CASE("expected non void", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(not extension || not std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); - static_assert(not extension || not std::is_nothrow_destructible_v); constexpr T a(std::in_place); constexpr T b = a; @@ -937,12 +938,11 @@ TEST_CASE("expected non void", "[expected][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(not extension || not std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); - static_assert(not extension || not std::is_nothrow_destructible_v); constexpr T a(unexpect); constexpr T b = a; @@ -1055,7 +1055,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v &&>); + static_assert(not std::is_nothrow_assignable_v &&>); T a(std::in_place, 4); a = unexpected(5); CHECK(a.error().v == 5 * helper::from_rval); @@ -1096,7 +1096,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v &&>); + static_assert(not std::is_nothrow_assignable_v &&>); T a(std::in_place, 4); a = unexpected(5); CHECK(a.error().v == 5 * helper::from_rval); @@ -1180,7 +1180,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); T a(unexpect, Error::file_not_found); a = E(5); CHECK(a.value().v == 5 * helper::from_rval); @@ -1221,7 +1221,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); T a(unexpect, Error::file_not_found); a = C(5); CHECK(a.value().v == 5 * helper::from_rval); @@ -1340,7 +1340,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") SECTION("nothrow move") { using T = expected; - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); { T a(std::in_place, 3); @@ -1362,7 +1362,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v const &>); + static_assert(not std::is_nothrow_assignable_v const &>); T a(std::in_place, 4); unexpected const b(5); a = b; @@ -1385,7 +1385,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") SECTION("throwing") { using T = expected; - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); { T a(std::in_place, 3); @@ -1407,7 +1407,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v const &>); + static_assert(not std::is_nothrow_assignable_v const &>); T a(std::in_place, 4); unexpected const b(5); a = b; @@ -1476,7 +1476,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") SECTION("nothrow move") { using T = expected; - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); { T a(unexpect, Error::file_not_found); @@ -1498,7 +1498,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); T a(unexpect, Error::file_not_found); M const b(5); a = b; @@ -1521,7 +1521,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") SECTION("throwing") { using T = expected; - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); { T a(unexpect, Error::file_not_found); @@ -1543,7 +1543,7 @@ TEST_CASE("expected non void", "[expected][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); T a(unexpect, Error::file_not_found); E const b(5); a = b; @@ -2833,7 +2833,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") { using T = expected; static_assert(std::is_constructible_v>); - static_assert(not extension || not std::is_nothrow_constructible_v>); + static_assert(not std::is_nothrow_constructible_v>); static_assert(std::is_constructible_v>); static_assert(not extension || std::is_nothrow_constructible_v>); @@ -2938,7 +2938,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(not extension || not std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); // required @@ -2972,12 +2972,11 @@ TEST_CASE("expected void", "[expected_void][polyfill]") using T = expected; static_assert(std::is_copy_constructible_v); static_assert(not std::is_trivially_copy_constructible_v); - static_assert(not extension || not std::is_nothrow_copy_constructible_v); + static_assert(not std::is_nothrow_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(not std::is_trivially_move_constructible_v); static_assert(not std::is_nothrow_move_constructible_v); // required static_assert(not std::is_trivially_destructible_v); - static_assert(not extension || not std::is_nothrow_destructible_v); constexpr T a(unexpect); constexpr T b = a; @@ -3072,7 +3071,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v &&>); + static_assert(not std::is_nothrow_assignable_v &&>); T a(std::in_place); a = unexpected(5); CHECK(a.error().v == 5 * helper::from_rval); @@ -3113,7 +3112,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v &&>); + static_assert(not std::is_nothrow_assignable_v &&>); T a(std::in_place); a = unexpected(5); CHECK(a.error().v == 5 * helper::from_rval); @@ -3219,7 +3218,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") SECTION("nothrow move") { using T = expected; - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); { T a(std::in_place); @@ -3241,7 +3240,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v const &>); + static_assert(not std::is_nothrow_assignable_v const &>); T a(std::in_place); unexpected const b(5); a = b; @@ -3264,7 +3263,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") SECTION("throwing") { using T = expected; - static_assert(not extension || not std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); { T a(std::in_place); @@ -3286,7 +3285,7 @@ TEST_CASE("expected void", "[expected_void][polyfill]") } { - static_assert(not extension || not std::is_nothrow_assignable_v const &>); + static_assert(not std::is_nothrow_assignable_v const &>); T a(std::in_place); unexpected const b(5); a = b; From 0e11c86794a05eef016c2a1234a960a65dd476fb Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sun, 21 Sep 2025 16:52:28 +0100 Subject: [PATCH 46/46] Minor --- include/pfn/expected.hpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index 968c610c..1406b8ce 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -365,7 +365,8 @@ template class expected { } constexpr expected(expected const &) = delete; - constexpr expected(expected const &s) // + constexpr expected(expected const &s) // + noexcept(::std::is_nothrow_copy_constructible_v && ::std::is_nothrow_copy_constructible_v) // extension requires(::std::is_copy_constructible_v && ::std::is_copy_constructible_v && ::std::is_trivially_copy_constructible_v && ::std::is_trivially_copy_constructible_v) = default; @@ -885,7 +886,7 @@ template class expected { template requires(not ::std::is_void_v) constexpr friend bool operator==(expected const &x, expected const &y) // - noexcept(noexcept(*x == *y) && noexcept(x.error() == y.error())) // extension + noexcept(noexcept(static_cast(*x == *y)) && noexcept(static_cast(x.error() == y.error()))) // extension requires ( // requires { { *x == *y } -> std::convertible_to; @@ -905,7 +906,7 @@ requires { template requires(not detail::_is_some_expected) constexpr friend bool operator==(expected const &x, const T2 &v) // - noexcept(noexcept(*x == v)) // extension + noexcept(noexcept(static_cast(*x == v))) // extension requires requires { { *x == v } -> std::convertible_to; } @@ -914,7 +915,7 @@ requires { } template constexpr friend bool operator==(expected const &x, unexpected const &e) // - noexcept(noexcept(x.error() == e.error())) // extension + noexcept(noexcept(static_cast(x.error() == e.error()))) // extension requires requires { { x.error() == e.error() } -> std::convertible_to; } @@ -1426,7 +1427,7 @@ class expected { template requires(::std::is_void_v) constexpr friend bool operator==(expected const &x, expected const &y) // - noexcept(noexcept(x.error() == y.error())) // extension + noexcept(noexcept(static_cast((x.error() == y.error())))) // extension requires requires { { x.error() == y.error() } -> std::convertible_to; } @@ -1441,7 +1442,7 @@ class expected { } template constexpr friend bool operator==(expected const &x, unexpected const &e) // - noexcept(noexcept(x.error() == e.error())) // extension + noexcept(noexcept(static_cast(x.error() == e.error()))) // extension requires requires { { x.error() == e.error() } -> std::convertible_to; }