Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmake/clang-22-libstdc++-toolchain.cmake
Original file line number Diff line number Diff line change
@@ -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")
10 changes: 10 additions & 0 deletions cmake/clang-23-libstdc++-toolchain.cmake
Original file line number Diff line number Diff line change
@@ -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")
10 changes: 10 additions & 0 deletions cmake/clang-23-toolchain.cmake
Original file line number Diff line number Diff line change
@@ -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")
16 changes: 16 additions & 0 deletions cmake/clang-libstdc++-flags.cmake
Original file line number Diff line number Diff line change
@@ -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
)
40 changes: 40 additions & 0 deletions include/beman/expected/expected.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,16 @@ class expected {
std::is_trivially_destructible_v<T> && std::is_trivially_copy_constructible_v<E> &&
std::is_trivially_copy_assignable_v<E> && std::is_trivially_destructible_v<E>));

//! \omit
constexpr expected& operator=(const expected&)
requires(!(std::is_trivially_copy_constructible_v<T> && std::is_trivially_copy_assignable_v<T> &&
std::is_trivially_destructible_v<T> && std::is_trivially_copy_constructible_v<E> &&
std::is_trivially_copy_assignable_v<E> && std::is_trivially_destructible_v<E>) &&
!(std::is_copy_constructible_v<T> && std::is_copy_assignable_v<T> &&
(std::is_reference_v<E> || (std::is_copy_constructible_v<E> && std::is_copy_assignable_v<E>)) &&
(std::is_nothrow_move_constructible_v<T> || std::is_nothrow_move_constructible_v<E>)))
= delete;

// Move assignment (trivial path)
//! \at expected.object.assign
//! \effects Trivially moves `rhs`'s active member into `*this`.
Expand Down Expand Up @@ -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<int, MoveOnly> 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<void, E> and expected<T&, E> 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`.
Expand Down Expand Up @@ -2025,6 +2051,13 @@ class expected<void, E> {
!(std::is_trivially_copy_constructible_v<E> && std::is_trivially_copy_assignable_v<E> &&
std::is_trivially_destructible_v<E>));

//! \omit
constexpr expected& operator=(const expected&)
requires(!(std::is_trivially_copy_constructible_v<E> && std::is_trivially_copy_assignable_v<E> &&
std::is_trivially_destructible_v<E>) &&
!(std::is_reference_v<E> || (std::is_copy_constructible_v<E> && std::is_copy_assignable_v<E>)))
= delete;

// Move assignment (trivial path)
//! \at expected.void.assign
//! \merge
Expand Down Expand Up @@ -3363,6 +3396,13 @@ class expected<T&, E> {
!(std::is_trivially_copy_constructible_v<E> && std::is_trivially_copy_assignable_v<E> &&
std::is_trivially_destructible_v<E>));

//! \omit
constexpr expected& operator=(const expected&)
requires(!(std::is_trivially_copy_constructible_v<E> && std::is_trivially_copy_assignable_v<E> &&
std::is_trivially_destructible_v<E>) &&
!(std::is_reference_v<E> || (std::is_copy_constructible_v<E> && std::is_copy_assignable_v<E>)))
= delete;

// Move assignment (trivial path)
//! \at expected.ref.assign
//! \effects If `rhs.has_value()` is `true`: if `has_value()` is `true`,
Expand Down
28 changes: 4 additions & 24 deletions tests/beman/expected/expected_std_equivalence.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<E> 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 <class E>
inline constexpr bool skip_trivial_parity = false;
template <>
inline constexpr bool skip_trivial_parity<Immovable> = 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
Expand Down Expand Up @@ -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<E>) {
INFO("beman is at least as trivial as std");
CHECK((!std::is_trivially_copy_assignable_v<St> || std::is_trivially_copy_assignable_v<Bm>));
CHECK((!std::is_trivially_move_assignable_v<St> || std::is_trivially_move_assignable_v<Bm>));
CHECK((!std::is_trivially_copyable_v<St> || std::is_trivially_copyable_v<Bm>));
}
INFO("beman is at least as trivial as std");
CHECK((!std::is_trivially_copy_assignable_v<St> || std::is_trivially_copy_assignable_v<Bm>));
CHECK((!std::is_trivially_move_assignable_v<St> || std::is_trivially_move_assignable_v<Bm>));
CHECK((!std::is_trivially_copyable_v<St> || std::is_trivially_copyable_v<Bm>));
}

} // namespace
Expand Down
39 changes: 39 additions & 0 deletions tests/beman/expected/expected_trivial.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,42 @@ TEST_CASE("trivial SMFs: expected<void,int> 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<expected<int, MoveOnly>>);
CHECK(std::is_trivially_copyable_v<expected<void, MoveOnly>>);
CHECK(std::is_trivially_copyable_v<expected<int&, MoveOnly>>);
CHECK_FALSE(std::is_copy_assignable_v<expected<int, MoveOnly>>);
CHECK_FALSE(std::is_copy_assignable_v<expected<void, MoveOnly>>);
CHECK_FALSE(std::is_copy_assignable_v<expected<int&, MoveOnly>>);

expected<int, MoveOnly> a(42);
expected<int, MoveOnly> b(unexpect);
b = std::move(a);
CHECK(b.has_value());
CHECK(*b == 42);

expected<int&, MoveOnly> r(i);
expected<int&, MoveOnly> s(unexpect);
s = std::move(r);
CHECK(&*s == &i);
}
Loading