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 ryanofsky, stickies-v
Stale ACK hodlinator

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 fa20a50. 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 fa0a1a6. 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 fa0a1a6

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 fa874c9

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 fa874c9. 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.

{
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" (fa5a49c)

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 fa9aad7 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.

Copy link
Member Author

Choose a reason for hiding this comment

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

@ryanofsky No rush, but if you get a chance to take another look here before branch-off [1], that'd be great. I think the pull is currently blocked because of your use of the spicy word "unsafe" 😅

[1] It would be good to get this in before branch-off. Otherwise, there could be (unlikely) compile-failures or silent performance regressions for backports due to #34032 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

re: #34032 (comment)

@ryanofsky No rush, but if you get a chance to take another look here before branch-off [1], that'd be great. I think the pull is currently blocked because of your use of the spicy word "unsafe" 😅

I'm not saying the pull is unsafe. I'm saying this specific commit fa90c44 is unsafe and the updated commit message is misleading.

This commit is unsafe and the new commit message saying that pointer operators throwing exceptions is "fine" is misleading, because if these operators throw exceptions, code can catch those exceptions, and that code will be broken when Expected is replaced with expected, in bad ways triggering UB, in code paths likely to be missing test coverage because we often don't have tests checking error paths.

However, the pull request as a whole is safe because a later commit fa15f71 adds noexcept. That later commit fixes safety issue apparently by accident because it does not even mention any change in behavior.

IMO, this pull is not in a good state even if it is technically safe. These issues could be fixed at a surface level by adding noexcept in fa90c44 instead of fa15f71. In that case, it would be good to have comments next to these noexcepts, because normally when developers see noexcept we think "exceptions are not expected to be thrown here" not "exceptions are thrown one level down from here but this triggers std::terminate and that is good."

I do think the real mistake here is implementing pointer methods in terms of value() instead of the other way around. Using value() complicates implementation of this class, makes error reporting more confusing and less reliable, and sets a bad example of how you should deal with nullable values in c++. Using value instead of bool/*/-> makes unsafe dereferences look like normal method calls, and negates decades of pattern recognition c/c++ developers have dealing with code with nullable values.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the reply. I think I understand your point better now.

These issues could be fixed at a surface level by adding noexcept in fa90c44 instead of fa15f71.

Thanks, done.

normally when developers see noexcept we think "exceptions are not expected to be thrown here" not "exceptions are thrown one level down from here but this triggers std::terminate and that is good."

I think the first interpretation is correct, because no code should exist that dereferences a nullopt, so I'll not add a comment for now.

I do think the real mistake here is implementing pointer methods in terms of value() instead of the other way around. Using value() complicates implementation of this class, makes error reporting more confusing and less reliable, and sets a bad example of how you should deal with nullable values in c++. Using value instead of bool/*/-> makes unsafe dereferences look like normal method calls, and negates decades of pattern recognition c/c++ developers have dealing with code with nullable values.

I guess I just disagree here.

  • I don't like UB and I think we should move toward eliminating it, so I am not going to re-write this as unsafe code. Just because C++ is know for its UB for decades doesn't mean all future code needs to be unsafe as well.
  • Also, I disagree that Assert has a nicer error message here than terminate. See my prior reply.
  • I think the implementation is simple and also uses less code than the alternative.

Copy link
Contributor

@ryanofsky ryanofsky Jan 15, 2026

Choose a reason for hiding this comment

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

re: #34032 (comment)

  • I don't like UB and I think we should move toward eliminating it, so I am not going to re-write this as unsafe code.

If you think something in the suggestion #34032 (comment) is unsafe, it would help to clarify what you are referring to.

Just because C++ is know for its UB for decades doesn't mean all future code needs to be unsafe as well.

We have no disagreement about this. I am not saying it is bad to use value() in general. I think it is great to use value() in code that catches exceptions. However, it is bad to use value() in code that does not catch exceptions because:

  • It is misleading, not distinguishing between preconditions that are never expected to be violated and expected errors.
  • It makes unsafe dereferences blend into code looking like normal method calls instead of standing out clearly, bypassing extra scrutiny c/c++ developers give to * and -> uses.


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*()&&" (fa9aad7)

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

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))

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 faebb16. I really only like the first commit fad4a9f in this PR and think the other commits are neutral or bad (see below), but these changes seem ok if others want them.

{
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.

re: #34032 (comment)

@ryanofsky No rush, but if you get a chance to take another look here before branch-off [1], that'd be great. I think the pull is currently blocked because of your use of the spicy word "unsafe" 😅

I'm not saying the pull is unsafe. I'm saying this specific commit fa90c44 is unsafe and the updated commit message is misleading.

This commit is unsafe and the new commit message saying that pointer operators throwing exceptions is "fine" is misleading, because if these operators throw exceptions, code can catch those exceptions, and that code will be broken when Expected is replaced with expected, in bad ways triggering UB, in code paths likely to be missing test coverage because we often don't have tests checking error paths.

However, the pull request as a whole is safe because a later commit fa15f71 adds noexcept. That later commit fixes safety issue apparently by accident because it does not even mention any change in behavior.

IMO, this pull is not in a good state even if it is technically safe. These issues could be fixed at a surface level by adding noexcept in fa90c44 instead of fa15f71. In that case, it would be good to have comments next to these noexcepts, because normally when developers see noexcept we think "exceptions are not expected to be thrown here" not "exceptions are thrown one level down from here but this triggers std::terminate and that is good."

I do think the real mistake here is implementing pointer methods in terms of value() instead of the other way around. Using value() complicates implementation of this class, makes error reporting more confusing and less reliable, and sets a bad example of how you should deal with nullable values in c++. Using value instead of bool/*/-> makes unsafe dereferences look like normal method calls, and negates decades of pattern recognition c/c++ developers have dealing with code with nullable values.

MarcoFalke added 3 commits January 15, 2026 16:04
This is not expected to be needed in this codebase, but brings the
implementation closer to std::expected::value().

Also, add noexcept, where std::expected has them. This will make
operator-> and operator* terminate, when 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.
MarcoFalke added 2 commits January 15, 2026 16:15
It is currently unused, but implementing it is closer to std::expected.
@maflcko
Copy link
Member Author

maflcko commented Jan 15, 2026

Force pushed something. Should be trivial to re-review, because nothing changed:

$ git diff faebb16547 faa59b3679 | wc -l
0

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 faa59b3. Thanks for the update. The commit I objected to is fixed now and the rest of the implementation seems good enough for code that's probably temporary.

{
assert(has_value());
if (!has_value()) {
throw BadExpectedAccess{};
Copy link
Contributor

@ryanofsky ryanofsky Jan 15, 2026

Choose a reason for hiding this comment

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

re: #34032 (comment)

  • I don't like UB and I think we should move toward eliminating it, so I am not going to re-write this as unsafe code.

If you think something in the suggestion #34032 (comment) is unsafe, it would help to clarify what you are referring to.

Just because C++ is know for its UB for decades doesn't mean all future code needs to be unsafe as well.

We have no disagreement about this. I am not saying it is bad to use value() in general. I think it is great to use value() in code that catches exceptions. However, it is bad to use value() in code that does not catch exceptions because:

  • It is misleading, not distinguishing between preconditions that are never expected to be violated and expected errors.
  • It makes unsafe dereferences blend into code looking like normal method calls instead of standing out clearly, bypassing extra scrutiny c/c++ developers give to * and -> uses.

@stickies-v
Copy link
Contributor

re-ACK faa59b3

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