-
Notifications
You must be signed in to change notification settings - Fork 38.3k
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 |
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 fabef9b
(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.
src/util/expected.h
Outdated
| constexpr const ValueType& value() const LIFETIMEBOUND | ||
| { | ||
| assert(has_value()); | ||
| if (!Assume(has_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: Make Expected::value() throw" (fa02975)
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:
template <class T, class E>
class Expected
{
private:
std::variant<T, E> m_data;
public:
// Value constructors.
constexpr Expected() = default;
constexpr Expected(T v) : m_data{std::in_place_index<0>, std::move(v)} {}
// Error constructor.
template <class G>
constexpr Expected(Unexpected<G> u) : m_data{std::in_place_index<1>, std::move(u).error()} {}
// Value accessors.
constexpr explicit operator bool() const noexcept { return m_data.index() == 0; }
constexpr const T& operator*() const& noexcept LIFETIMEBOUND { return *Assert(std::get_if<0>(&m_data)); }
constexpr T& operator*() & noexcept LIFETIMEBOUND { return *Assert(std::get_if<0>(&m_data)); }
constexpr T&& operator*() && noexcept LIFETIMEBOUND { return std::move(**this); }
constexpr const T* operator->() const noexcept LIFETIMEBOUND { return Assert(std::get_if<0>(&m_data)); }
constexpr T* operator->() noexcept LIFETIMEBOUND { return Assert(std::get_if<0>(&m_data)); }
// Error accessors.
constexpr const E& error() const& LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); }
constexpr E& error() & LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); }
constexpr E&& error() && LIFETIMEBOUND { return std::move(this->error()); }
// Higher level helpers.
constexpr bool has_value() const noexcept { return bool{*this}; }
constexpr const T& value() const& LIFETIMEBOUND { if (!*this) throw BadExpectedAccess{}; return **this; }
constexpr T& value() & LIFETIMEBOUND { if (!*this) throw BadExpectedAccess{}; return **this; }
constexpr T&& value() && LIFETIMEBOUND { return std::move(this->value()); }
template <class U>
T value_or(U&& default_value) const&
{
return *this ? **this : std::forward<U>(default_value);
}
template <class U>
T value_or(U&& default_value) &&
{
return *this ? std::move(**this) : std::forward<U>(default_value);
}
};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.
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.
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.
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.
Agree on making the other operators
noexcept.
thx, done
Agree on change to use the default ctor,
variantwill initialize with the first type (https://en.cppreference.com/w/cpp/utility/variant/variant.html, 1).
shouldn't hurt to be explicit
src/util/expected.h
Outdated
| constexpr E&& error() && LIFETIMEBOUND { return std::move(m_err); } | ||
|
|
||
| private: | ||
| E m_err; |
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: Add Unexpected::error()" (faa0d23)
Calling this m_error would seem more consistent.
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
| template <class E> | ||
| class Expected<void, E> |
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: Add Expected<void, E> specialization" (fa3d6c7)
This specialization is more complicated than it needs to be. Would suggest simplifying with inheritance:
//! Specialization for void returning void from value methods.
template <class E>
class Expected<void, E> : public Expected<std::monostate, E>
{
using Base = Expected<std::monostate, E>;
public:
using Expected<std::monostate, E>::Expected;
constexpr void operator*() const noexcept { Base::operator*(); }
constexpr void value() const& { Base::value(); }
constexpr void value() && { Base::value(); }
void value_or() = delete;
};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.
This just brings back the problem we are trying to solve: Solve exposing monostate externally?
+ Expected<void, std::string> e{std::monostate{}};
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
src/util/expected.h
Outdated
| constexpr const ValueType& value() const LIFETIMEBOUND | ||
| { | ||
| assert(has_value()); | ||
| if (!Assume(has_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.
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.
This is not needed, but a bit closer to the std lib.
This is not expected to be needed in this codebase, but brings the implementation closer to std::expected::value().
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.
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!
| constexpr const E& error() const LIFETIMEBOUND | ||
| constexpr const E& error() const& noexcept LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); } | ||
| constexpr E& error() & noexcept LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); } | ||
| constexpr E&& error() && noexcept LIFETIMEBOUND { return std::move(error()); } |
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.
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?
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.
Is
thisin 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
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.
Can't read that. 😁
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.
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();
| constexpr T& operator*() & noexcept LIFETIMEBOUND { return value(); } | ||
| constexpr const T& operator*() const& noexcept LIFETIMEBOUND { return value(); } | ||
| constexpr T&& operator*() && noexcept LIFETIMEBOUND { return std::move(value()); } | ||
|
|
||
| constexpr T* operator->() noexcept LIFETIMEBOUND { return &value(); } | ||
| constexpr const T* operator->() const 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.
nit: Might be nice to have these assert before failing due to an exception being thrown inside of noexcept.
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.
Why? Just seems more code for no reason?
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.
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.
₿ ./build/bin/test_bitcoin -t util_expected_tests
Running 9 test cases...
terminate called after throwing an instance of 'util::BadExpectedAccess'
what(): Bad util::Expected access
unknown location(0): fatal error: in "util_expected_tests/expected_value_throws": signal: SIGABRT (application abort requested)
../src/test/util_expected_tests.cpp(73): last checkpoint
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.
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
/../include/c++/v1/__expected/expected.h:811: libc++ Hardening assertion this->__has_val() failed: expected::operator* requires the expected to contain a value
Program terminated with signal: SIGSEGV
Also, this is the behavior with an assert/abort, so I don't see the difference!?
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.
Yeah, we don't need to spoil ourselves before the switcharoo to std::expected. :)
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