util: Add some more Unexpected and Expected methods #34032

pull maflcko wants to merge 8 commits into bitcoin:master from maflcko:2512-exp changing 7 files +146 −40
  1. maflcko commented at 8:01 am on December 9, 2025: member

    Reviewers requested more member functions In #34006.

    They are currently unused, but bring the port closer to the original std::expected implementation:

    • Make Expected::value() throw when no value exists
    • Add Unexpected::error() methods
    • Add Expected<void, E> specialization
    • Add Expected::value()&& and Expected::error()&& methods
    • Add Expected::swap()

    Also, include a tiny tidy fixup:

    • tidy: Set AllowCastToVoid in the bugprone-unused-return-value check
  2. DrahtBot added the label Utils/log/libs on Dec 9, 2025
  3. DrahtBot commented at 8:01 am on December 9, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34032.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK hodlinator
    Stale ACK ryanofsky

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

  4. in src/util/expected.h:84 in fa4794dfeb outdated
    81     }
    82     template <class U>
    83-    ValueType value_or(U&& default_value) &&
    84+    T value_or(U&& default_value) &&
    85     {
    86         return has_value() ? std::move(value()) : std::forward<U>(default_value);
    


    ryanofsky commented at 1:06 pm on December 9, 2025:
    Would seem good to change std::move(value()) to value() since it should already be an rvalue, and this would make the value_or implementation more consistent

    maflcko commented at 1:58 pm on December 9, 2025:
    I kept it, because it is a bit more consistent with https://en.cppreference.com/w/cpp/utility/expected/value_or, also it should be harmless to cast twice and even clarify it a bit more that the value really is moved out.
  5. ryanofsky approved
  6. ryanofsky commented at 1:19 pm on December 9, 2025: contributor

    Code review ACK fa20a50f46307b45a82a6e4283a945ef2f9f92a2. Thanks for the followups.

    I’d probably replace std::variant<std::monostate, E> with std::optional<E> but both seem reasonable.

  7. maflcko commented at 2:00 pm on December 9, 2025: member

    I’d probably replace std::variant<std::monostate, E> with std::optional<E> but both seem reasonable.

    I kept the variant, because it is more consistent and shows that the error() methods are just 1:1 copy-pasted between both templates.

  8. maflcko commented at 4:38 pm on December 9, 2025: member

    pushed a commit, so that the two compile and behave identically:

    0    const auto moved{*std::move(no_copy)};
    1    const auto moved{std::move(*no_copy)};
    

    Sorry for missing it earlier, but I think now all && methods are implemented completely and correctly.

  9. in src/test/util_expected_tests.cpp:31 in fabef9bbef


    hodlinator commented at 9:11 am on December 10, 2025:

    nit: Could use BOOST_CHECK_EQUAL when applicable:

    0    BOOST_CHECK_EQUAL(e.value().x, 42);
    

    maflcko commented at 4:31 pm on December 10, 2025:
    thx, done
  10. in src/util/expected.h:31 in fabef9bbef
    26+    constexpr const E& error() const& LIFETIMEBOUND { return err; }
    27+    constexpr E& error() & LIFETIMEBOUND { return err; }
    28+    constexpr E&& error() && LIFETIMEBOUND { return std::move(err); }
    29+
    30+private:
    31     E err;
    


    hodlinator commented at 3:22 pm on December 10, 2025:
    nit: Could rename err -> m_err for consistency with Expected::m_data.

    maflcko commented at 4:31 pm on December 10, 2025:
    thx, done
  11. in src/util/expected.h:105 in fabef9bbef outdated
    132+
    133+template <class E>
    134+class Expected<void, E>
    135+{
    136+private:
    137+    std::variant<std::monostate, E> m_data;
    


    hodlinator commented at 3:30 pm on December 10, 2025:

    nit: Could use optional instead of monostate? Ah, I see that was already discussed #34032 (comment).

     0--- a/src/util/expected.h
     1+++ b/src/util/expected.h
     2@@ -10,6 +10,7 @@
     3 
     4 #include <cassert>
     5 #include <exception>
     6+#include <optional>
     7 #include <utility>
     8 #include <variant>
     9 
    10@@ -119,16 +120,16 @@ template <class E>
    11 class Expected<void, E>
    12 {
    13 private:
    14-    std::variant<std::monostate, E> m_data;
    15+    std::optional<E> m_error;
    16 
    17 public:
    18-    constexpr Expected() : m_data{std::in_place_index<0>, std::monostate{}} {}
    19+    constexpr Expected() = default;
    20     template <class Err>
    21-    constexpr Expected(Unexpected<Err> u) : m_data{std::in_place_index<1>, std::move(u).error()}
    22+    constexpr Expected(Unexpected<Err> u) : m_error{std::move(u).error()}
    23     {
    24     }
    25 
    26-    constexpr bool has_value() const noexcept { return m_data.index() == 0; }
    27+    constexpr bool has_value() const noexcept { return !m_error.has_value(); }
    28     constexpr explicit operator bool() const noexcept { return has_value(); }
    29 
    30     constexpr void value() const
    31@@ -141,17 +142,17 @@ public:
    32     constexpr const E& error() const& LIFETIMEBOUND
    33     {
    34         assert(!has_value());
    35-        return std::get<1>(m_data);
    36+        return m_error.value();
    37     }
    38     constexpr E& error() & LIFETIMEBOUND
    39     {
    40         assert(!has_value());
    41-        return std::get<1>(m_data);
    42+        return m_error.value();
    43     }
    44     constexpr E&& error() && LIFETIMEBOUND
    45     {
    46         assert(!has_value());
    47-        return std::get<1>(std::move(m_data));
    48+        return std::move(m_error).value();
    49     }
    50 };
    
  12. hodlinator approved
  13. hodlinator commented at 3:42 pm on December 10, 2025: contributor

    ACK fabef9bbef255796f28c04c5b478df9544c749eb

    (Caveat: Not yet fully at ease with ampersands after the method parameters such as in the last two commits, example: constexpr E&& error() &&).

  14. DrahtBot requested review from ryanofsky on Dec 10, 2025
  15. maflcko force-pushed on Dec 10, 2025
  16. in src/util/expected.h:58 in fa0297540e outdated
    54@@ -49,12 +55,16 @@ class Expected
    55 
    56     constexpr const ValueType& value() const LIFETIMEBOUND
    57     {
    58-        assert(has_value());
    59+        if (!Assume(has_value())) {
    


    ryanofsky commented at 6:03 pm on December 10, 2025:

    In commit “util: Make Expected::value() throw” (fa0297540e29f077a76133d1b60d3fd35f03b4e1)

    In the standard std::expected class there is a clear difference between the high-level value() method which throws an exception when the value is unset, and lower level pointer operators which are noexcept and can’t be called without a value. This makes std::expected consistent with std::optional, which also has noexcept pointer operators and a throwing value() method. It’s also similar to std::vector which has nonthrowing operator[] methods and throwing at() methods.

    I don’t see a reason why util::Expected class shouldn’t follow the same pattern and have noexcept pointer operators, especially if it’s intended to be replaced with std::expected in the future. I also think it would clean up the implementation to have a separation between low level pointer operators and higher level accessor methods. Would suggest:

     0template <class T, class E>
     1class Expected
     2{
     3private:
     4    std::variant<T, E> m_data;
     5
     6public:
     7    // Value constructors.
     8    constexpr Expected() = default;
     9    constexpr Expected(T v) : m_data{std::in_place_index<0>, std::move(v)} {}
    10
    11    // Error constructor.
    12    template <class G>
    13    constexpr Expected(Unexpected<G> u) : m_data{std::in_place_index<1>, std::move(u).error()} {}
    14
    15    // Value accessors.
    16    constexpr explicit operator bool() const noexcept { return m_data.index() == 0; }
    17    constexpr const T& operator*() const& noexcept LIFETIMEBOUND { return *Assert(std::get_if<0>(&m_data)); }
    18    constexpr T&       operator*() &      noexcept LIFETIMEBOUND { return *Assert(std::get_if<0>(&m_data)); }
    19    constexpr T&&      operator*() &&     noexcept LIFETIMEBOUND { return std::move(**this); }
    20    constexpr const T* operator->() const noexcept LIFETIMEBOUND { return Assert(std::get_if<0>(&m_data)); }
    21    constexpr T*       operator->()       noexcept LIFETIMEBOUND { return Assert(std::get_if<0>(&m_data)); }
    22
    23    // Error accessors.
    24    constexpr const E& error() const& LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); }
    25    constexpr E& error() & LIFETIMEBOUND            { return *Assert(std::get_if<1>(&m_data)); }
    26    constexpr E&& error() && LIFETIMEBOUND          { return std::move(this->error()); }
    27
    28    // Higher level helpers.
    29    constexpr bool has_value() const noexcept       { return bool{*this}; }
    30    constexpr const T& value() const& LIFETIMEBOUND { if (!*this) throw BadExpectedAccess{}; return **this; }
    31    constexpr T& value() & LIFETIMEBOUND            { if (!*this) throw BadExpectedAccess{}; return **this; }
    32    constexpr T&& value() && LIFETIMEBOUND          { return std::move(this->value()); }
    33
    34    template <class U>
    35    T value_or(U&& default_value) const&
    36    {
    37        return *this ? **this : std::forward<U>(default_value);
    38    }
    39
    40    template <class U>
    41    T value_or(U&& default_value) &&
    42    {
    43        return *this ? std::move(**this) : std::forward<U>(default_value);
    44    }
    45};
    

    This implementation would also make the value() methods throw in debug builds instead of aborting, which I think is good because std::expected does that, and there isn’t much reason to call value() if exceptions are unwanted. But no strong opinion about this aspect, and this could be easily tweaked to make value() abort in debug builds by replacing if (!*this) with if (!Assume(*this)) in the two value() methods.


    hodlinator commented at 8:42 am on December 11, 2025:

    Agree on change to use the default ctor, variant will initialize with the first type (https://en.cppreference.com/w/cpp/utility/variant/variant.html, 1).

    I prefer the current way of implementing operator bool in terms of has_value() though, think it’s clearer.

    Agree on making the other operators noexcept.


    maflcko commented at 9:28 am on December 11, 2025:

    Agree on making the other operators noexcept.

    thx, done

    Agree on change to use the default ctor, variant will initialize with the first type (https://en.cppreference.com/w/cpp/utility/variant/variant.html, 1).

    shouldn’t hurt to be explicit

  17. in src/util/expected.h:32 in faa0d23ee7 outdated
    29+    constexpr const E& error() const& LIFETIMEBOUND { return m_err; }
    30+    constexpr E& error() & LIFETIMEBOUND { return m_err; }
    31+    constexpr E&& error() && LIFETIMEBOUND { return std::move(m_err); }
    32+
    33+private:
    34+    E m_err;
    


    ryanofsky commented at 6:07 pm on December 10, 2025:

    In commit “util: Add Unexpected::error()” (faa0d23ee75aac289c3766ce6ee27948b0d982d9)

    Calling this m_error would seem more consistent.


    maflcko commented at 9:28 am on December 11, 2025:
    thx, done
  18. in src/util/expected.h:102 in fa3d6c760d outdated
    104+    constexpr T* operator->() LIFETIMEBOUND { return &value(); }
    105+    constexpr const T* operator->() const LIFETIMEBOUND { return &value(); }
    106+};
    107+
    108+template <class E>
    109+class Expected<void, E>
    


    ryanofsky commented at 6:27 pm on December 10, 2025:

    In commit “util: Add Expected<void, E> specialization” (fa3d6c760db3dafbaa530401893a40fc5f6575f9)

    This specialization is more complicated than it needs to be. Would suggest simplifying with inheritance:

     0//! Specialization for void returning void from value methods.
     1template <class E>
     2class Expected<void, E> : public Expected<std::monostate, E>
     3{
     4    using Base = Expected<std::monostate, E>;
     5public:
     6    using Expected<std::monostate, E>::Expected;
     7    constexpr void operator*() const noexcept { Base::operator*(); }
     8    constexpr void value() const& { Base::value(); }
     9    constexpr void value() && { Base::value(); }
    10    void value_or() = delete;
    11};
    

    maflcko commented at 9:27 am on December 11, 2025:

    This just brings back the problem we are trying to solve: Solve exposing monostate externally?

    + Expected<void, std::string> e{std::monostate{}};

  19. ryanofsky approved
  20. ryanofsky commented at 6:33 pm on December 10, 2025: contributor
    Code review ACK fa0a1a6a4986c1a977bbcd5f48834bedeac5dffb. Left some suggestions which I think would make this better in significant ways, but I do this is already an improvement in its current form.
  21. DrahtBot requested review from hodlinator on Dec 10, 2025
  22. Set bugprone-unused-return-value.AllowCastToVoid
    It only makes sense to turn this off with C++26, which introduces the _
    placeholder.
    fad4a9fe2b
  23. test: refactor: Use BOOST_CHECK_EQUAL over BOOST_CHECK == faa109f8be
  24. in src/util/expected.h:128 in fa0a1a6a49 outdated
    167+    constexpr E&& error() && LIFETIMEBOUND
    168+    {
    169+        assert(!has_value());
    170+        return std::get<1>(std::move(m_data));
    171+    }
    172 };
    


    hodlinator commented at 8:52 am on December 11, 2025:

    nit: Would be nice to add swap() as well. I already needed that here: #25665 (review) / https://github.com/hodlinator/bitcoin/commit/b40a36cbbab59440d84dba6d1cc16bce17d4869c

    (I forgot to include the test in that commit).

    0BOOST_AUTO_TEST_CASE(expected_swap)
    1{
    2    Expected<const char*, int> a{Unexpected{55}};
    3    Expected<const char*, int> b{"21"};
    4    a.swap(b);
    5    BOOST_CHECK_EQUAL(a.value(), "21");
    6    BOOST_CHECK_EQUAL(b.error(), 55);
    7}
    

    maflcko commented at 9:48 am on December 11, 2025:
    thx, done
  25. in src/test/util_expected_tests.cpp:29 in fa0a1a6a49 outdated
    21@@ -17,25 +22,39 @@ BOOST_AUTO_TEST_CASE(expected_value)
    22         int x;
    23     };
    24     Expected<Obj, int> e{};
    25-    BOOST_CHECK(e.value().x == 0);
    26+    BOOST_CHECK_EQUAL(e.value().x, 0);
    27 
    28     e = Obj{42};
    29 
    30     BOOST_CHECK(e.has_value());
    


    hodlinator commented at 9:05 am on December 11, 2025:

    nit: Should this be a require since otherwise we would start triggering internal asserts or exceptions further down on failure?

    0    BOOST_REQUIRE(e.has_value());
    

    maflcko commented at 9:49 am on December 11, 2025:
    Yes, but that seems fine? Not sure what problem this would be solving. Leaving as-is for now.

    hodlinator commented at 10:53 am on December 11, 2025:
    I think it’s slightly better as a general principle, to make test failures more intelligible. But it’s very unlikely that anyone would even locally tweak the code being tested here so that it fails.
  26. in src/util/expected.h:28 in fa0a1a6a49
    25-    E err;
    26+    constexpr explicit Unexpected(E e) : m_err(std::move(e)) {}
    27+
    28+    constexpr const E& error() const& LIFETIMEBOUND { return m_err; }
    29+    constexpr E& error() & LIFETIMEBOUND { return m_err; }
    30+    constexpr E&& error() && LIFETIMEBOUND { return std::move(m_err); }
    


    hodlinator commented at 9:08 am on December 11, 2025:

    maflcko commented at 9:29 am on December 11, 2025:
    thx, done
  27. hodlinator commented at 9:10 am on December 11, 2025: contributor
    re-ACK fa0a1a6a4986c1a977bbcd5f48834bedeac5dffb
  28. util: Add Unexpected::error()
    This is not needed, but a bit closer to the std lib.
    fa1de1103f
  29. util: Make Expected::value() throw
    This is not expected to be needed in this codebase, but brings the
    implementation closer to std::expected::value().
    fa5a49c988
  30. maflcko force-pushed on Dec 11, 2025
  31. util: Add Expected<void, E> specialization
    This is not needed, but a bit closer to the std lib, because
    std::monostate is no longer leaked through ValueType from the value()
    method.
    fa696c4ae8
  32. util: Implement Expected::value()&& and Expected::error()&&
    They are currently unused, but implementing them is closer to the
    std::expected.
    fa71c13f11
  33. util: Implement Expected::operator*()&&
    It is currently unused, but implementing it is closer to std::expected.
    fa9aad738a
  34. util: Add Expected::swap() fa874c9f05
  35. maflcko force-pushed on Dec 11, 2025
  36. DrahtBot added the label CI failed on Dec 11, 2025
  37. in src/util/expected.h:89 in fa874c9f05
     96     }
     97 
     98-    constexpr const E& error() const LIFETIMEBOUND
     99+    constexpr const E& error() const& noexcept LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); }
    100+    constexpr E& error() & noexcept LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); }
    101+    constexpr E&& error() && noexcept LIFETIMEBOUND { return std::move(error()); }
    


    hodlinator commented at 10:23 am on December 11, 2025:

    How come this method overload is not calling itself recursively?

    Tested adding BOOST_CHECK_EQUAL((Expected<void, int>{Unexpected{1}}.error()), 1); to the tests to confirm it doesn’t but don’t understand why.

    Is this in some kind of &-mode, making it call the method above?


    maflcko commented at 11:08 am on December 11, 2025:

    Is this in some kind of &-mode, making it call the method above?

    It is not this, but (*this), see https://eel.is/c++draft/expr.prim#id.general-2


    hodlinator commented at 1:32 pm on December 11, 2025:
    Can’t read that. :grin:

    maflcko commented at 2:11 pm on December 11, 2025:

    Can’t read that. 😁

    i’d say it says that error() in this context is (*this).error(). So it calls error()& and not error()&&. If you want to (incorrectly) recursively call error(), you’d have to add a cast: std::move(*this).error();

  38. in src/util/expected.h:98 in fa874c9f05
    105+    constexpr T& operator*() & noexcept LIFETIMEBOUND { return value(); }
    106+    constexpr const T& operator*() const& noexcept LIFETIMEBOUND { return value(); }
    107+    constexpr T&& operator*() && noexcept LIFETIMEBOUND { return std::move(value()); }
    108+
    109+    constexpr T* operator->() noexcept LIFETIMEBOUND { return &value(); }
    110+    constexpr const T* operator->() const noexcept LIFETIMEBOUND { return &value(); }
    


    hodlinator commented at 10:27 am on December 11, 2025:
    nit: Might be nice to have these assert before failing due to an exception being thrown inside of noexcept.

    maflcko commented at 11:08 am on December 11, 2025:
    Why? Just seems more code for no reason?

    hodlinator commented at 1:31 pm on December 11, 2025:

    It’s about failing with a clearer stack trace/message. An exception will unwind the stack before giving a stack-trace (depending upon how the compiler treats exceptions within noexcept?).

    Tried adding *e in the unit tests and it doesn’t pinpoint the offending line, but gets by on checkpoints.

    0 ./build/bin/test_bitcoin -t util_expected_tests
    1Running 9 test cases...
    2terminate called after throwing an instance of 'util::BadExpectedAccess'
    3  what():  Bad util::Expected access
    4unknown location(0): fatal error: in "util_expected_tests/expected_value_throws": signal: SIGABRT (application abort requested)
    5../src/test/util_expected_tests.cpp(73): last checkpoint
    

    maflcko commented at 1:39 pm on December 11, 2025:

    Yes, this is also the behavior that you get in C++23 (if you are lucky). If you are unlucky, you’ll just get the silent UB:

    https://godbolt.org/z/jYxqvoejn

    0/../include/c++/v1/__expected/expected.h:811: libc++ Hardening assertion this->__has_val() failed: expected::operator* requires the expected to contain a value
    1Program terminated with signal: SIGSEGV
    

    Also, this is the behavior with an assert/abort, so I don’t see the difference!?


    hodlinator commented at 1:42 pm on December 11, 2025:
    Yeah, we don’t need to spoil ourselves before the switcharoo to std::expected. :)
  39. hodlinator approved
  40. hodlinator commented at 10:43 am on December 11, 2025: contributor

    re-ACK fa874c9f050500dc521ef8d9c81b043687f0dcc7

    Thanks for grabbing swap!

  41. DrahtBot requested review from ryanofsky on Dec 11, 2025
  42. DrahtBot removed the label CI failed on Dec 11, 2025

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-12-11 21:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me