-
Notifications
You must be signed in to change notification settings - Fork 37.7k
BResult improvements, allow returning values on failure #25608
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
Conversation
I would like to clean this up more and implement it on top of #25308 so the LoadChainstate function can use this class. But this class contains all the essential things I think are missing from BResult: support for error values, support for void values, support for multiple errors, chained errors, and warnings. Was motivated to work on this by suggestion to use BResult in #25308 #25308 (comment) and by #25601, which adds the same error type functionality this PR does, but doesn't support returning custom errors for error handling and returning standardized errors for error reporting at the same time. |
#include <util/string.h> | ||
|
||
namespace util { | ||
bilingual_str ErrorDescription(const Result<void>& result) |
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.
Any reason not to just include this as a method called e.g. result.ErrorsStr()
?
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 is an error formatting function and I think Result class should only be responsible for returning errors, not formatting them.
Also I generally think classes with private state should have fewer methods accessing that state to keep code readable and maintainable. If a function can use a class's public interface, it should use that instead of poking around at internals.
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.
Nice! Concept ACK
Result(ErrorChain, Str&& str, Prev&& prev, Args&&... args) : Result<void>{ErrorChain{}, std::forward<Str>(str), std::forward<Prev>(prev)}, m_result{std::forward<Args>(args)...} {}; | ||
|
||
//! std::optional methods, so Result<T> can be easily swapped for | ||
//! std::optional<T> to add error reporting to existing code or remove it if |
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.
I think you meant Result<T>
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.
I think you meant
Result<T>
I should probably rewrite this comment but it is trying to say if you have an existing function returning std::optional<T>
you can most likely change it to return util::Result<T>
and provide error information to new callers without affecting existing callers.
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.
Oh yeah, my mistake, misread 😅. Existing comment seems fine.
|
||
util::Result<int> ChainedFailFn(FnStatus arg, int ret) | ||
{ | ||
return {util::ErrorChain{}, Untranslated("chained fail"), StatusFailFn(arg), ret}; |
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.
Might be nice to have an interface that uses static constructor shorthands for concision, like
return Result::Err(ret, StatusFailFn(arg), Untranslated("chained fail"));
or for the function above
return Result::Err(ret, Untranslated("status fail"));
Edit: ah, I guess not so easy because in practice Result
would need to be templated.
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 would be possible and not very difficult to change syntax from
return {util::Error{}, "error string", error_value};
to
return util::Error("error string", error_value);
for both Error and ErrorChain with some template magic. It just made the implementation a little uglier so I decided to drop it.
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.
Concept ACK. I don't have a strong opinion between this approach or #25601.
But I don't think we should maintain BResult
. As done at #25601, the use of BResult
should be replaced by the new interface.
It's going to be confusing to have a class that shouldn't be used anymore, as said in the comment, when that can easily be replaced.
|
||
//! Error retrieval. | ||
const std::vector<bilingual_str>& GetErrors() const { return m_errors; } | ||
std::tuple<const std::vector<bilingual_str>&, const std::vector<bilingual_str>&> GetErrorsAndWarnings() const { return {m_errors, m_warnings}; } |
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.
Maybe a GetWarnings()
method ?
Or m_errors
and m_warnings
can be public.
I think there are some success cases that also return warnings.
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.
Maybe a
GetWarnings()
method ? Orm_errors
andm_warnings
can be public. I think there are some success cases that also return warnings.
Yes sorry interface is just incomplete while in draft.
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. 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. |
class Result<void> | ||
{ | ||
protected: | ||
std::vector<bilingual_str> m_errors; |
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 about moving the members into a single std::unique_ptr
, something like this:
protected:
struct ErrorsAndWarnings {
std::vector<bilingual_str> m_errors;
std::vector<bilingual_str> m_warnings;
};
std::unique_ptr<ErrorsAndWarnings> m_errors_and_warnings{};
This has the advantage that the default case when no error/warning happens is really fast: no temporary std::vector
need to be constructed. Also, sizeof()
is much smaller, only a single pointer. Then Result
is also noncopyable. I think this would be an advantage, because usually these are supposed to be moved as the return value, and not copied.
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 about moving the members into a single
std::unique_ptr
, something like this:
This is a great idea, and implemented in #25665
This is a draft commit trying to make some improvements to the `BResult` class to make it more usable. Adds a `util::Result` class with the following improvements (and implements `BResult` in terms of it so existing code doesn't have to change): - Supports returning error strings and error information at the same time. This functionality is needed by the `LoadChainstate` function in bitcoin#25308 or any function that can fail in multiple ways which need to be handled differently. And it means Result class is compatible with rust-style error handling and pattern matching, unlike BResult. - Makes Result class provide same operators and methods as std::optional, so it doesn't require calling less familiar `HasRes`/`GetObj`/`ReleaseObj` methods, and so error reporting functionality can be easily added or dropped from existing code by switching between util::Result and std::optional. - Supports `Result<void>` return values, so it is possible to return error reporting information from functions in a uniform way even if they don't have other return values. - Supports `Result<bilingual_str>` return values. Drops ambiguous and potentially misleading `BResult` constructor that treats any bilingual string as an error, and any other type as a non-error. Adds `util::Error` constructor so errors have to be explicitly constructed and not any `bilingual_str` will be turned into one. - Puts `src/util/` code in the `util::` namespace so naming reflects code organization and it is obvious where the class is coming from. Drops "B" from name because it is undocumented what it stands for (bilingual?) - Adds unit tests.
Rebased 39769cc -> dd91f29 ( (@martinus suggestion to optimize happy path when there are no warnings or errors a makes a lot of sense so I started implementing that, but I figured I'd rebase this in the meantime due to the conflict) |
Result(ErrorChain, Str&& str, Prev&& prev, Args&&... args) : Result<void>{ErrorChain{}, std::forward<Str>(str), std::forward<Prev>(prev)}, m_result{std::forward<Args>(args)...} {}; | ||
|
||
//! std::optional methods, so Result<T> can be easily swapped for | ||
//! std::optional<T> to add error reporting to existing code or remove it if |
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.
I don't think this is true? std::optional
with nullopt
must not be dereferenced, whereas Result
can be, and would return a default constructed object?
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.
I don't think this is true?
std::optional
withnullopt
must not be dereferenced, whereasResult
can be, and would return a default constructed object?
I can clarify comment, but this interface is superset of std::optional interface and allows dereferencing in cases when original object can't be derefenced. The object does not have to be default constructable since util::Error{} can forward any constructor arguments
template<class T> | ||
class BResult { | ||
private: | ||
util::Result<std::optional<T>> m_result; |
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.
I don't think this is too helpful to force most call sites (that can use BResult or use it today) into a double wrapping util::Result<std::optional<T>>
. I liked a single wrapping better.
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.
I don't think this is too helpful to force most call sites (that can use BResult or use it today) into a double wrapping
util::Result<std::optional<T>>
. I liked a single wrapping better.
Right this was supposed to be only temporary within the PR. A followup commit basically like 6a06a7c (from #25665) would replace BResult<T>
with util::Result<T>
std::optional was used here just because it's the simplest type definition that preserves BResult semantics.
Closing for now in favor of #25665 which implements Martinus suggestion. Will post a comparison of different approaches there. |
This is a draft PR trying to make some improvements to the
BResult
class to make it more usable. Adds autil::Result
class with the following improvements (and implementsBResult
in terms of it so existing code doesn't have to change):Supports returning error strings and error information at the same time. This functionality is needed by the
LoadChainstate
function in refactor: Reduce number of LoadChainstate parameters and return values #25308 or any function that can fail in multiple ways which need to be handled differently. And it means Result class is compatible with rust-style error handling and pattern matching, unlike BResult.Makes Result class provide same operators and methods as std::optional, so it doesn't require calling less familiar
HasRes
/GetObj
/ReleaseObj
methods, and so error reporting functionality can be easily added or dropped from existing code by switching between util::Result and std::optional.Supports
Result<void>
return values, so it is possible to return error reporting information from functions in a uniform way even if they don't have other return values.Supports
Result<bilingual_str>
return values. Drops ambiguous and potentially misleadingBResult
constructor that treats any bilingual string as an error, and any other type as a non-error. Addsutil::Error
constructor so errors have to be explicitly constructed and not anybilingual_str
will be turned into one.Puts
src/util/
code in theutil::
namespace so naming reflects code organization and it is obvious where the class is coming from. Drops "B" from name because it is undocumented what it stands for (bilingual?)Adds unit tests.