Repository navigation
Keep expected trivially copyable under clang when no copy assignment is eligible - #100
Merged
steve-downey merged 2 commits intoOct 18, 2026
Merged
Conversation
Every local clang toolchain forces -stdlib=libc++, and the std::expected equivalence gate is skipped on libc++, so no local build ran that gate with clang. CI's clang jobs run libstdc++ too, and that is where they fail. clang-22-libstdc++ and clang-23-libstdc++ share clang-libstdc++-flags.cmake, which takes clang-flags.cmake and swaps only the standard library. Also add a plain clang-23 toolchain alongside clang-22.
…is eligible For a move-only E, neither the trivial nor the non-trivial copy assignment of expected is satisfied. [class.prop] counts only eligible special members, so expected<int, MoveOnly> is trivially copyable, and gcc says so. Clang (17 through at least 24) does not: a class whose copy-assignment candidates all have unsatisfied constraints is, to clang, not trivially copyable. libstdc++ 16 made std::expected<int, MoveOnly> trivially copyable, so the equivalence gate's "beman is at least as trivial as std" check now fails in every clang job built against libstdc++ in CI. Add a deleted copy assignment, satisfied exactly when neither path is, to the primary template, expected<void, E> and expected<T&, E>. A deleted candidate is not eligible either, so the triviality it restores is the standard's. It is `\omit`ted from the wording; the rationale sits after the primary template because specgen carries a plain comment on an omitted member into the synopsis. Move assignment is left alone: a satisfied deleted move assignment would stop an rvalue falling back to copy assignment. The same bug was behind the equivalence gate's Immovable skip, which is removed. expected_trivial gains a direct check, which runs on libc++ as well.
steve-downey
added a commit
that referenced
this pull request
Oct 18, 2026
…nches (#101) Supersedes #80 and #82, rebuilt on current `main`. Both had fallen behind the switch from `static_assert` to runtime `CHECK`s and the conversion tests added since. ### What's here - **Remaining unexercised members** (from #80). Tests for the out-of-line definitions no test reached. Most are `const&` overloads shadowed by their `&&` twins: `const unexpected<G>&` construction and assignment, converting move constructors, `error_or() &&`, the `const&` monadic overloads of `expected<void, E>` and `expected<T&, E>`, the ref-qualified `error()` overloads of `expected<T&, E>`, its `unexpect_t` + `initializer_list` constructor, and `bad_expected_access<void>::what()`. Also excludes `BEMAN_EXPECTED_TRAP()` lines from the gcovr report: reaching one kills the process before libgcov flushes, so they can't be covered by construction. - **Reference specializations in the hardened build** (from #82). `expected_hardened.test.cpp` never instantiated `expected<T&, E>` or any reference `E`, so 6 of the 21 trap sites were never compiled. It now covers all four reference shapes. - **`expected<void, E&>` conversion tests that didn't convert.** Two tests named for the `expected<void, G&>` converting constructors used the same type on both sides, so they ran the copy and move constructors instead. Their destinations now bind `const int&`. - **Copy assignment with a non-trivial `E`.** The existing copy-assignment tests for `expected<void, E>` and `expected<T&, E>` all use `int`, which takes the trivial path, so the non-trivial operator's mixed-state branches never ran. These tests use `std::string`. Where the PRs had `static_assert`s on types or traits, they're now `CHECK`s, matching the rest of the suite. The one exception asserts the `initializer_list` constructor under constant evaluation, which is the thing being tested. ### Results - gcc-16: 1248/1248 tests pass (1178 on `main`). - Every instrumented line of `expected.hpp` (786/786) and `unexpected.hpp` (28/28) now runs, not counting trap lines. On `main`, 18 instrumented lines of `expected.hpp` never ran. These figures come from gcovr's JSON output, without the coveralls line counting. - clang-22 and clang-23 against libstdc++ 16: all pass except `beman::expected type traits match std::expected`. That failure is already on `main`, in a file this PR doesn't touch, and #100 fixes it.
steve-downey
added a commit
that referenced
this pull request
Oct 19, 2026
…id, E> (#104) gcc's `-fsanitize=address` build reports `-Wmaybe-uninitialized` on reads of `unex_` in value-state `expected<void, E>` objects. Every report is a false positive, since `has_val_` guards each read exactly. They were 43 of the 44 warnings in the default Asan build with gcc 15. - **Fix at the source.** Give the union an empty `struct {} void_;` alternative and initialize it in every constructor that can end in the value state, as libstdc++'s `expected<void, E>` does. The member is unmarked, so specgen leaves it out of the synopsis. Its rationale sits after the class body, because a comment inside the union would be carried into the synopsis. - **Move the remaining suppression.** The gcc-14/15/16 toolchain files each added `-Wno-maybe-uninitialized` to the Asan flags, but the default toolchain and gcc-12/13 did not. A plain `make` warned 44 times where `make TOOLCHAIN=gcc-16` was clean. The suppression isn't version-gated, so it now lives in `gcc-flags.cmake`. It's still needed for `expected<T&, E>`, whose value constructor stores a runtime address, so the same folding can't apply. gcc-16: 1249/1249 tests pass. The generated wording is unchanged. I checked that by regenerating it with specgen 0.4.0 on top of this branch. Earlier specgen releases could leak `void_;` into the `expected<void, E>` synopsis once this member sat beside the `\omit`ted copy assignment from #100 (steve-downey/specgen#147), so regenerate with 0.4.0 or later.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every clang job built against libstdc++ fails
beman::expected type traits match std::expectedonmain, for clang 19 through 22 and every C++ mode. The gcc jobs and the clang jobs on libc++ pass. The failing check isis_trivially_copyableforexpected<int, MoveOnly>andexpected<void, MoveOnly>(and the same forNoexceptMoveOnly).Cause
Two things combine.
libstdc++ 16 made
std::expected<int, MoveOnly>trivially copyable. The test checks "beman is at least as trivial as std", which is an implication, so until the CI images moved to libstdc++ 16 there was nothing to check.Clang gets
beman::expected<int, MoveOnly>wrong. For a move-only E, neither the trivial nor the non-trivial copy assignment is satisfied. [class.prop] counts only eligible special members, so the class is trivially copyable, and gcc agrees. Clang (17 through at least 24) treats a class whose copy-assignment candidates all have unsatisfied constraints as not trivially copyable:Fix
The primary template,
expected<void, E>andexpected<T&, E>each get a deleted copy assignment that is satisfied exactly when neither existing path is. A deleted candidate is not eligible either, so the triviality it restores is the one the standard gives, andis_copy_assignableis unchanged. Move assignment is left alone: a satisfied deleted move assignment would stop an rvalue falling back to copy assignment for an E with a deleted move.The new overload is
\omitted from the generated wording, and its rationale sits after the primary template rather than on the declaration, because a comment on an omitted member is still carried into the synopsis.The equivalence test's skip for
Immovablewas covering the same clang bug, so it is removed.expected_trivial.test.cppgains a direct check thatexpected<int|void|int&, MoveOnly>is trivially copyable and not copy-assignable; unlike the equivalence test, it also runs on libc++.Toolchains
The local
clang-Ntoolchains all use libc++, where the equivalence test is skipped, so this failure could not be reproduced with them. This addsclang-22-libstdc++andclang-23-libstdc++(make TOOLCHAIN=clang-22-libstdc++), which sharecmake/clang-libstdc++-flags.cmake, plus a plainclang-23toolchain.Testing
make TOOLCHAIN=clang-22-libstdc++: fails before the fix with the same four CI failures; passes after, 1179/1179.make TOOLCHAIN=clang-23-libstdc++andmake TOOLCHAIN=gcc-16: 1179/1179.Wording
Adding a member to
expected<void, E>makes specgen emit thevoid_alternative into the synopsis, because of a tie in how it orders edits: steve-downey/specgen#147. The checked-in wording is not regenerated here.