diff --git a/cmake/clang-22-libstdc++-toolchain.cmake b/cmake/clang-22-libstdc++-toolchain.cmake new file mode 100644 index 0000000..899f233 --- /dev/null +++ b/cmake/clang-22-libstdc++-toolchain.cmake @@ -0,0 +1,10 @@ +# cmake/clang-22-libstdc++-toolchain.cmake -*-cmake-*- +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER clang-22) +set(CMAKE_CXX_COMPILER clang++-22) +set(GCOV_EXECUTABLE "llvm-cov-22 gcov" CACHE STRING "GCOV executable" FORCE) + +include("${CMAKE_CURRENT_LIST_DIR}/clang-libstdc++-flags.cmake") diff --git a/cmake/clang-23-libstdc++-toolchain.cmake b/cmake/clang-23-libstdc++-toolchain.cmake new file mode 100644 index 0000000..df0eb7b --- /dev/null +++ b/cmake/clang-23-libstdc++-toolchain.cmake @@ -0,0 +1,10 @@ +# cmake/clang-23-libstdc++-toolchain.cmake -*-cmake-*- +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER clang-23) +set(CMAKE_CXX_COMPILER clang++-23) +set(GCOV_EXECUTABLE "llvm-cov-23 gcov" CACHE STRING "GCOV executable" FORCE) + +include("${CMAKE_CURRENT_LIST_DIR}/clang-libstdc++-flags.cmake") diff --git a/cmake/clang-23-toolchain.cmake b/cmake/clang-23-toolchain.cmake new file mode 100644 index 0000000..479c036 --- /dev/null +++ b/cmake/clang-23-toolchain.cmake @@ -0,0 +1,10 @@ +# cmake/clang-23-toolchain.cmake -*-cmake-*- +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +include_guard(GLOBAL) + +set(CMAKE_C_COMPILER clang-23) +set(CMAKE_CXX_COMPILER clang++-23) +set(GCOV_EXECUTABLE "llvm-cov-23 gcov" CACHE STRING "GCOV executable" FORCE) + +include("${CMAKE_CURRENT_LIST_DIR}/clang-flags.cmake") diff --git a/cmake/clang-libstdc++-flags.cmake b/cmake/clang-libstdc++-flags.cmake new file mode 100644 index 0000000..243b19c --- /dev/null +++ b/cmake/clang-libstdc++-flags.cmake @@ -0,0 +1,16 @@ +# cmake/clang-libstdc++-flags.cmake -*-cmake-*- +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +include_guard(GLOBAL) + +# The clang flags, built against libstdc++ instead of libc++. CI's clang jobs +# run both standard libraries; the libc++-only local toolchains skip the +# std::expected equivalence gate, which is only built off libc++. +include("${CMAKE_CURRENT_LIST_DIR}/clang-flags.cmake") + +set(CMAKE_CXX_FLAGS + "-stdlib=libstdc++ -Wall -Wextra -std=gnu++20" + CACHE STRING + "CXX_FLAGS" + FORCE +) diff --git a/include/beman/expected/expected.hpp b/include/beman/expected/expected.hpp index d1bfdbb..7bc015f 100644 --- a/include/beman/expected/expected.hpp +++ b/include/beman/expected/expected.hpp @@ -384,6 +384,16 @@ class expected { std::is_trivially_destructible_v && std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v)); + //! \omit + constexpr expected& operator=(const expected&) + requires(!(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && + std::is_trivially_destructible_v && std::is_trivially_copy_constructible_v && + std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v) && + !(std::is_copy_constructible_v && std::is_copy_assignable_v && + (std::is_reference_v || (std::is_copy_constructible_v && std::is_copy_assignable_v)) && + (std::is_nothrow_move_constructible_v || std::is_nothrow_move_constructible_v))) + = delete; + // Move assignment (trivial path) //! \at expected.object.assign //! \effects Trivially moves `rhs`'s active member into `*this`. @@ -639,6 +649,22 @@ class expected { }; }; +// The deleted copy assignment, the `\omit`ted one after the trivial and +// non-trivial paths, is satisfied exactly when neither of them is, so some +// candidate always is. It is here for clang. Clang (17 through at least 24) +// treats a class whose copy-assignment candidates all have unsatisfied +// constraints as not trivially copyable, although none of them is eligible +// ([special]) and [class.prop] counts only eligible ones; gcc gets it right. +// Without it, expected is trivially copyable under gcc and not +// under clang, while libstdc++'s std::expected is under both. A deleted +// candidate is not eligible either, so the triviality it restores is the one +// the standard gives. It is copy assignment only: a satisfied deleted move +// assignment would win overload resolution for an rvalue and stop it falling +// back to copy assignment. expected and expected carry the +// same overload for the same reason. It is not wording, so it is `\omit`ted, +// and a comment on it would be carried into the synopsis, which is why this +// one sits here. + // \rSec3[expected.object.cons]{Constructors} //! \effects Value-initializes `val`. @@ -2025,6 +2051,13 @@ class expected { !(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v)); + //! \omit + constexpr expected& operator=(const expected&) + requires(!(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && + std::is_trivially_destructible_v) && + !(std::is_reference_v || (std::is_copy_constructible_v && std::is_copy_assignable_v))) + = delete; + // Move assignment (trivial path) //! \at expected.void.assign //! \merge @@ -3363,6 +3396,13 @@ class expected { !(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && std::is_trivially_destructible_v)); + //! \omit + constexpr expected& operator=(const expected&) + requires(!(std::is_trivially_copy_constructible_v && std::is_trivially_copy_assignable_v && + std::is_trivially_destructible_v) && + !(std::is_reference_v || (std::is_copy_constructible_v && std::is_copy_assignable_v))) + = delete; + // Move assignment (trivial path) //! \at expected.ref.assign //! \effects If `rhs.has_value()` is `true`: if `has_value()` is `true`, diff --git a/tests/beman/expected/expected_std_equivalence.test.cpp b/tests/beman/expected/expected_std_equivalence.test.cpp index 9711163..7467548 100644 --- a/tests/beman/expected/expected_std_equivalence.test.cpp +++ b/tests/beman/expected/expected_std_equivalence.test.cpp @@ -91,24 +91,6 @@ struct NoexceptNonTrivial { NoexceptNonTrivial& operator=(NoexceptNonTrivial&&) noexcept { return *this; } }; -// Immovable is fully non-copyable and non-movable (all four special members -// deleted). Whether a class template's copy/move assignment operator, when -// declared only via two constraint-guarded candidates that both fail to be -// satisfied for a given instantiation, still triggers implicit fallback -// generation -- and whether that fallback is trivial-or-deleted -- is a -// corner of the conditionally-trivial special member function rules -// ([class.mem]/[dcl.fct.def.default]) where GCC and Clang currently disagree -// for this one pathological error type. It predates and is independent of -// this change (which only re-keys the *conditions* from unexpected to E; -// it does not alter the constrained-candidate declaration technique). Skip -// the "beman is at least as trivial as std" checks for Immovable only, so the -// gate does not flake on a pre-existing toolchain quirk unrelated to what it -// is checking. -template -inline constexpr bool skip_trivial_parity = false; -template <> -inline constexpr bool skip_trivial_parity = true; - // Every trait parity for one (E, T) pair, each one reported. `Bm` and `St` are // named in the failure output so a divergence says which pair of types it was // found on; the trait's own name is carried by the CHECK expression and by the @@ -149,12 +131,10 @@ void check_parity() { // Drive-by fix: beman is at least as trivially copyable/assignable as std. // Each is an implication, so it is one bool rather than a comparison; the // extra parentheses keep Catch2 from trying to decompose the `||`. - if constexpr (!skip_trivial_parity) { - INFO("beman is at least as trivial as std"); - CHECK((!std::is_trivially_copy_assignable_v || std::is_trivially_copy_assignable_v)); - CHECK((!std::is_trivially_move_assignable_v || std::is_trivially_move_assignable_v)); - CHECK((!std::is_trivially_copyable_v || std::is_trivially_copyable_v)); - } + INFO("beman is at least as trivial as std"); + CHECK((!std::is_trivially_copy_assignable_v || std::is_trivially_copy_assignable_v)); + CHECK((!std::is_trivially_move_assignable_v || std::is_trivially_move_assignable_v)); + CHECK((!std::is_trivially_copyable_v || std::is_trivially_copyable_v)); } } // namespace diff --git a/tests/beman/expected/expected_trivial.test.cpp b/tests/beman/expected/expected_trivial.test.cpp index ce85ec3..e088f39 100644 --- a/tests/beman/expected/expected_trivial.test.cpp +++ b/tests/beman/expected/expected_trivial.test.cpp @@ -87,3 +87,42 @@ TEST_CASE("trivial SMFs: expected is trivially copyable", "[trivial]") CHECK(!b.has_value()); CHECK(b.error() == 7); } + +// --------------------------------------------------------------------------- +// A trivially movable, non-copyable E: no copy assignment is eligible +// --------------------------------------------------------------------------- + +namespace { +struct MoveOnly { + MoveOnly() = default; + MoveOnly(const MoveOnly&) = delete; + MoveOnly(MoveOnly&&) = default; + MoveOnly& operator=(const MoveOnly&) = delete; + MoveOnly& operator=(MoveOnly&&) = default; +}; +} // namespace + +// Neither the trivial nor the non-trivial copy assignment is satisfied for a +// move-only E. The class is still trivially copyable -- [class.prop] counts +// only eligible members -- but clang said otherwise until a deleted candidate +// was added for this case (see the note after the primary template). +TEST_CASE("trivial SMFs: a trivially movable, non-copyable E stays trivially copyable", "[trivial]") { + int i = 0; + CHECK(std::is_trivially_copyable_v>); + CHECK(std::is_trivially_copyable_v>); + CHECK(std::is_trivially_copyable_v>); + CHECK_FALSE(std::is_copy_assignable_v>); + CHECK_FALSE(std::is_copy_assignable_v>); + CHECK_FALSE(std::is_copy_assignable_v>); + + expected a(42); + expected b(unexpect); + b = std::move(a); + CHECK(b.has_value()); + CHECK(*b == 42); + + expected r(i); + expected s(unexpect); + s = std::move(r); + CHECK(&*s == &i); +}