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 stickies-v, 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.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34208 (bench: add fluent API for untimed setup steps in nanobench by l0rinc)
    • #33689 (http: replace WorkQueue and single threads handling for ThreadPool by furszy)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  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{}};


    ryanofsky commented at 6:50 pm on December 17, 2025:

    re: #34032 (review)

    This just brings back the problem we are trying to solve

    I don’t think it does. The problem I want the void specialization to solve is leaking the std::monostate values to code that isn’t otherwise using it and is expecting void, making it harder to use std::expected in places like template functions or generic lambdas. I don’t see std::expected accepting (not returning) std::monostate{} as a comparable problem, or as a real problem, even though it could be avoided by declaring a deleted constructor. The current approach does seem ok though, just repetitive.

  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. maflcko force-pushed on Dec 11, 2025
  30. maflcko force-pushed on Dec 11, 2025
  31. DrahtBot added the label CI failed on Dec 11, 2025
  32. in src/util/expected.h:89 in fa874c9f05 outdated
     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();

  33. in src/util/expected.h:98 in fa874c9f05 outdated
    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. :)
  34. hodlinator approved
  35. hodlinator commented at 10:43 am on December 11, 2025: contributor

    re-ACK fa874c9f050500dc521ef8d9c81b043687f0dcc7

    Thanks for grabbing swap!

  36. DrahtBot requested review from ryanofsky on Dec 11, 2025
  37. DrahtBot removed the label CI failed on Dec 11, 2025
  38. maflcko requested review from stickies-v on Dec 15, 2025
  39. in src/util/expected.h:70 in fa5a49c988 outdated
    68     }
    69     constexpr ValueType& value() LIFETIMEBOUND
    70     {
    71-        assert(has_value());
    72+        if (!has_value()) {
    73+            throw BadExpectedAccess{};
    


    ryanofsky commented at 6:41 pm on December 17, 2025:

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

    The change seems unsafe because none of the other functions currently calling value() are switched to use operator* instead, so they will now throw instead of assert if the value is not set. This would make it dangerous to swap the util::Expected class with std::expected in the future.

    I’d recommend reconsidering the suggestion from #34032 (review) and avoiding unnecessary uses of value() and has_value() preferring to use c++’s built in operators, and setting a better example for other code to follow. Alternately more asserts could be added to fix this. This is also sort-of fixed in a later commit fa9aad738a9bfd3969aa4e1f19f87edadc69f455 adding noexcept to pointer operators, but asserts would probably lead to clearer runtime errors than noexcept violations.


    maflcko commented at 8:01 am on December 18, 2025:

    The change seems unsafe because none of the other functions currently calling value() are switched to use operator* instead, so they will now throw instead of assert if the value is not set. This would make it dangerous to swap the util::Expected class with std::expected in the future.

    I understand that switching util::Expected::operator* to std::expected::operator* is unsafe, but this is documented in https://en.cppreference.com/w/cpp/utility/expected/operator%252A.html:

    If has_value() is false, the behavior is undefined. (until C++26)

    Though, that is true with or without this pull request, so it seems unrelated.

    As for this implementation: I think it is perfectly fine to do whatever on UB, and asserting, or throwing seems better than UB.

    but asserts would probably lead to clearer runtime errors than noexcept violations.

    I don’t understand why that would be. The only difference with an assert is that it prints the file name and line number, but this will just be a inside this util header, not in the place in the code where the assertion happens. So reading the tb is needed anyway. For reference, with noexecpt, it looks like:

     0valgrind --tool=none ./bld-cmake/bin/test_bitcoin -t util_expected_tests/expected_error --catch_system_errors=no
     1==1207793== Nulgrind, the minimal Valgrind tool
     2...
     3==1207793== Command: ./bld-cmake/bin/test_bitcoin -t util_expected_tests/expected_error --catch_system_errors=no
     4==1207793== 
     5Running 1 test case...
     6terminate called after throwing an instance of 'util::BadExpectedAccess'
     7  what():  Bad util::Expected access
     8==1207793== 
     9==1207793== Process terminating with default action of signal 6 (SIGABRT): dumping core
    10...
    11==1207793==    by 0x4F1A98A: util_expected_tests::expected_error::test_method() (expected.h:117)
    12==1207793==    by 0x4F19B56: util_expected_tests::expected_error_invoker() (./test/util_expected_tests.cpp:79)
    13...
    14Aborted (core dumped)
    

    I think this is perfectly fine, and I don’t see the benefit of the assertion.

  40. in src/util/expected.h:97 in fa9aad738a outdated
    94+    constexpr const T& operator*() const& noexcept LIFETIMEBOUND { return value(); }
    95+    constexpr T&& operator*() && noexcept LIFETIMEBOUND { return std::move(value()); }
    96 
    97-    constexpr T* operator->() LIFETIMEBOUND { return &value(); }
    98-    constexpr const T* operator->() const LIFETIMEBOUND { return &value(); }
    99+    constexpr T* operator->() noexcept LIFETIMEBOUND { return &value(); }
    


    ryanofsky commented at 7:01 pm on December 17, 2025:

    In commit “util: Implement Expected::operator*()&&” (fa9aad738a9bfd3969aa4e1f19f87edadc69f455)

    Would be good for commit message to mention it is also adding noexcept to other overloads


    maflcko commented at 1:46 pm on December 19, 2025:
    thx, done
  41. ryanofsky commented at 7:13 pm on December 17, 2025: contributor

    Code review fa874c9f050500dc521ef8d9c81b043687f0dcc7. Looks mostly good and thanks for considering the previous suggestions. One thing that I think may be a problem is use of throwing value() method in operator methods declared noexcept but I left a more detailed comment below.

    Various changes were made since last review: renaming m_err, getting rid of Assume() in value(), testing void value() specialization, adding noexcepts, adding method & qualifiers, reformatting and using std::get_if, adding swap method.

  42. DrahtBot requested review from ryanofsky on Dec 17, 2025
  43. util: Make Expected::value() throw
    This is not expected to be needed in this codebase, but brings the
    implementation closer to std::expected::value().
    
    Review note: This will also make operator-> and operator* throw, but
    this is fine, because calling those is UB when
    std::expected::has_value() is false.
    fa90c44341
  44. 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.
    fa31884cd0
  45. util: Implement Expected::value()&& and Expected::error()&&
    They are currently unused, but implementing them is closer to the
    std::expected.
    fad1be98d2
  46. util: Implement Expected::operator*()&&
    It is currently unused, but implementing it is closer to std::expected.
    
    Also, add noexcept, where std::expected has them.
    fa15f71e34
  47. util: Add Expected::swap() faebb16547
  48. maflcko force-pushed on Dec 18, 2025
  49. in src/util/expected.h:91 in faebb16547
     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()); }
    102+
    103+    constexpr void swap(Expected& other) noexcept { m_data.swap(other.m_data); }
    


    stickies-v commented at 9:08 am on January 5, 2026:
    review note: std::variant::swap is not unconditionally noexcept but has a specification. However, I think for our minimal implementation just letting it std::terminate is fine.
  50. stickies-v approved
  51. stickies-v commented at 9:11 am on January 5, 2026: contributor
    utACK faebb1654743adcc5888464f9a7167c315ecce1f
  52. DrahtBot requested review from hodlinator on Jan 5, 2026
  53. maflcko removed review request from ryanofsky on Jan 5, 2026
  54. maflcko requested review from ryanofsky on Jan 5, 2026
  55. hodlinator approved
  56. hodlinator commented at 3:39 pm on January 6, 2026: contributor

    re-ACK faebb1654743adcc5888464f9a7167c315ecce1f

    Only improved commit messages and made operator* for Expected<void, E>-specialization throw on error since my previous review (https://github.com/bitcoin/bitcoin/pull/34032#pullrequestreview-3566738644)


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: 2026-01-12 15:13 UTC

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