-
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 fa20a50. 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 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.
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 fa0a1a6
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 fa874c9
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 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{}; |
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" (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.
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.
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.
@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)
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: #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.
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.
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. Usingvalue()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++. Usingvalueinstead ofbool/*/->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
Asserthas 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.
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: #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(); } |
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*()&&" (fa9aad7)
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
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))
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.
| { | ||
| 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.
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.
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.
It is currently unused, but implementing it is closer to std::expected.
|
Force pushed something. Should be trivial to re-review, because nothing changed: |
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 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{}; |
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: #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.
|
re-ACK faa59b3 |
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