-
Notifications
You must be signed in to change notification settings - Fork 38.7k
util: Add some more Unexpected and Expected methods #34032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34032. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please copy-paste ConflictsReviewers, this pull request conflicts with the following ones:
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. |
ryanofsky
left a comment
There was a problem hiding this 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.
I kept the variant, because it is more consistent and shows that the |
|
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 |
There was a problem hiding this 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() &&).
ryanofsky
left a comment
There was a problem hiding this 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.
It only makes sense to turn this off with C++26, which introduces the _ placeholder.
hodlinator
left a comment
There was a problem hiding this 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.
fa6fcae to
fa874c9
Compare
hodlinator
left a comment
There was a problem hiding this 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!
ryanofsky
left a comment
There was a problem hiding this 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{}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 useoperator*instead, so they will now throw instead of assert if the value is not set. This would make it dangerous to swap theutil::Expectedclass withstd::expectedin 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(); } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thx, done
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.
stickies-v
left a comment
There was a problem hiding this 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); } |
There was a problem hiding this comment.
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.
hodlinator
left a comment
There was a problem hiding this 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))
Reviewers requested more member functions In #34006.
They are currently unused, but bring the port closer to the original
std::expectedimplementation:Expected::value()throw when no value existsUnexpected::error()methodsExpected<void, E>specializationExpected::value()&&andExpected::error()&&methodsExpected::swap()Also, include a tiny tidy fixup:
AllowCastToVoidin thebugprone-unused-return-valuecheck