Repository navigation
Add C++23 expected polyfill (#13) - #266
Open
thirtytwobits wants to merge 4 commits into
Open
thirtytwobits wants to merge 4 commits into
thirtytwobits wants to merge 4 commits into
Conversation
libcyphal wants cetl::expected (its types.hpp aliases Expected<S, F> to cetl::variant<S, F> as a stand-in) and embedded code built without exceptions needs a way to return a value or the reason there is none. C++23 standardized that as std::expected; CETL now provides it for C++14 and newer as cetl::pf23::expected, unexpected, unexpect_t/unexpect and bad_expected_access, including expected<void, E> and the monadic operations (and_then, or_else, transform, transform_error). The polyfill follows the current standard, including the defect reports that libraries apply in C++23 mode: LWG 3836 (expected<bool, E> from another expected), LWG 4222 (the value constructor excludes unexpect_t), P3379R0 (the equality operators are constrained rather than mandated), and remove_cv_t<T> as the default template argument of the value constructor and value_or. Assignments that switch between the value and the error give the strong exception guarantee the standard specifies. Copy/move construction and destruction are trivial when T and E are; assignment is too, which the standard leaves unspecified. Without exceptions value() calls std::terminate, as cetl::pf17::optional does. Unchecked access (operator*, operator->, error()) has debug assertions. Known differences, documented on the class: in C++14 the in-place constructors take cetl::pf17::in_place_t (std::in_place_t from C++17 on, which is what cetl::in_place is there), members that need placement new are not constexpr, and transform() needs a movable result because C++14 has no guaranteed copy elision; cv-qualified void is not supported as the value type. cetl/pf23/cetlpf.hpp selects std::expected when __cpp_lib_expected >= 202211L (the monadic operations) and the polyfill otherwise, and cetl/pf26/cetlpf.hpp now includes it so the polyfill headers chain. Tests are typed over the implementation: where the standard library has std::expected every behavioural test also runs against it, which checks the expectations as well as the polyfill. libstdc++ 13 predates LWG 3836, so that one oracle check is skipped where the defect is detected. Coverage of expected.hpp is 99% of lines and 79% of branches (GCC 13, C++14). There are compile-failure tests for the mandates, death tests for the debug assertions and for value() without exceptions, and example 14 for the documentation. Stacked on the issue #39 branch, which adds the C++23/26 presets that exercise the std::expected path in CI. Closes #13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assignment, emplace(), swap() and the constructors that convert from another expected change which member of the storage union is active. C++20 allows that in a constant expression (std::construct_at and constexpr destructors), and std::expected is constexpr throughout, so the polyfill now marks those operations constexpr when the language and library support it (__cpp_constexpr_dynamic_alloc and __cpp_lib_constexpr_dynamic_alloc). Before C++20 they cannot be constant expressions; the class documentation now says "before C++20" rather than "in C++14". A typed test runs every such operation in a constant expression, for a trivial and a non-trivial literal type, so in C++23 builds it is also checked against std::expected. A compile-failure test pins that value() && still requires a copy constructible error type: [expected.object.obs] mandates it for the rvalue overloads too, and libc++ 23 and libstdc++ 15 reject a move-only error type the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The standard returns the comparison itself from the equality operators of expected and unexpected, and the argument itself from error_or, so the conversion applied is the implicit one that the constraints check. The polyfill used static_cast instead, which can pick a different, explicit conversion: a comparison result with a better-matching explicit operator bool gave the opposite answer (or did not compile if that operator was deleted), and error_or could select an explicit constructor of E over the implicit conversion from G. The comparisons now return the comparison result, as LWG 4366 (C++26) specifies for expected and as C++23 already specifies for unexpected, and error_or returns its forwarded argument. libc++ 23 behaves the same way; libstdc++ 13 and 15 still cast in the expected comparisons, so that oracle check is skipped where the cast is detected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[expected.un.swap] mandates is_swappable_v<E> for the member swap and constrains the friend swap on it. libc++ follows that wording; libstdc++ constrains the member swap as well, so code that only asks whether x.swap(y) is callable gets different answers. The polyfill keeps the standard's wording. A typed test now checks, against libc++ and libstdc++ as well as the polyfill, the parts they agree on: the friend swap is constrained, so the swappable traits follow E (including nothrow), and swapping works for swappable error types. A compile-failure test checks that calling the member swap with a non-swappable error does not compile, which holds under either reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thirtytwobits
force-pushed
the
claude/issue-13-expected
branch
from
October 25, 2026 15:45
7eb5d28 to
6906a9a
Compare
This branch has not been deployed
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.
Summary
Adds
cetl::pf23::expected<T, E>(includingexpected<void, E>),unexpected<E>,unexpect_t/unexpectandbad_expected_access<E>for C++14 and newer, pluscetl/pf23/cetlpf.hpp. Closes #13.expected<bool, E>from another expected), LWG 4222 (value ctor excludesunexpect_t), P3379R0 (equality operators are constrained),remove_cv_t<T>default template args.and_then,or_else,transform,transform_errorin all four value categories, using a detail-only C++17INVOKE(callables, member function/data pointers,reference_wrapper). In C++17+transformconstructs its result in place (guaranteed elision).swapthat switch between value and error (reinit-expected), exactly as specified; assignment is deleted unless T or E is nothrow-move-constructible.value()on an error callsstd::terminate(aspf17::optionaldoes). Unchecked access (*,->,error()) hasCETL_DEBUG_ASSERTs (CDE_expected_001–015).cetl/pf23/cetlpf.hppusesstd::expectedwhen__cpp_lib_expected >= 202211L(has the monadic ops), else the polyfill.cetl/pf26/cetlpf.hppnow includes it so the polyfill headers chain.Documented differences (C++14 language limits / scope): in C++14 the in-place ctors take
cetl::pf17::in_place_t(from C++17 onstd::in_place_t, which is whatcetl::in_placeis there); members needing placement new aren'tconstexprin C++14;transformneeds a movable result in C++14; cv-qualifiedvoidvalue type is not supported (plainvoidis).Tests
test_pf23_expected.cpp(type properties, construction, assignment, emplace, swap, exception safety) andtest_pf23_expected_operations.cpp(observers,bad_expected_access, monadic ops, comparison,unexpected,cetlpf.hpp) are typed over the implementation: where the library hasstd::expected, every behavioural test also runs against it, so the expectations are checked against a real standard library too. libstdc++ 13 predates LWG 3836; that one oracle check is skipped only when the defect is detected at runtime.test_pf23_expected_asserts.cpp: death tests for the debug assertions and forvalue()with-fno-exceptions.and_thenreturning a non-expected,transformreturning a reference,unexpected<int>as value type.example_14_expected.cpp) for the docs.Validation
run_unittests+run_examples, GCC 13 C++14 Debug): all 71 executables pass. pf26 suites pass under g++-15 C++26 with the new include chain.expected.hpp(GCC 13, C++14): 99% lines, 79% branches.bad_expected_access<void>(same as the existing one intype_traits_ext.hpp;WARN_AS_ERROR=NO).main, CETLVaSt does not trigger on it automatically; it is dispatched manually on the branch: run 40208489520.🤖 Generated with Claude Code