Skip to content

Conversation

@maflcko
Copy link
Member

@maflcko maflcko commented Dec 9, 2025

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

@DrahtBot
Copy link
Contributor

DrahtBot commented Dec 9, 2025

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:

  • #34264 (fuzz: Extend spend coverage by Chand-ra)
  • #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.

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@maflcko
Copy link
Member Author

maflcko commented Dec 9, 2025

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.

@maflcko
Copy link
Member Author

maflcko commented Dec 9, 2025

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

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

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

Copy link
Contributor

@hodlinator hodlinator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() &&).

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

MarcoFalke added 2 commits December 11, 2025 09:46
It only makes sense to turn this off with C++26, which introduces the _
placeholder.
Copy link
Contributor

@hodlinator hodlinator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-ACK fa0a1a6a4986c1a977bbcd5f48834bedeac5dffb

This is not needed, but a bit closer to the std lib.
@maflcko maflcko force-pushed the 2512-exp branch 2 times, most recently from fa6fcae to fa874c9 Compare December 11, 2025 09:47
Copy link
Contributor

@hodlinator hodlinator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-ACK fa874c9f050500dc521ef8d9c81b043687f0dcc7

Thanks for grabbing swap!

@DrahtBot DrahtBot requested a review from ryanofsky December 11, 2025 10:43
@maflcko maflcko requested a review from stickies-v December 15, 2025 11:14
Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

return std::get<0>(m_data);
}
constexpr ValueType& value() LIFETIMEBOUND
{
assert(has_value());
if (!has_value()) {
throw BadExpectedAccess{};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (comment) 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

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

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


constexpr T* operator->() LIFETIMEBOUND { return &value(); }
constexpr const T* operator->() const LIFETIMEBOUND { return &value(); }
constexpr T* operator->() noexcept LIFETIMEBOUND { return &value(); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx, done

@DrahtBot DrahtBot requested a review from ryanofsky December 17, 2025 19:13
MarcoFalke added 4 commits December 18, 2025 08:58
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.
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.
They are currently unused, but implementing them is closer to the
std::expected.
It is currently unused, but implementing it is closer to std::expected.

Also, add noexcept, where std::expected has them.
Copy link
Contributor

@stickies-v stickies-v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK faebb16

constexpr E& error() & noexcept LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); }
constexpr E&& error() && noexcept LIFETIMEBOUND { return std::move(error()); }

constexpr void swap(Expected& other) noexcept { m_data.swap(other.m_data); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@DrahtBot DrahtBot requested a review from hodlinator January 5, 2026 09:11
@maflcko maflcko requested review from ryanofsky and removed request for ryanofsky January 5, 2026 10:10
Copy link
Contributor

@hodlinator hodlinator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-ACK faebb16

Only improved commit messages and made operator* for Expected<void, E>-specialization throw on error since my previous review (#34032 (review))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants