ryanofsky
commented at 12:46 pm on July 21, 2022:
contributor
Add util::Result support for returning more error information and make use of it in LoadChainstate method as an initial application. Followup PRs #25722 and #29700 use it more broadly to return errors and warnings from wallet and kernel functions as well.
This change adds two major features to the result class:
For better error handling, adds the ability to return a value on failure, not just a value on success. This is a key missing feature that makes the result class not useful for functions like LoadChainstate() which produce different errors that need to be handled differently 1.
For better error reporting, adds the ability to return warning messages and multiple errors, not just a single error string. This provides a way for functions to report errors and warnings in a standard way, and simplifies interfaces:
This change also makes some more minor improvements:
Smaller type size. util::Result<int> is 16 bytes, and util::Result<void> is 8 bytes. Previously both were 72 bytes.
Broader type compatibility. Supports noncopyable and nonmovable success and error types.
Alternatives & design notes
The main goal for the util::Result class is to provide a standard way for functions to report error and warnings strings. The interface tries to make it easy to provide detailed error feedback to end users, without cluttering code or making it harder to return other result information. There have been multiple iterations of the design using different internal representations and providing different capabilities:
Representation (approximate)
Notes
1
tuple<T,optional<bilingual_str>>
Original #25218 implementation in da98fd2efc1a6b9c6e876cf3e227a8c2e9a10827. Good capabilities, but larger type size. Supports error standardized error reporting and customized error handling with failure values.
2
variant<T,bilingual_str>
Final #25218 implementation in 7a45c33d1f8a758850cf8e7bd6ad508939ba5c0d. No support for returning failure values so not useful in many cases.
3
variant<monostate,T,F>
Pending #25601 implementation that allows choosing whether to use standardized error reporting or return custom failure values, but doesn’t support having both at the same time, like approaches (1), (4), (5), (6) do.
4
tuple<variant<T,F>,bilingual_str>
Original #25608 implementation in c29d3008de9314dd463ed08e8bff39fe55324498. Basically the same as (1) except it uses separate success and failure types instead of the same type. Using separate success and failure types makes the result class a little less flexible but can help avoid some ambiguity and inconsistent result states.
5
tuple<T,optional<bilingual_str>>
Final #25608 implementation in dd91f294206ac87b213d23bb76656a0a5f0f4781. Essentially the same as original implementation (1) except has some usability improvements.
6
tuple<T,unique_ptr<tuple<F,bilingual_str>>
Pending #25665 implementation (this PR). Supports returning failure values, uses separate success and failure types to avoid ambiguity, uses unique_ptr to reduce result type size, and avoids heap allocation in the happy path.
Prior discussions & history
furszy introduced a BResult class providing a standard error reporting mechanism in #25218. It was renamed to util::Result in #25721 but kept the same representation and capabilities.
MarcoFalke suggested using BResult for the LoadChainstate function in #25308 (comment). Inability to use BResult there due to lack of support for failure values led to initial work on #25608.
w0xlt wrote a StructuredResult class in #25308 adding the ability to return failure values but sacrificing standard error reporting, which led to more work on #25608.
martinus suggested a space optimization in #25608 (comment) that also made it easier to support distinct failure & success types, leading to #25665 as a replacement for #25608.
Ability to return error values was actually present in the original implementation of #25218, but unfortunately removed in later iterations. ↩︎
in
src/bench/wallet_loading.cpp:49
in
6a06a7c3adoutdated
This pull changes both the interface, as well as the internal representation. I wonder if changing the interface and renames can be done separate from the restructuring.
Hopefully those would be uncontroversial and could be reviewed easier/faster, so that less silent merge conflicts arise as the class gets used more and more. Also, less code will need to be changes if the interface changes are made earlier rather than later.
This pull changes both the interface, as well as the internal representation. I wonder if changing the interface and renames can be done separate from the restructuring.
Only the third commit drops BResult usages, so the answer should be yes. IMO std::optional has a nice interface and it is a shame to reinvent it with stranger HasRes GetObj ReleaseObj methods.
Makes Result class provide same operators and methods as std::optional
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?)
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.
I think these things are already split out. The first two commits do not change the interface or naming or functionality of BResult in any way. If you want me to move the last commit to another, PR I can do that.
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 force-pushed
on Jul 21, 2022
ryanofsky force-pushed
on Jul 21, 2022
DrahtBot added the label
Needs rebase
on Jul 22, 2022
ryanofsky force-pushed
on Jul 22, 2022
DrahtBot removed the label
Needs rebase
on Jul 22, 2022
ryanofsky force-pushed
on Jul 25, 2022
ryanofsky force-pushed
on Jul 26, 2022
ryanofsky force-pushed
on Jul 26, 2022
ryanofsky force-pushed
on Jul 26, 2022
ryanofsky renamed this:
BResult improvements, allow returning separate value on failure
refactor: Add util::Result class and use it in LoadChainstate
on Jul 27, 2022
DrahtBot added the label
Refactoring
on Jul 27, 2022
ryanofsky force-pushed
on Jul 27, 2022
ryanofsky marked this as ready for review
on Jul 27, 2022
ryanofsky
commented at 7:32 pm on July 27, 2022:
contributor
Moved out of draft, since result interface is more complete now and there’s more code using it
maflcko
commented at 7:44 pm on July 27, 2022:
member
As said on the previous pull, I’d prefer if the order of the changes was inversed: First, change the existing interface methods and names, then change the internal class implementation. Otherwise we’ll end up with the odd state of two classes that do the same thing but have a different name and people will continue to add more used of the “deprecated” class. I’d also find it easier to review an incremental diff than having a full separate implementation. But no strong opinion, if you and the other reviewers prefer it as-is now.
Edit: My comment was #25665 (review) and I just realized that it was ambiguous and could be interpreted the opposite of what I meant.
ryanofsky
commented at 8:13 pm on July 27, 2022:
contributor
Thanks, Marco! I will just tweak #25721 to be a standalone PR instead of depending on this PR
ryanofsky referenced this in commit
2aa408b4cc
on Jul 27, 2022
ryanofsky referenced this in commit
e71b858bc0
on Jul 27, 2022
ryanofsky
commented at 9:39 pm on July 27, 2022:
contributor
Thanks, Marco! I will just tweak #25721 to be a standalone PR instead of depending on this PR
This is done now
ryanofsky referenced this in commit
42f4f7d126
on Jul 27, 2022
ryanofsky referenced this in commit
e0289b1cdf
on Aug 1, 2022
ryanofsky referenced this in commit
3262acf70a
on Aug 2, 2022
ryanofsky referenced this in commit
1e1f5ca8ac
on Aug 2, 2022
ryanofsky referenced this in commit
6777df621a
on Aug 2, 2022
ryanofsky referenced this in commit
c654ec55f8
on Aug 2, 2022
ryanofsky referenced this in commit
7b249b3a16
on Aug 2, 2022
ryanofsky referenced this in commit
a23cca56c0
on Aug 3, 2022
ryanofsky referenced this in commit
0e50848064
on Aug 3, 2022
maflcko referenced this in commit
006740b6f6
on Aug 5, 2022
DrahtBot added the label
Needs rebase
on Aug 5, 2022
ryanofsky renamed this:
refactor: Add util::Result class and use it in LoadChainstate
refactor: Add util::Result failure values, multiple error and warning messages
on Aug 5, 2022
ryanofsky force-pushed
on Aug 5, 2022
ryanofsky
commented at 6:48 pm on August 5, 2022:
contributor
Rebased c2dc8a8a747d639acfa4a26db2c61c25b6f82571 -> 590bc615a3120a8f11712220546f9654058b82f0 (pr/bresult2.10 -> pr/bresult2.11, compare) due to conflicts with #25721
DrahtBot removed the label
Needs rebase
on Aug 5, 2022
I’m not sure why iwyu doesn’t catch it, but shouldn’t forward, types and utility also be included here?
Thanks, added utility. (I think the others are nonstandard.) IWYU might not catch references to code in templates that aren’t instantiated, so I’m not too surprised it didn’t add this.
Could probably make sense to split this up and use it there: #25616 (comment)
Is the suggestion to add the GetErrors() method in this PR, but delay adding the GetWarnings() method until followup PR #25722? I could do that, but I don’t think it would simplify this PR very much (it would remove a few lines) and it would add more churn to result implementation/documentation/unit tests review
This would allow reviewers to just look at the GetWarnings implementation, API, and proposed usage, without having to think about union/void/... etc.
Oh. I guess there could be a third PR independent of this PR and #25722 that adds a std::vector<bilingual_str> m_warnings; field to util::Result and AddWarning()/GetWarnings() methods. I don’t think it would be as useful as you might be thinking without template constructors that can move errors & warnings from one result object to another, though.
Maybe if you are interested you could open a PR that does this? I’d be happy to review it. I’m just not sure exactly what you want to do here because the goal of #25722 is a lot broader. Not just dropping std::vector<bilingual_str>& warnings output arguments but also dropping DatabaseStatus& status output arguments and returning errors and warnings directly from nested calls.
The thing is that you are basically adding a ton of dead code in this pull. bitcoind compiles fine without it, so I might be picky, but my preference would be to only add the code when it is needed. This also makes it easier for reviewers because they can think about actual use cases and don’t have to imagine them from the unit tests.
0diff --git a/src/util/result.cpp b/src/util/result.cpp
1index 9526b5b785..199a6579d7 100644
2--- a/src/util/result.cpp
3+++ b/src/util/result.cpp
4@@ -3,26 +3,8 @@
5 // file COPYING or https://www.opensource.org/licenses/mit-license.php.
6 7 #include <util/result.h>
8-#include <util/string.h>
9 10 namespace util {
11 namespace detail {
12-bilingual_str JoinMessages(const std::vector<bilingual_str>& errors, const std::vector<bilingual_str> warnings)
13-{
14- bilingual_str result;
15- for (const auto& messages : {errors, warnings}) {
16- for (const auto& message : messages) {
17- if (!result.empty()) result += Untranslated(" ");
18- result += message;
19- }
20- }
21- return result;
22-}
23-
24-void MoveMessages(std::vector<bilingual_str>& src, std::vector<bilingual_str>& dest)
25-{
26- dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));
27- src.clear();
28-}
29 } // namespace detail
30 } // namespace util
31diff --git a/src/util/result.h b/src/util/result.h
32index 32fe40763f..8eb5b552d3 100644
33--- a/src/util/result.h
34+++ b/src/util/result.h
35@@ -17,21 +17,11 @@
36 37 namespace util {
38 namespace detail {
39-//! Empty string list
40-const std::vector<bilingual_str> EMPTY_LIST{};
41-
42-//! Helper function to join messages in space separated string.
43-bilingual_str JoinMessages(const std::vector<bilingual_str>& errors, const std::vector<bilingual_str> warnings);
44-
45-//! Helper function to move messages from one vector to another.
46-void MoveMessages(std::vector<bilingual_str>& src, std::vector<bilingual_str>& dest);
47-
48-//! Error information only allocated if there are errors or warnings.
49+//! Error information only allocated if there is an error.
50 template <typename F>
51 struct ErrorInfo {
52 std::optional<std::conditional_t<std::is_same<F, void>::value, std::monostate, F>> failure{};
53- std::vector<bilingual_str> errors;
54- std::vector<bilingual_str> warnings;
55+ bilingual_str error;
56 };
57 58 //! Result base class which is inherited by Result<T, F>.
59@@ -66,25 +56,7 @@ protected:
60 public:
61 void AddError(bilingual_str error)
62 {
63- if (!error.empty()) Info().errors.emplace_back(std::move(error));
64- }
65-
66- void AddWarning(bilingual_str warning)
67- {
68- if (!warning.empty()) Info().warnings.emplace_back(std::move(warning));
69- }
70-
71- //! Operator moving warning and error messages from other result to this.
72- //! Only moves message strings, does not change success or failure values of
73- //! either Result object.
74- template<typename O>
75- O&& operator<<(O&& other)
76- {
77- if (other.m_info) {
78- if (!other.m_info->errors.empty()) MoveMessages(other.m_info->errors, Info().errors);
79- if (!other.m_info->warnings.empty()) MoveMessages(other.m_info->warnings, Info().warnings);
80- }
81- return std::forward<O>(other);
82+ Info().error = std::move(error);
83 }
84 85 //! Success check.
86@@ -93,8 +65,7 @@ public:
87 //! Error retrieval.
88 template <typename _F = F>
89 std::enable_if_t<!std::is_same<_F, void>::value, const _F&> GetFailure() const { assert(!*this); return *m_info->failure; }
90- const std::vector<bilingual_str>& GetErrors() const { return m_info ? m_info->errors : EMPTY_LIST; }
91- const std::vector<bilingual_str>& GetWarnings() const { return m_info ? m_info->warnings : EMPTY_LIST; }
92+ bilingual_str GetError() const { return m_info ? m_info->error : bilingual_str{}; }
93 };
94 95 //! Result base class for T value type. Holds value and provides accessor methods.
96@@ -145,9 +116,6 @@ public:
97 struct Error {
98 bilingual_str message;
99 };
100-struct Warning {
101- bilingual_str message;
102-};
103104 //! The util::Result class provides a standard way for functions to return error
105 //! and warning strings in addition to optional result values.
106@@ -190,23 +158,11 @@ protected:
107 }, std::forward<Args>(args)...);
108 }
109110- template <typename Fn, typename... Args>
111- void Construct(const Fn& fn, Warning warning, Args&&... args)
112- {
113- this->AddWarning(std::move(warning.message));
114- Construct(fn, std::forward<Args>(args)...);
115- }
116-
117- template <typename Fn, typename OT, typename OF, typename... Args>
118- void Construct(const Fn& fn, Result<OT, OF>&& other, Args&&... args)
119- {
120- *this << other;
121- Construct(fn, std::forward<Args>(args)...);
122- }
123-
124 void MoveConstruct(Result& other)
125 {
126- *this << other;
127+ if (other.m_info) {
128+ this->AddError(std::move(other.m_info->error));
129+ }
130 if (other) this->MoveValue(other); else this->Info().failure = std::move(other.m_info->failure);
131 }
132133@@ -233,7 +189,7 @@ public:
134 //! are present. More complicated applications should use GetErrors() and
135 //! GetWarning() methods directly.
136 template <typename T, typename F>
137-bilingual_str ErrorString(const Result<T, F>& result) { return detail::JoinMessages(result.GetErrors(), result.GetWarnings()); }
138+bilingual_str ErrorString(const Result<T, F>& result) { return result.GetError(); }
139 } // namespace util
140141 #endif // BITCOIN_UTIL_RESULT_H
ryanofsky
commented at 4:15 pm on August 29, 2022:
The thing is that you are basically adding a ton of dead code in this pull
Interesting and thanks for the diff! Now I understand what you are suggesting, and I think I’d be ok with making that change. I wasn’t really looking at that code as dead because I’m immediately using it in the next pull #25722 and also using it in unit tests this PR. I also didn’t consider it to be a ton of code, since the code in your diff is just getter/setter functions that provide access to errors/warnings variables. But no objection to splitting it off into another pull.
Yeah, I don’t feel too strong as well. Though, if you keep it, it would be good to fixup the typo: #25665 (comment)
Ok, I’m hedging right now by moving multiple error & warning support into a separate commit but not a separate PR. I maybe have a slight preference for just squashing the commits again to reduce churn, but I’m also fine with keeping separate commits, or moving the commit to another PR if one of those alternatives seems better, so let me know!
Riahiamirreza
commented at 4:42 pm on August 13, 2022:
What is the difference between when the this->m_info is true and when it is false? While as far as I understand in both cases the this->m_info is destroyed and replaced by other.m_info. Am I correct? So why bother to check other.m_info && !this->m_info and other.m_info && this->m_info?
ryanofsky
commented at 8:02 pm on August 13, 2022:
What is the difference between when the this->m_info is true and when it is false?
The implementation is optimized for the “happy path” where a success value is set and there is no failure value and no error or warning message strings. In the happy path case, m_info is null and no allocation is needed. Otherwise, if there has been any kind of error or warning m_info will be non-null.
While as far as I understand in both cases the this->m_info is destroyed and replaced by other.m_info. Am I correct?
No that’s not really correct. As the comment “Operator moving warning and error messages from one result to another” says, only warning and error strings are moved from other to *this. The success and value statuses of other and *this objects are unchanged, and the success and failure values of both objects (if any) are also unchanged. Existing warning and error strings in *this are also not affected, the new errors and warnings just get appended and don’t replace existing one.
So why bother to check other.m_info && !this->m_info and other.m_info && this->m_info?
I’m not sure what other code you might expect to see here, but in both cases this is just moving other.m_info->errors strings to this->m_info->errors and moving other.m_info->warnings strings to this->m_info->warnings while not deleting any existing strings and not changing other.m_info->failure and this->m_info->failure values.
Riahiamirreza
commented at 8:49 am on August 15, 2022:
Thanks for your explanation. My wrong assumption was that by replacing the warnings and errors the this is completely like the other, so why checking whether this->m_info is null or not.
But as you mentioned the success and value statuses remained unchanged.
in
src/util/result.h:244
in
590bc615a3outdated
86+//! Result base class for T value type. Holds value and provides accessor methods.
87+template <typename T, typename F>
88+class ResultBase : public ResultBase<void, F>
89+{
90+protected:
91+ union { T m_value; };
Riahiamirreza
commented at 8:39 am on August 15, 2022:
I don’t understand why the m_value is in a union while there is only one member in the union? Would you explain why?
ryanofsky
commented at 12:47 pm on August 15, 2022:
I don’t understand why the m_value is in a union while there is only one member in the union? Would you explain why?
I’ll add a code comment, but using a union avoids m_value getting constructor and destructor being called automatically, so in the failure case m_value is never constructed.
Riahiamirreza approved
ryanofsky force-pushed
on Aug 15, 2022
ryanofsky
commented at 6:16 pm on August 15, 2022:
contributor
Updated 590bc615a3120a8f11712220546f9654058b82f0 -> 65481de0646f21349f24327410e4d7eb5189e5b3 (pr/bresult2.11 -> pr/bresult2.12, compare) just adding some comments to answer review questions
in
src/node/chainstate.h:39
in
65481de064outdated
42-using ChainstateLoadResult = std::tuple<ChainstateLoadStatus, bilingual_str>;
43+//! Chainstate load errors. Simple applications can just treat all errors as
44+//! failures. More complex applications may want to try reindexing in the
45+//! generic error case, and pass an interrupt callback and exit cleanly in the
46+//! interrupted case.
47+enum class ChainstateLoadError { FAILURE, FAILURE_INCOMPATIBLE_DB, INTERRUPTED };
So that it’s more inline with indicating “what to do about the failure”.
I agree it’s good to suggest how failures can be handled, so I tried to do that in the comment. But I think if failure has a clear cause it’s best for the error name to describe the cause, especially since not every application will want to handle errors the same way, and bitcoin-qt, bitcoind, and bitcoin-chainstate all handle errors differently.
I’m also don’t think FAILURE_TRY_REINDEX is an appropriate synonym for FAILURE, since generic failures can happen on any exception, even if reindexing was already requested. Trying to reindex when reindexing fails does not make a lot of sense. I’d be happy to rename FAILURE to FAILURE_GENERIC. But would probably do it in a separate PR to not complicate this one.
Could also move the “4 types of outcomes” comment up here where it’s clearer.
Thanks for pointing it out. I think that comment actually does not add much information anymore, and is a little confusing because it is mentioning shutdowns when the check_interrupt callback and INTERRUPTED error could indicate any custom interruption, not just a shutdown.
I can add more documentation if you think anything is missing, but I think a simple message of “It is fine to treat any error code as a failure and ignore the specific cause” is the most helpful takeaway. Setting interrupt callbacks and trying to reindex are optional enhancements mostly helpful for interactive applications.
Would it be sufficient to use explicit operator bool() const instead here? That would avoid e.g. “(result + 3)” from compiling (and evaluating to 3 or 4).
ryanofsky
commented at 6:00 pm on August 17, 2022:
Would it be sufficient to use explicit operator bool() const instead here? That would avoid e.g. “(result + 3)” from compiling (and evaluating to 3 or 4).
Good catch, added explicit. This was an unintended regression (operator bool was explicit before this PR)
hernanmarino
commented at 6:22 pm on August 17, 2022:
ryanofsky
commented at 7:26 pm on August 17, 2022:
contributor
Updated 65481de0646f21349f24327410e4d7eb5189e5b3 -> 9bd10728bada8b04d86f5621ee127713f628a9ad (pr/bresult2.12 -> pr/bresult2.13, compare) with suggestions
hernanmarino
commented at 4:38 am on August 18, 2022:
contributor
Tested ACK and Code review ACK. I got a few warnings while compiling though (missing-field-initializers)
ryanofsky
commented at 2:18 pm on August 18, 2022:
contributor
Tested ACK and Code review ACK. I got a few warnings while compiling though (missing-field-initializers)
Thanks for testing! @hernanmarino could you post the warnings, and maybe post your compiler version? I don’t think I’m seeing these and I don’t think they are happening on CI because those builds treat warnings as errors. I’d like to fix this if possible.
in
src/util/result.h:227
in
9bd10728baoutdated
256-{
257- return result ? bilingual_str{} : std::get<0>(result.m_variant);
258-}
259+//! Helper methods to format error strings.
260+bilingual_str ErrorString(const std::vector<bilingual_str>& errors);
261+bilingual_str ErrorString(const std::vector<bilingual_str>& errors, const std::vector<bilingual_str>& warnings);
stickies-v
commented at 1:53 pm on August 19, 2022:
Do we want to expose these 2 helper methods in the header?
ryanofsky
commented at 7:59 pm on August 23, 2022:
stickies-v
commented at 1:54 pm on August 19, 2022:
What about renaming this function to ErrorWarningString() since at the moment it can contain both? I can see some scenario where people would want to access just the error but not the warning string (and of course it’s just a convenience fn), so that would allow them to create the more narrow ErrorString() later on?
Perhaps a brief docstring above this fn would be beneficial as well since that’s the one people will mostly use.
ryanofsky
commented at 7:59 pm on August 23, 2022:
What about renaming this function to ErrorWarningString() since at the moment it can contain both? I can see some scenario where people would want to access just the error but not the warning string (and of course it’s just a convenience fn), so that would allow them to create the more narrow ErrorString() later on?
Added some more documentation about how this should be used. I think a function that just puts errors not warnings in a result message could be a potential footgun, because warnings could be unintentionally dropped if errors and warnings are returned together. For example if code initially doesn’t generate any warnings, then someone adds one not realizing it won’t show up anywhere.
stickies-v
commented at 7:21 pm on August 24, 2022:
I didn’t think about the footgun, that’s a good point why we probably indeed won’t want to add the other fn later. I still think it’s a bit awkward that the function name doesn’t entirely capture what the function is doing, but I think it’s within reason and may be worth the trade-off for brevity (vs ErrorWarningString(). Not sure what I prefer, so feel free to ignore.
The new docstring is great, thanks!
ryanofsky
commented at 2:55 pm on August 25, 2022:
I still think it’s a bit awkward that the function name doesn’t entirely capture what the function is doing, but I think it’s within reason and may be worth the trade-off for brevity (vs ErrorWarningString(). Not sure what I prefer, so feel free to ignore.
Could call it util::MessageString(result) instead of util::ErrorString(result). I think the longer name would be ok too. I was pushing back more against changing the functionality than changing the name.
I do think any renaming should happen in a separate PR, before or after this one. The function already exists and is called in current code. This PR is backwards compatible just extends the Result API without requiring changes to existing code.
stickies-v
commented at 3:28 pm on August 25, 2022:
The function already exists and is called in current code.
Right, I did not consider that. Don’t think it’s worth the extra PR without more demand for it so I’m happy to just leave it at ErrorString until then.
in
src/util/result.h:96
in
9bd10728baoutdated
25+{
26+ dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));
27+ src.clear();
28+}
29+
30+//! Error information only allocated if there are errors or warnings.
stickies-v
commented at 2:02 pm on August 19, 2022:
nit: I think this docstring is more relevant for m_info, perhaps move it to above its declaration in ResultBase?
ryanofsky
commented at 8:37 pm on August 22, 2022:
nit: I think this docstring is more relevant for m_info, perhaps move it to above its declaration in ResultBase?
I think it’s relevant here because the struct is pretty big thing to have in a return type, so good to say it won’t typically be needed.
Also, this may be a stylistic thing, but I tend to prefer class-level documentation that gets at why/how questions, over member level documentation only answers more basic what questions, and can also clutter the class definition.
stickies-v
commented at 11:23 am on August 31, 2022:
Maybe I’m confused, but I don’t think I agree. Since the optional allocation of an ErrorInfo is a result of how Info() is implemented, not of how the ErrorInfo struct is defined - isn’t that where it should be documented? Unless you’re talking about the optional allocation of failure instead of ErrorInfo, in which case I would agree that this should be explained at this location (and you might want to add another one to info explaining the dynamic allocation of m_info)?
ryanofsky
commented at 2:07 pm on September 1, 2022:
Maybe I’m confused, but I don’t think I agree. Since the optional allocation of an ErrorInfo is a result of how Info() is implemented, not of how the ErrorInfo struct is defined - isn’t that where it should be documented?
I’m only pushing back against removing this comment here. I’m happy to add more documentation anywhere would would like.
I think the answer to your question is no, because this isn’t a general purpose API. This is a custom, private struct that is never exposed externally and exists for one purpose. The comment is describing what the purpose is. If I were reading this code and saw this struct, I would be wondering why there is such a heavyweight struct being used in a lightweight result type, why error information is segregated from normal result information, and why a separate struct definition is needed at all instead using normal class members. This comment explains what the purpose of the struct is, why it exists and how it is used, and I think is appropriate documentation for an single-purpose piece of a larger implementation.
in
src/util/result.h:104
in
9bd10728baoutdated
33+ std::optional<std::conditional_t<std::is_same<F, void>::value, std::monostate, F>> failure;
34+ std::vector<bilingual_str> errors;
35+ std::vector<bilingual_str> warnings;
36+};
37+
38+//! Result base class which is inherited by Result<T, F>.
stickies-v
commented at 2:07 pm on August 19, 2022:
nit: You could add a bit more info on T, F for quick understanding?
0//! Result base class which is inherited by Result<T, F>.
1//! T is the type of the success return value, or void if there is none.
2//! F is the type of the failure return value, or void if there is none.
ryanofsky
commented at 8:38 pm on August 22, 2022:
This is repeated a few times, what do you think about this:
Nice catch! I implemented something similar to what you suggested to dedup
stickies-v
commented at 7:32 pm on August 24, 2022:
I like your solution, very elegant!
hernanmarino
commented at 2:36 pm on August 19, 2022:
contributor
Thanks for testing! @hernanmarino could you post the warnings, and maybe post your compiler version? I don’t think I’m seeing these and I don’t think they are happening on CI because those builds treat warnings as errors. I’d like to fix this if possible.
Yes, this is one of them (there are a few more , all similar)
0 In file included from ./interfaces/wallet.h:15,
1 from wallet/interfaces.cpp:5:
2./util/result.h: In instantiation of ‘util::Result<OT, OF>&& util::Result<T, F>::operator<<(util::Result<OT, OF>&&) [with OT = std::unique_ptr<interfaces::Wallet>; OF = void; T = std::unique_ptr<interfaces::Wallet>; F = void]’:
3 4./util/result.h:187:15: required from ‘void util::Result<T, F>::MoveConstruct(util::Result<OT, OF>&) [with OT = std::unique_ptr<interfaces::Wallet>; OF = void; T = std::unique_ptr<interfaces::Wallet>; F = void]’
5 6./util/result.h:202:51: required from ‘util::Result<T, F>::Result(util::Result<OT, OF>&&) [with OT = std::unique_ptr<interfaces::Wallet>; OF = void; T = std::unique_ptr<interfaces::Wallet>; F = void]’
7 8wallet/interfaces.cpp:580:16: required from here
910./util/result.h:218:32: warning: missing initializer for member ‘util::detail::ErrorInfo<void>::failure’ [-Wmissing-field-initializers]
11 218 | this->m_info.reset(new detail::ErrorInfo<F>{.errors = std::move(other.m_info->errors),
12 | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13 219 | .warnings = std::move(other.m_info->warnings)});
14 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I’m using gcc 11.2.0 . If you need them all, just let me know
pablomartin4btc
commented at 2:53 pm on August 19, 2022:
member
stickies-v
commented at 3:05 pm on August 19, 2022:
As part of the public interface, I think these could benefit from a very brief per-method docstring (that probably just mirrors the std::optional docstring)?
ryanofsky
commented at 8:38 pm on August 22, 2022:
As part of the public interface, I think these could benefit from a very brief per-method docstring (that probably just mirrors the std::optional docstring)?
I’m a little reluctant to change this code here because it is just moving (not new), but would encourage you to submit a separate PR to add more documentation to existing code if it would be helpful. I’m happy to rebase this as other PR are merged.
I do personally tend not to like documenting individual class members if it’s obvious what they do, and save comments for when there is something non-obvious or higher level to point out, but I wouldn’t have any objection to more detailed documentation.
in
src/util/result.h:104
in
9bd10728baoutdated
99+ friend class ResultBase;
100+
101+public:
102+ //! std::optional methods, so functions returning optional<T> can change to
103+ //! return Result<T> with minimal changes to existing code, and vice versa.
104+ bool has_value() const { return *this ? true : false; }
stickies-v
commented at 3:24 pm on August 19, 2022:
nit: slightly more concise and clear on intent imo
0bool has_value() const { returnbool(*this); }
ryanofsky
commented at 8:38 pm on August 22, 2022:
nit: slightly more concise and clear on intent imo
Ok! I would have defaulted to that but had seen some feedback bool() was not good #25616 (review), #25721 (review) so was trying to avoid it. Switched to bool{} for now.
in
src/test/result_tests.cpp:191
in
9bd10728baoutdated
I think this line is redundant since we already call this->AddError() right above?
I think it was needed to avoid a segfault if the error message was empty, but in any case this line is gone now from implementing your earlier suggestion to dedup this.
I think I prefer if statement to ternary here, and I’m not used to seeing ternary statements rather than expressions in the codebase, but would be happy to change if there are other opinions.
stickies-v
commented at 7:53 pm on August 24, 2022:
Hmm you’re right, we’re not using ternaries as statements it seems. I couldn’t find a single instance of a one-line if ...; else either though, and unfortunately the developer notes are rather ambiguous on the topic.
In that case I’d prefer if and else on separate lines (personal preference and to not be the first in the repo) but no strong view so won’t comment on it further.
in
src/util/result.h:208
in
9bd10728baoutdated
227+ MoveConstruct(other);
228+ return *this;
229+ }
230+ ~Result() { if (*this) this->DestroyValue(); }
231+
232+ //! Operator moving warning and error messages from one result to another.
stickies-v
commented at 4:23 pm on August 19, 2022:
nit:
0 //! Operator moving warning and error messages from other Result to this.
ryanofsky
commented at 8:04 pm on August 23, 2022:
stickies-v
commented at 4:42 pm on August 19, 2022:
Since ErrorInfo doesn’t have a move constructor, do the move semantics make sense here? (I’m still easily confused by move semantics, so I’m probably missing something - sorry)
ryanofsky
commented at 8:04 pm on August 23, 2022:
Since ErrorInfo doesn’t have a move constructor, do the move semantics make sense here? (I’m still easily confused by move semantics, so I’m probably missing something - sorry)
ErrorInfo should have an implicit ErrorInfo(ErrorInfo&&) move constructor, but it actually isn’t relevant here, because the new ErrorInfo object that’s being allocated isn’t being constructed with that constructor. Instead the newly allocated ErrorInfo is aggregate initialized, and the two errors and warnings variables are individually move-constructed, rather than the whole ErrorInfo object being move constructed.
If the question here is whether the std::move calls here have any effect, the answer is yes they do because errors and warnings variables are vectors, and vectors have move constructors.
stickies-v
commented at 6:58 pm on August 24, 2022:
Thank you for the thoughtful response, that was very helpful. Your usage of move semantics here now makes sense to me.
Now I’m just confused that the designated initialization compiles fine even though the spec says it’s since C++20 and I haven’t configured with --enable-c++20 (as does the CI I think)?
ryanofsky
commented at 7:03 pm on August 24, 2022:
Now I’m just confused that the designated initialization compiles fine even though the spec says it’s since C++20 and I haven’t configured with --enable-c++20 (as does the CI I think)?
Oh, we are just outside of the c++17 spec there. The build enables designated initializers as an extension since #24531
in
src/util/result.h:33
in
9bd10728baoutdated
28+}
29+
30+//! Error information only allocated if there are errors or warnings.
31+template <typename F>
32+struct ErrorInfo {
33+ std::optional<std::conditional_t<std::is_same<F, void>::value, std::monostate, F>> failure;
hernanmarino
commented at 4:58 pm on August 19, 2022:
@ryanofsky the following change fixes the warnings for me, but i don’t know if this is the best way to deal with this
0g++ --version
1Apple clang version 13.1.6 (clang-1316.0.21.2.5)2Target: arm64-apple-darwin21.5.0
It’s a beautiful implementation and I’ve learned a lot while reviewing this. That’s both a compliment and a warning that my review shouldn’t weigh heavily, even if I’m doing it as thoroughly as I can. My main concern is that for everyone not already intimately familiar with C++, I think this takes a long time to review thoroughly. The genericness made it difficult to reason about for me. I haven’t come up with a simpler alternative so I don’t think I’ll want that to stand in the way of anything, though. Just something to be mindful of.
Rspigler referenced this in commit
f2f8c5be49
on Aug 21, 2022
Since we’re expecting T to be a container type, would this benefit from living in span.h for reusability? The generic implementation/naming isn’t really necessary in result.h, and people won’t come looking here when they need something to move elements between containers.
It would be ok to move this somewhere, especially if something else was calling it. But I don’t think span.h would be the right place since this shouldn’t work on a span due to spans having fixed size. It also seems ok to me to keep this as a private helper function in a detail namespace as long as it is only called one place.
stickies-v
commented at 7:11 pm on August 24, 2022:
Sorry for the ghost comment. I had realized after posting that this comment about moving it to span.h wasn’t really applicable so I removed it, but apparently not timely enough. I agree with you.
The only further thought I had was that if there’s no need to be generic (yet - and maybe never) it might be worth not doing that to simplify things a bit where possible? E.g. the below is easier to quickly understand imo (and maybe compiles a bit faster?):
0//! Helper to move warnings and errors from one ErrorInfo to another.
1void MoveMessages(std::vector<bilingual_str>& src, std::vector<bilingual_str>& dest)
2{
3 dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));
4 src.clear();
5}
No strong views either way though, so feel free to ignore. Just trying to minimize on what’s already a lot of genericness.
ryanofsky
commented at 3:13 pm on August 25, 2022:
Interesting! I do think the generic version is shorter and easier to understand. But I like moving more code from the .h file to the .cpp file, and I like the consistency between JoinMessages and MoveMessages, so I took this suggestion. Thanks!
ryanofsky force-pushed
on Aug 24, 2022
ryanofsky
commented at 6:30 pm on August 24, 2022:
contributor
Thanks for testing and reviews!
Updated 9bd10728bada8b04d86f5621ee127713f628a9ad -> 10e158a5b57ba3a26e5046a9b42fcc757652f35a (pr/bresult2.13 -> pr/bresult2.14, compare) with suggestions
ryanofsky force-pushed
on Aug 25, 2022
ryanofsky
commented at 4:34 pm on August 25, 2022:
contributor
Updated 10e158a5b57ba3a26e5046a9b42fcc757652f35a -> 5aff7baf375c432746dff6862e9d06064ea1fb18 (pr/bresult2.14 -> pr/bresult2.15, compare) adding MoveMessages suggestion and few more comments and simplifications.
in
src/util/result.h:24
in
5aff7baf37outdated
19+namespace detail {
20+//! Empty string list
21+const std::vector<bilingual_str> EMPTY_LIST{};
2223+//! Helper function to join messages in space separated string.
24+bilingual_str JoinMessages(const std::vector<bilingual_str>& errors, const std::vector<bilingual_str> warnings);
nit: Can use is_same_v instead of ...::value? Also, missing LIFETIMEBOUND?
Thanks, added these improvements
ryanofsky force-pushed
on Aug 30, 2022
ryanofsky
commented at 6:27 pm on August 30, 2022:
contributor
Updated 5aff7baf375c432746dff6862e9d06064ea1fb18 -> 834857e56b8de0bfabee7315622c0211b4a48746 (pr/bresult2.15 -> pr/bresult2.16, compare) with suggestions, and splitting the main commit
36+
37+//! Result base class which is inherited by Result<T, F>.
38+//! T is the type of the success return value, or void if there is none.
39+//! F is the type of the failure return value, or void if there is none.
40+template <typename T, typename F>
41+class ResultBase;
I think it’s a little confusing.
If I understand correctly, template <typename F> class ResultBase<void, F> needs to know about template <typename T, typename F> class ResultBase; but the latter can only be declared after the former because it is a derived class. Therefore, a forward declaration is required.
If the ResultBase classes shouldn’t be used anywhere outside of result.h and their only purpose is to be the base class of Result, I would suggest merging them all into a single class.
ryanofsky
commented at 1:57 am on August 31, 2022:
Hmm, I wonder if you can say more about what is confusing or misleading. This is just a forward declaration for a template class.
It is true that the reason for the forward declaring ResultBase<T, F> is to allow it to inherit from ResultBase<void, F>. So ResultBase<T, F> must be defined after ResultBase<void, F>, but declared before it.
But this is a pretty standard thing for C and C++ code. Definition of one thing 1 depends on declaration of thing 2, so thing 2 needs to be forward declared. It can happen for normal classes and functions as well as templates.
If the ResultBase classes shouldn’t be used anywhere outside of result.h and their only purpose is to be the base class of Result, I would suggest merging them all into a single class.
This isn’t easily possible because Result<void, F> inherits directly from detail::ResultBase<void, F> and does not inherit from detail::ResultBase<T, F>. It does not have an m_value member or value() functions or a dereferencing operator. If the result type T is void the Result class doesn’t hold a value and can’t be dereferenced.
w0xlt approved
w0xlt
commented at 10:42 pm on August 30, 2022:
contributor
262+//! intended for simple applications where there's probably only one error or
263+//! warning message to report, but multiple messages should not be lost if they
264+//! are present. More complicated applications should use GetErrors() and
265+//! GetWarning() methods directly.
266+template <typename T, typename F>
267+bilingual_str ErrorString(const Result<T, F>& result) { return detail::JoinMessages(result.GetErrors(), result.GetWarnings()); }
stickies-v
commented at 10:50 am on August 31, 2022:
Since the friend declaration is gone, I think this should be inline?
Since the friend declaration is gone, I think this should be inline?
Yes, makes sense to be inline.
stickies-v
commented at 11:33 am on August 31, 2022:
contributor
Code Review ACK834857e56
ryanofsky force-pushed
on Sep 1, 2022
ryanofsky
commented at 3:21 pm on September 1, 2022:
contributor
Thanks for the reviews! Just tweaked a few things as suggested.
Updated 834857e56b8de0bfabee7315622c0211b4a48746 -> 82c549aa538a5318fdb56d91117b4c9fc43737de (pr/bresult2.16 -> pr/bresult2.17, compare) with suggestions
ryanofsky force-pushed
on Sep 1, 2022
ryanofsky
commented at 9:02 pm on September 1, 2022:
contributor
Some changes made earlier in #25665#pullrequestreview-1085638384 broke derived-to-base type conversions used in followup PR #25722. Latest push fixes this and adds a test.
Updated 82c549aa538a5318fdb56d91117b4c9fc43737de -> c14e904f66505b3e89ca1138c8d2fa4e3d0916d0 (pr/bresult2.17 -> pr/bresult2.18, compare) adding fix and test for derived to base conversions
in
src/util/result.h:57
in
c14e904f66outdated
52+ {
53+ if (!m_info) m_info = std::make_unique<ErrorInfo<F>>();
54+ return *m_info;
55+ }
56+
57+ //! Value accessors that do nothing this because class has value type T=void.
stickies-v
commented at 3:42 pm on September 7, 2022:
nit: typo
0 //! Value accessors that do nothing because this class has value type T=void.
ryanofsky
commented at 6:59 pm on September 12, 2022:
Hmm, I meant const auto&, not auto, or is auto in this context magically the same as const auto&?
No, I just messed up and unintentionally did a copy. Fixed this and added tests to make sure GetFailure() does not copy.
in
src/util/result.h:241
in
c14e904f66outdated
245+//! intended for simple applications where there's probably only one error or
246+//! warning message to report, but multiple messages should not be lost if they
247+//! are present. More complicated applications should use GetErrors() and
248+//! GetWarning() methods directly.
249+template <typename T, typename F>
250+inline bilingual_str ErrorString(const Result<T, F>& result) { return detail::JoinMessages(result.GetErrors(), result.GetWarnings()); }
maflcko
commented at 1:38 pm on September 12, 2022:
nit in c14e904f66505b3e89ca1138c8d2fa4e3d0916d0:
Can remove inline, as all template are inline by definition.
ryanofsky
commented at 7:02 pm on September 12, 2022:
maflcko
commented at 2:14 pm on September 12, 2022:
nit in 3507da864a1dd7be1bc72ada26d830a4da0c37ae:
Maybe add a comment to explain those a bit more? While the ResultBase() constructor leaves the object uninitialized, the Result constructor guarantees that the object is either filled with a value or an error.
ryanofsky
commented at 7:03 pm on September 12, 2022:
Maybe add a comment to explain those a bit more? While the ResultBase() constructor leaves the object uninitialized, the Result constructor guarantees that the object is either filled with a value or an error.
Sure, I moved the DestroyValue call to this ~ResultBase destructor instead of the other Result constructor to make this more self contained and clear. Also added a comment explaining why the empty constructor is required. Hopefully these are improvements. Also happy to make other changes to clarify.
in
src/util/result.h:153
in
3507da864aoutdated
165+
166+ template <typename, typename>
167+ friend class Result;
168+
169+public:
170+ //! Constructors, destructor, and assignment operator.
maflcko
commented at 2:18 pm on September 12, 2022:
nit in 3507da864a1dd7be1bc72ada26d830a4da0c37ae:
doxygen will attach the comment to the constructor. In any case, I think this can be removed since it is not adding any info that isn’t already there.
ryanofsky
commented at 7:04 pm on September 12, 2022:
doxygen will attach the comment to the constructor. In any case, I think this can be removed since it is not adding any info that isn’t already there.
Thanks, dropped. This was used to group methods in an earlier version of PR that had more methods below.
in
src/util/result.h:41
in
3507da864aoutdated
36+class ResultBase<void, F>
37+{
38+protected:
39+ std::unique_ptr<ErrorInfo<F>> m_info;
4041+ //! Value accessors that do nothing this because class has value type T=void.
maflcko
commented at 2:23 pm on September 12, 2022:
nit in 3507da864a1dd7be1bc72ada26d830a4da0c37ae:
Are those really “accessors”, not “setters”?
ryanofsky
commented at 7:05 pm on September 12, 2022:
ryanofsky
commented at 3:22 pm on September 13, 2022:
contributor
Thanks for the reviews! New pushes implement all the suggested changes.
Updated c14e904f66505b3e89ca1138c8d2fa4e3d0916d0 -> 05a97d3208cc365cdeac9de281529568b3cd056c (pr/bresult2.18 -> pr/bresult2.19, compare) with suggestions. Also replaced operator« with operator» to simplify followup PR #25722 a bit
Rebased 05a97d3208cc365cdeac9de281529568b3cd056c -> e04d8a754ff1b25cab483996319a583e6e3e680a (pr/bresult2.19 -> pr/bresult2.20, compare) due to conflict with #24513
maflcko
commented at 5:36 pm on September 13, 2022:
member
ryanofsky
commented at 7:17 pm on September 13, 2022:
contributor
Updated e04d8a754ff1b25cab483996319a583e6e3e680a -> 52a4e50fb4b1171ee0f6814b0a50bc70cdd77134 (pr/bresult2.20 -> pr/bresult2.21, compare) fixing unintentional GetFailure copy introduced last push, and adding test to detect this copy
in
src/util/result.h:271
in
52a4e50fb4outdated
219+ if (!warning.empty()) this->Info().warnings.emplace_back(std::move(warning));
220+ }
221+
222+ //! Operator moving warning and error messages from this result object to
223+ //! another one. Only moves message strings, does not change success or
224+ //! failure values of either Result object.
maflcko
commented at 9:34 am on September 14, 2022:
52a4e50fb4b1171ee0f6814b0a50bc70cdd77134: I don’t like that this takes over error strings, but leaves the value and failure untouched. It seems fine to have a result and warnings, but having a result and also an error at the same time seems odd.
Same with operator=. I think this is the first time I’ve seen that after calling =, state is preserved from before = was called.
ryanofsky
commented at 8:37 pm on September 14, 2022:
52a4e50: I don’t like that this takes over error strings, but leaves the value and failure untouched. It seems fine to have a result and warnings, but having a result and also an error at the same time seems odd.
Agree having both a result value and an error message should be avoided. Also having neither a result value nor an error message should be avoided. But there are tradeoffs around where and how strictly you want to enforce these things. The main thing currently enforcing value/error consistency is having a constructor that sets error messages and failure values at the same time and does not allow setting a result value, and a constructor that only sets a success value and does not allow setting an error message.
But this leaves open the question of what helpers functions like operator>> should do when they combine multiple result values that have already been constructed.
The use-case for operator>> is when you have an outer function returning Result<T, F> calling inner functions returning Result<T1, F1>, and Result<T2, F2>, etc. Examples would be LoadWalletInternal, CreateWallet, and DoMigration from #25722. The outer function can handle failures from inner functions it calls any way it wants: passing failure values up to its caller, translating failure values, ignoring failures, retrying after failures, falling back to an alternate approaches, etc. It sets success and failure values directly, and it can use operator>> to collect error and warning messages separately and pass them along. I don’t think operator>> should be involved in success and failure value handling. I think it would be bad if operator>> discarded error messages, or it it threw runtime exceptions, instead of just passing messages on to ultimately get displayed or logged.
In C++ generally operator>> is used for things as varied as bit shifting and stream I/O and can be interpreted as “move data from this place to another place” so I think it reasonable that this operator>> just moves error and warning messages from one Result object to another, as long as behavior is clearly documented.
Same with operator=. I think this is the first time I’ve seen that after calling =, state is preserved from before = was called.
Yes it is true that assigning to an existing result does not erase warning and errors messages already accumulated in the result. It only sets the success or failure value and appends new warning and error messages to existing ones.
I think this this behavior is safe and useful. Setting a value should not automatically erase warning and error messages that are meant to be displayed or logged. But if this behavior is too surprising for an operator= method, we don’t actually need to make Result assignable, and could rename operator=() to Set() or SetValue(). It looks like even after #25722 there are only 3 operator= calls in the codebase outside of tests, so this would be an easy change.
maflcko
commented at 7:53 am on September 15, 2022:
Also having neither a result value nor an error message should be avoided.
Well, this is already impossible for the reasons you gave. (Edit: When calling only the constructors)
Agree having both a result value and an error message should be avoided.
Then, why not make it impossible? If there is an outer function returning Result<T, F> that wants to combine Result<T1, F1> and Result<T2, F2>, then it seems better if it explicitly takes (moves) the result T1/F1 out and translates it into T/F. For example, if it passes up a failure value, it seems best to just create a fresh Result (of the outer type) with the failure and return that. (Same if it translates failure values). If it ignores failures, it would be good to translate them to warnings first and not blindly take them over as errors with the >> operator. (Same if it retries or falls back).
I think this this behavior is safe and useful.
Why couldn’t the same be achieved by explicitly constructing a new Result with either an error or a value?
ryanofsky
commented at 9:24 am on September 15, 2022:
Also having neither a result value nor an error message should be avoided.
Well, this is already impossible for the reasons you gave.
“Should be avoided” means that users should avoid it, and the implementation makes it easy to avoid by default. The reasons I gave were reasons why a function combining multiple result values needs to either (1) allow result values and error messages to exist at the same time or (2) discard error messages or result values or (3) throw exceptions. And I believe the best choice for operator>> is (1), just to be a plain message mover that moves message strings and leaves result values alone. I linked to use-cases showing how this works in practice.
Agree having both a result value and an error message should be avoided.
Then, why not make it impossible? If there is an outer function returning Result<T, F> that wants to combine Result<T1, F1> and Result<T2, F2>, then it seems better if it explicitly takes (moves) the result T1/F1 out and translates it into T/F. For example, if it passes up a failure value, it seems best to just create a fresh Result (of the outer type) with the failure and return that.
This is actually what the implementation does. But if the outer function returns success value, and there are error messages, I don’t think it is good default behavior to throw away the error messages or raise an exception. I think the best default behavior is to keep error messages so they can be displayed or logged.
(Same if it translates failure values). If it ignores failures, it would be good to translate them to warnings first and not blindly take them over as errors with the >> operator. (Same if it retries or falls back).
So change you are asking for there is basically:
0--- a/src/util/result.h
1+++ b/src/util/result.h
2@@ -222,7 +222,7 @@ public:
3 Result&& operator>>(O&& other LIFETIMEBOUND) &&
4 {
5 if (this->m_info) {
6- if (!this->m_info->errors.empty()) detail::MoveMessages(this->m_info->errors, other.Info().errors);
7+ if (!this->m_info->errors.empty()) detail::MoveMessages(this->m_info->errors, other ? other.Info().warnings : other.Info().errors);
8 if (!this->m_info->warnings.empty()) detail::MoveMessages(this->m_info->warnings, other.Info().warnings);
9 }
10 return std::move(*this);
I wouldn’t object to it, but it just seems more invasive and doesn’t offer practical benefits.
I think this this behavior is safe and useful.
Why couldn’t the same be achieved by explicitly constructing a new Result with either an error or a value?
because it doesn’t require introducing multiple result variables. If you are trying to get rid of both operator= and operator>>, I believe operator= or Set is also better than:
I’m happy to rename operator= to Set if you think operator= is misleading. But if you look at the places where these functions are used, it is easier to see why they are useful. Conversely, if you think there is a footgun here, it would be helpful to see an example of the footgun.
ryanofsky
commented at 9:54 am on September 15, 2022:
But if you look at the places where these functions are used, it is easier to see why they are useful.
ryanofsky
commented at 3:30 pm on September 15, 2022:
Went ahead and renamed operator= to Set for now. Seems like a good way to avoid some confusion.
ryanofsky force-pushed
on Sep 15, 2022
ryanofsky
commented at 3:31 pm on September 15, 2022:
contributor
Updated 52a4e50fb4b1171ee0f6814b0a50bc70cdd77134 -> f9accbc6e296adadac374eca085f8b2ce095c8a4 (pr/bresult2.21 -> pr/bresult2.22, compare) just renaming operator= to Set to avoid some confusion
Updated f9accbc6e296adadac374eca085f8b2ce095c8a4 -> 776d9b3fbb4cf83c81cc38c44cae10d3f3344b1b (pr/bresult2.22 -> pr/bresult2.23, compare) tweaking commit message
Rebased 776d9b3fbb4cf83c81cc38c44cae10d3f3344b1b -> 456e3d4eccf010eba30096061b83adc45c371b92 (pr/bresult2.23 -> pr/bresult2.24, compare) due to conflict with #25499
Rebased 456e3d4eccf010eba30096061b83adc45c371b92 -> 28a6934da980703006e028776d276ae77121c586 (pr/bresult2.24 -> pr/bresult2.25, compare) due to conflict with #25667
Rebased 28a6934da980703006e028776d276ae77121c586 -> f4d55d858d9da08612a8ba3b7ceeaf36dfe6cc30 (pr/bresult2.25 -> pr/bresult2.26, compare) due to conflicts with #26289 and #26661
ryanofsky force-pushed
on Sep 15, 2022
DrahtBot added the label
Needs rebase
on Sep 16, 2022
ryanofsky force-pushed
on Sep 20, 2022
DrahtBot removed the label
Needs rebase
on Sep 20, 2022
DrahtBot added the label
Needs rebase
on Oct 13, 2022
ryanofsky force-pushed
on Oct 14, 2022
DrahtBot removed the label
Needs rebase
on Oct 14, 2022
DrahtBot added the label
Needs rebase
on Jan 3, 2023
ryanofsky force-pushed
on Jan 6, 2023
DrahtBot removed the label
Needs rebase
on Jan 6, 2023
janus referenced this in commit
56ca68bb23
on Jan 20, 2023
DrahtBot added the label
Needs rebase
on Jan 27, 2023
ryanofsky force-pushed
on Feb 10, 2023
ryanofsky force-pushed
on Feb 10, 2023
DrahtBot removed the label
Needs rebase
on Feb 10, 2023
hebasto
commented at 2:54 pm on February 12, 2023:
member
Concept ACK.
hebasto
commented at 3:27 pm on February 12, 2023:
member
It seems the 7cdb7d1e9573ae60e7335af5d3de99191ad68b3f commit adds src/wallet/test/availablecoins_tests.cpp by accident, doesn’t it?
hebasto
commented at 5:42 pm on February 12, 2023:
member
In every other case, braces are required, and the then and else clauses must appear correctly indented on a new line.
in
src/util/result.h:159
in
eb50fcd685outdated
145+//!
146+//! Most code does not need different error-handling behavior for different
147+//! types of errors, and can suffice just using the type `T` success value on
148+//! success, and descriptive error strings when there's a failure. But
149+//! applications that do need more complicated error-handling behavior can
150+//! override the default `F = void` failure type and get failure values by
maflcko
commented at 10:42 am on February 13, 2023:
With multiple warning and error messages this may already be incompatible, though?
Yes, if you are thinking that util::Result could wrap std::expected, that would be probably be awkward and not worth it.
But I don’t think there is a conflict because the two classes are mostly doing different things. The util::Result class is mostly providing error-reporting functionality (passing error and warning strings up to the user). The std::expected class is only providing error-handling functionality (passing failure and success values between functions). Here is how I would choose the between the two classes:
If your function never fails, it should just return success value directly.
If your function can fail, but doesn’t provide any error strings or specific failure information, it should return std::optional.
If your function can fail, and provides failure information but not error strings, it should return std::expected.
If your function can fail, and generates error or warning strings it should return util::Result.
We have a lot of functions that generate error strings as you can see by all the code using util::Error and util::Result presently, and in more code that is converted to use util::Result in this PR and #25722. After std::expected is available, most of these functions still be better off using util::Result instead of std::expected so they are able to pass back error strings in a uniform way.
But when std::expected is available, we may want to tweak the util::Result class to make it easier to switch between std::expected and util::Result with minimal code changes. For example, the util::Result already has a value_or method to be compatible with std::optional. It could also have and_then and or_else methods to be compatible with std::expected.
ryanofsky force-pushed
on Feb 16, 2023
ryanofsky
commented at 6:54 pm on February 16, 2023:
contributor
DrahtBot added the label
Needs rebase
on Feb 22, 2023
ryanofsky force-pushed
on Mar 1, 2023
DrahtBot removed the label
Needs rebase
on Mar 1, 2023
DrahtBot added the label
Needs rebase
on Mar 8, 2023
ryanofsky force-pushed
on Mar 16, 2023
DrahtBot removed the label
Needs rebase
on Mar 16, 2023
DrahtBot added the label
Needs rebase
on Mar 23, 2023
ryanofsky force-pushed
on Apr 4, 2023
DrahtBot removed the label
Needs rebase
on Apr 4, 2023
DrahtBot added the label
CI failed
on Apr 24, 2023
ryanofsky force-pushed
on May 2, 2023
DrahtBot removed the label
CI failed
on May 2, 2023
hernanmarino approved
hernanmarino
commented at 4:42 pm on May 11, 2023:
contributor
re ACK28a954c7034077ac3a45083dd5e2b5cdb4d4cdde
DrahtBot requested review from maflcko
on May 11, 2023
DrahtBot requested review from stickies-v
on May 11, 2023
DrahtBot requested review from w0xlt
on May 11, 2023
ryanofsky
commented at 5:30 pm on May 11, 2023:
contributor
Thanks for the review.
Note: @martinus left several review comments on #25722#pullrequestreview-1386736519, which is based on this PR, which apply to this PR and can improve it a little. I’m planning to update this PR to incorporate the suggestions.
DrahtBot removed review request from w0xlt
on May 11, 2023
DrahtBot requested review from w0xlt
on May 11, 2023
DrahtBot added the label
Needs rebase
on May 26, 2023
maflcko
commented at 1:35 pm on May 26, 2023:
member
void can be removed from OP?
DrahtBot removed review request from w0xlt
on May 26, 2023
DrahtBot requested review from w0xlt
on May 26, 2023
in
src/test/result_tests.cpp:159
in
28a954c703outdated
136+util::Result<int, int> TruthyFalsyFn(int i, bool success)
137+{
138+ if (success) return i;
139+ return {util::Error{Untranslated(strprintf("failure value %i.", i))}, i};
140+}
141+
TheCharlatan
commented at 4:28 pm on June 19, 2023:
Can you add an example for when the Result error type in a chain of function calls remains the same, but the value type changes? Is there a better way to do this conversion than manually constructing the result again (like below)?
0util::Result<std::string, FnError> CastFailFn() {
1 auto res = IntFailFn(1, false);
2 return {util::Error{ErrorString(res)}, res.GetFailure()};
3}
Can you add an example for when the Result error type in a chain of function calls remains the same, but the value type changes? Is there a better way to do this conversion than manually constructing the result again (like below)?
Yes, there is definitely a better way to pass along errors and warnings from one result to another without calling ErrorString and flattening them. The util::Result constructor will move errors and warnings directly from an existing result object if you just pass the other result object as an argument with std::move. If you look at the MultiWarnFn there was an example of this feature in the success case. But I added a new StrFailFn function now based on your code that demonstrates it in both the success and failure cases.
I am also thinking of adding a util::Messages{Result&&} helper similar to existing util::Error{std::string} and util::Warning{std::string} helpers to make it more obvious how you can construct a Result value with errors and warning from different sources.
Revisiting this makes me realize I should probably add a tutorial-style document that describes how to use the Result class with standalone functions that return util::Result, chained functions that return util::Result values of the same type, and chained functions that return util::Result values of different types.
I should probably add a tutorial-style document that describes how to use the Result class with standalone functions that return util::Result, chained functions that return util::Result values of the same type, and chained functions that return util::Result values of different types.
I think this would be valuable, either in util/result.h directly or in doc/developer-notes.md or another file in /doc.
TheCharlatan
commented at 2:33 pm on July 27, 2023:
I am also thinking of adding a util::Messages{Result&&} helper.
That sounds like a worthwhile improvement of the ergonomics here.
I am also thinking of adding a util::Messages{Result&&} helper.
That sounds like a worthwhile improvement of the ergonomics here.
Thanks, added this
DrahtBot removed review request from w0xlt
on Jun 19, 2023
DrahtBot requested review from w0xlt
on Jun 19, 2023
ryanofsky force-pushed
on Jul 21, 2023
ryanofsky
commented at 4:47 pm on July 21, 2023:
contributor
Rebased 28a954c7034077ac3a45083dd5e2b5cdb4d4cdde -> 40f09de73e61e7ae62d6639a49b7c7ac48d514d9 (pr/bresult2.33 -> pr/bresult2.34, compare) due to conflict with various PR and making many suggested changes from review #25722 (which is based on this PR)
Thanks, no longer mentioning it since #25977 added support for Result<void>
DrahtBot removed the label
Needs rebase
on Jul 21, 2023
DrahtBot added the label
CI failed
on Jul 21, 2023
in
src/util/result.h:47
in
f1b46f4017outdated
38@@ -39,13 +39,23 @@ class Result
3940 std::variant<bilingual_str, T> m_variant;
4142+ //! Make operator= private and instead require explicit Set() calls to
43+ //! avoid confusion in the future when the Result class gains support for
44+ //! richer errors and callers want to set result values without erasing
45+ //! error strings.
46+ Result& operator=(const Result&) = default;
47+ Result& operator=(Result&&) = default;
0init.cpp:948:12: error: 'operator=' is a private member of 'util::Result<void>'
1 result = init::SetLoggingLevel(args);
2 ~~~~~~ ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
3./util/result.h:47:13: note: declared private here
4 Result& operator=(Result&&) = default;
5 ^
0The locale dependent function std::to_string(...) appears to be used:
1src/test/result_tests.cpp:101: return {std::move(result), std::to_string(*result)};
0@@ -0,0 +1,28 @@
1+// Copyright (c) 2022 The Bitcoin Core developers
2+// Distributed under the MIT software license, see the accompanying
3+// file COPYING or https://www.opensource.org/licenses/mit-license.php.
4+
5+#include <util/result.h>
6+#include <util/string.h>
In https://github.com/bitcoin/bitcoin/commit/332e847c9ec0241efd9681eee3b03ff819aaddc3 and 4d995d3fa66fbc3eb87c6627e5ba1b2a809402a4, I wonder if some of the custom operator (i.e. move) definitions should have a noexcept-specification. Also, notating any methods where it would be incorrect if the return value isn’t checked (e.g. for error-handling) and optionally getter-like pure functions with nodiscard may aid reviewers / readers of the code.
DrahtBot removed review request from w0xlt
on Jul 21, 2023
DrahtBot requested review from hernanmarino
on Jul 21, 2023
DrahtBot requested review from w0xlt
on Jul 21, 2023
DrahtBot removed review request from hernanmarino
on Jul 21, 2023
DrahtBot removed review request from w0xlt
on Jul 21, 2023
DrahtBot requested review from hernanmarino
on Jul 21, 2023
DrahtBot requested review from w0xlt
on Jul 21, 2023
DrahtBot removed the label
CI failed
on Jul 21, 2023
DrahtBot removed review request from hernanmarino
on Jul 23, 2023
DrahtBot removed review request from w0xlt
on Jul 23, 2023
DrahtBot requested review from hernanmarino
on Jul 23, 2023
DrahtBot requested review from w0xlt
on Jul 23, 2023
in
src/test/result_tests.cpp:100
in
4d995d3fa6outdated
96 }
97 98+util::Result<std::string, FnError> StrFailFn(int i, bool success)
99+{
100+ auto result = IntFailFn(i, success);
101+ if (!success) return {std::move(result), util::Error{Untranslated("str error")}, result.GetFailure()};
TheCharlatan
commented at 2:57 pm on July 27, 2023:
It strikes me as unfortunate that the move constructor cannot move the failure values across different result value types, meaning the failure needs to be passed in as a separate argument. At the same time the user would be allowed to return a {std::move(result), util::Error{Untranslated("str error")}, potentially without the user noticing that this will not move the failure value ~and instead initialize a Monostate failure~ and instead default construct the failure value.
It strikes me as unfortunate that the move constructor cannot move the failure values across different result value types, meaning the failure needs to be passed in as a separate argument. At the same time the user would be allowed to return a {std::move(result), util::Error{Untranslated("str error")}, potentially without the user noticing that this will not move the failure value ~and instead initialize a Monostate failure~ and instead default construct the failure value.
Yes agree that would have been misleading. This should be fixed with the new util::Messages helper. Now just passing a bare std::move(result) is no longer allowed, so it shouldn’t look like the failure value is being moved. If you want to return a value from a Result<T1, F> function using a Result<T2, F> result value, now you have to write:
I’m not sure how common it will be to have functions calling each other that have different success types but the same failure type. If it does turn out to be common, it should be possible to add syntax sugar so that can be shortened to:
0return {util::Error{}, std::move(result)};
DrahtBot removed review request from hernanmarino
on Jul 27, 2023
DrahtBot removed review request from w0xlt
on Jul 27, 2023
DrahtBot requested review from hernanmarino
on Jul 27, 2023
DrahtBot requested review from w0xlt
on Jul 27, 2023
DrahtBot removed review request from hernanmarino
on Jul 27, 2023
DrahtBot removed review request from w0xlt
on Jul 27, 2023
DrahtBot requested review from hernanmarino
on Jul 27, 2023
DrahtBot requested review from w0xlt
on Jul 27, 2023
in
src/util/result.h:230
in
775b54e881outdated
229+ // Result::Set() method instead to set a result value while keeping any
230+ // existing errors and warnings.
231+ template <typename O>
232+ Result& operator=(O&& other) = delete;
233+
234+ Result& Set(Result&& other) LIFETIMEBOUND
Would it make sense to rename this to emplace, to keep the interface in line with optional and expected?
This isn’t an actually an emplace method, it’s an assignment method. An emplace method for a Result<T> object would take arguments that would be accepted by one of T’s constructors, and construct a new T object in place with them.
By contrast, this method doesn’t accept arguments that could be forwarded to a T constructor. Instead this method takes a Result<T> argument, and is basically the same as an operator=(Result&&) method.
Added better documentation about this in the “multiple error and warning messages” commit:
0//! Move success or failure values from another result object to this
1//! object. Also move any error and warning messages from the other result
2//! object to this one. If this result object has an existing success or
3//! failure value it is cleared and replaced by the other value. If this
4//! result object has any error or warning messages, they are not cleared
5//! the messages will accumulate.
6 Result& Set(Result&& other) LIFETIMEBOUND
in
src/util/result.h:102
in
775b54e881outdated
98+public:
99+ //! std::optional methods, so functions returning optional<T> can change to
100+ //! return Result<T> with minimal changes to existing code, and vice versa.
101+ bool has_value() const { return bool{*this}; }
102+ const T& value() const LIFETIMEBOUND { assert(*this); return m_value; }
103+ T& value() LIFETIMEBOUND { assert(*this); return m_value; }
nit: would m_error or (m_error_info) be a more appropriate name? E.g. in bool(), the meaning of !m_error is much more intuitive at first sight compared to !m_info, I think? (i.e. it’s not really clear what it means to “not have info”, whereas “not have error” is clear).
nit: would m_error or (m_error_info) be a more appropriate name? E.g. in bool(), the meaning of !m_error is much more intuitive at first sight compared to !m_info, I think? (i.e. it’s not really clear what it means to “not have info”, whereas “not have error” is clear).
Calling it m_error would be misleading in later commits. The purpose of this field isn’t to indicate the presence of an error. It happens to do that temporarily in the “Add util::Result failure values” commit, but in later commit “Add util::Result multiple error and warning messages”, it can hold error and warning messages even in the success state.
stickies-v
commented at 4:55 pm on July 28, 2023:
contributor
Will continue my (re-)review next week, this is mostly up until 332e847c9ec0241efd9681eee3b03ff819aaddc3
DrahtBot removed review request from hernanmarino
on Jul 28, 2023
DrahtBot removed review request from w0xlt
on Jul 28, 2023
DrahtBot requested review from hernanmarino
on Jul 28, 2023
DrahtBot requested review from stickies-v
on Jul 28, 2023
DrahtBot requested review from w0xlt
on Jul 28, 2023
DrahtBot removed review request from hernanmarino
on Jul 28, 2023
DrahtBot removed review request from w0xlt
on Jul 28, 2023
DrahtBot requested review from hernanmarino
on Jul 28, 2023
DrahtBot requested review from w0xlt
on Jul 28, 2023
DrahtBot removed review request from hernanmarino
on Jul 28, 2023
DrahtBot removed review request from w0xlt
on Jul 28, 2023
DrahtBot requested review from hernanmarino
on Jul 28, 2023
DrahtBot requested review from w0xlt
on Jul 28, 2023
ryanofsky force-pushed
on Aug 1, 2023
ryanofsky
commented at 8:26 pm on August 1, 2023:
contributor
I still want to do more work to make the result class to enforce more safety with bool/optional/pointer types as discussed #25722 (review), and work better with the ResultPtr class from #26022. I also want to write better documentation with usage examples. So I’ll keep working on this, and push more changes here or in followup PRs.
DrahtBot added the label
CI failed
on Aug 1, 2023
ryanofsky force-pushed
on Aug 1, 2023
ryanofsky
commented at 10:37 pm on August 1, 2023:
contributor
584e3fa Not sure, but these if (!result) return result; idioms (here and lines 228-229 below) seem “odd” enough that an explanatory comment might be helpful.
584e3fa Not sure, but these if (!result) return result; idioms (here and lines 228-229 below) seem “odd” enough that an explanatory comment might be helpful.
I think this will probably be a common pattern and it would be too verbose to explain what is happening each place it is used. If the code were misleading, I think I would want to do something to fix it now. But if it’s just a little unusual looking, I think that’s expected at this point, and we can see what improvements would be useful in the future as it is used more widely.
in
src/util/result.h:30
in
08f5febfc5outdated
26+bilingual_str JoinMessages(const std::vector<bilingual_str>& errors, const std::vector<bilingual_str>& warnings);
27+
28+//! Helper function to move messages from one vector to another.
29+void MoveMessages(std::vector<bilingual_str>& src, std::vector<bilingual_str>& dest);
30+
31+//! Subsitute for std::monostate that doesn't depend on std::variant.
0//! Substitute for std::monostate that doesn't depend on std::variant.
0src/util/result.h:30: Subsitute ==> Substitute
1src/util/result.h:200: OT ==> TO, OF, OR, NOT
2src/util/result.h:201: OT ==> TO, OF, OR, NOT
3src/util/result.h:221: OT ==> TO, OF, OR, NOT
4src/util/result.h:222: OT ==> TO, OF, OR, NOT
5^ Warning: codespell identified likely spelling errors. Any false positives? Add them to the list of ignored words in test/lint/spelling.ignore-words.txt
in
src/test/result_tests.cpp:6
in
08f5febfc5outdated
1@@ -2,6 +2,7 @@
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
45+#include <util/check.h>
6ddcc9beGetErrors() and GetWarnings() don’t seem to have any test coverage yet. Note that there is also already a GetWarnings() in src/warnings.{h.cpp}, perhaps disambiguate naming/grepping-wise in addition to namespace-wise.
6ddcc9bGetErrors() and GetWarnings() don’t seem to have any test coverage yet. Note that there is also already a GetWarnings() in src/warnings.{h.cpp}, perhaps disambiguate naming/grepping-wise in addition to namespace-wise.
Thanks, added some test coverage. On the warnings.h overlap, I think that GetWarnings() is not a great name for a top level function, and that function would be better to rename (or delete) regardless.
jonatack
commented at 11:45 pm on August 2, 2023:
member
ACK08f5febfc571220043436bbec96a326beebdee22
Some non-blocking comments follow.
DrahtBot removed review request from hernanmarino
on Aug 2, 2023
DrahtBot removed review request from w0xlt
on Aug 2, 2023
DrahtBot requested review from hernanmarino
on Aug 2, 2023
DrahtBot requested review from w0xlt
on Aug 2, 2023
DrahtBot removed review request from hernanmarino
on Aug 2, 2023
DrahtBot removed review request from w0xlt
on Aug 2, 2023
DrahtBot requested review from hernanmarino
on Aug 2, 2023
DrahtBot requested review from w0xlt
on Aug 2, 2023
ryanofsky force-pushed
on Aug 3, 2023
ryanofsky
commented at 8:42 pm on August 3, 2023:
contributor
Updated 08f5febfc571220043436bbec96a326beebdee22 -> b0beb4c504da29c27358d4602a045aaab39305f6 (pr/bresult2.37 -> pr/bresult2.38, compare) moving a lot more functionality from the Result class to the ResultBase class so the new code can be compatible with the ResultPtr class from #26022. Also rewrote and added more documentation and implemented latest review suggestions.
DrahtBot added the label
CI failed
on Aug 3, 2023
ryanofsky force-pushed
on Aug 4, 2023
ryanofsky
commented at 11:26 am on August 4, 2023:
contributor
835f094 It looks like this empty vector might be declarable constexpr, at least with clang 16.0.6 arm64. Possibly not uniformly until C++20.
This is a good idea, but I think you are right it requires c++20. With my compiler (clang 13) I see:
0./util/result.h:85:38:error: constexpr variable cannot have non-literal type 'const std::vector<bilingual_str>'1constexpr std::vector<bilingual_str> EMPTY_LIST{};
2^3/nix/store/acbklvmaxi32lj3f7k1m1y00017f89ix-gcc-11.3.0/include/c++/11.3.0/bits/stl_vector.h:389:11:note: 'vector<bilingual_str>' is not literal because it is not an aggregate and has no constexpr constructors other than copy or move constructors
4classvector:protected _Vector_base<_Tp, _Alloc>
in
src/util/result.h:286
in
9e80d0b754outdated
282+class Result : public detail::ResultBase<T, F>
283 {
284- return result ? bilingual_str{} : std::get<0>(result.m_variant);
285-}
286+protected:
287+ using Base = detail::ResultBase<T, F>;
9ec1bda Can Base here be private instead of protected?
It could, but in general I prefer to use protected over private when it is safe for extensibility and testing, and in this case I want to support adding a ResultPtr type like the one in #26022 which inherits from Result. Also, Base is just a type alias for a public type, so making it private could only make the alias private, not the type, and only force type declarations to be more verbose.
Conceptually, I think the inheritance used in result.h is basically just an implementation detail, and that ResultBase/Result/ResultPtr classes should act like a single class and have full access to result state without any unnecessary obstacles or verbosity or layers of abstraction. In the future it would even be nice to combine Result and ResultBase classes together using c++23’s deduced this or maybe doing it without waiting for c++23 using the CRTP pattern mentioned at that link.
in
src/util/result.h:301
in
9e80d0b754outdated
301+ }
302+
303+ //! Move-construct a Result object from another Result object, moving the
304+ //! success or failure value any any error or warning messages.
305+ template <typename OT, typename OF>
306+ Result(Result<OT, OF>&& other)
0src/util/result.h:300: OT ==> TO, OF, OR, NOT
1src/util/result.h:301: OT ==> TO, OF, OR, NOT
2^ Warning: codespell identified likely spelling errors. Any false positives? Add them to the list of ignored words in test/lint/spelling.ignore-words.txt
0^ Warning: codespell identified likely spelling errors. Any false positives? Add them to the list of ignored words in test/lint/spelling.ignore-words.txt
Thanks, added
jonatack
commented at 5:08 pm on August 4, 2023:
member
ACK9e80d0b754a28733c79a52c8e0431616c31d071c
Nice documentation, code, and commit message improvements. Some non-blocking comments. Consider also making swap/move/destructors in Result/ResultBase noexcept. FWIW I didn’t hit the CI build error in the previous push with arm64 clang 16.0.6.
DrahtBot removed review request from hernanmarino
on Aug 4, 2023
DrahtBot removed review request from w0xlt
on Aug 4, 2023
DrahtBot requested review from hernanmarino
on Aug 4, 2023
DrahtBot requested review from w0xlt
on Aug 4, 2023
DrahtBot removed review request from hernanmarino
on Aug 4, 2023
DrahtBot removed review request from w0xlt
on Aug 4, 2023
DrahtBot requested review from hernanmarino
on Aug 4, 2023
DrahtBot requested review from w0xlt
on Aug 4, 2023
DrahtBot removed review request from hernanmarino
on Aug 4, 2023
DrahtBot removed review request from w0xlt
on Aug 4, 2023
DrahtBot requested review from hernanmarino
on Aug 4, 2023
DrahtBot requested review from w0xlt
on Aug 4, 2023
DrahtBot removed review request from hernanmarino
on Aug 4, 2023
DrahtBot removed review request from w0xlt
on Aug 4, 2023
DrahtBot requested review from hernanmarino
on Aug 4, 2023
DrahtBot requested review from w0xlt
on Aug 4, 2023
DrahtBot removed review request from hernanmarino
on Aug 4, 2023
DrahtBot removed review request from w0xlt
on Aug 4, 2023
DrahtBot requested review from hernanmarino
on Aug 4, 2023
DrahtBot requested review from w0xlt
on Aug 4, 2023
DrahtBot removed review request from hernanmarino
on Aug 4, 2023
DrahtBot removed review request from w0xlt
on Aug 4, 2023
DrahtBot requested review from hernanmarino
on Aug 4, 2023
DrahtBot requested review from w0xlt
on Aug 4, 2023
ryanofsky force-pushed
on Aug 7, 2023
ryanofsky
commented at 6:06 pm on August 7, 2023:
contributor
Updated 9e80d0b754a28733c79a52c8e0431616c31d071c -> 7f883b33bb89205a9d00c2d20d363a36a0167c7c (pr/bresult2.39 -> pr/bresult2.40, compare) responding to new review comments and also making some internal changes within the PR to reduce unnecessary diffs between commits.
jonatack
commented at 6:39 pm on August 7, 2023:
member
ACK7f883b33bb89205a9d00c2d20d363a36a0167c7c
DrahtBot removed review request from hernanmarino
on Aug 7, 2023
DrahtBot removed review request from w0xlt
on Aug 7, 2023
DrahtBot requested review from hernanmarino
on Aug 7, 2023
DrahtBot requested review from w0xlt
on Aug 7, 2023
This line is missing test coverage. It could be achieved with the following diff (though there might be a more elegant way to achieve this):
Thanks, I added a test just calling the Set method to cover these branches. Suggestion to modify the AccumulateFn function instead was good, too, just seemed more complicated.
TheCharlatan
commented at 7:34 am on August 29, 2023:
contributor
ACK7f883b33bb89205a9d00c2d20d363a36a0167c7c
DrahtBot removed review request from hernanmarino
on Aug 29, 2023
DrahtBot removed review request from w0xlt
on Aug 29, 2023
DrahtBot requested review from hernanmarino
on Aug 29, 2023
DrahtBot requested review from w0xlt
on Aug 29, 2023
DrahtBot removed review request from hernanmarino
on Aug 29, 2023
DrahtBot removed review request from w0xlt
on Aug 29, 2023
DrahtBot requested review from hernanmarino
on Aug 29, 2023
DrahtBot requested review from w0xlt
on Aug 29, 2023
achow101
commented at 6:51 pm on August 29, 2023:
member
ACK7f883b33bb89205a9d00c2d20d363a36a0167c7c
achow101
commented at 6:54 pm on August 29, 2023:
member
Silent merge conflict:
0In file included from ../../../src/wallet/coinselection.h:16,
1 from ../../../src/wallet/test/fuzz/coinselection.cpp:12:
2../../../src/util/result.h: In instantiation of ‘void util::detail::ResultBase<T, F>::ConstructValue(Args&&...) [with Args = {util::Result<wallet::SelectionResult, void>&}; T = wallet::SelectionResult; F = void]’:
3../../../src/util/result.h:141:34: required from ‘static void util::detail::ResultBase<void, F>::ConstructResult(Result&, Args&&...) [with bool Failure = false; Result = util::Result<wallet::SelectionResult>; Args = {util::Result<wallet::SelectionResult, void>&}; F = void]’ 4../../../src/util/result.h:295:58: required from ‘util::Result<T, F>::Result(Args&&...) [with Args = {util::Result<wallet::SelectionResult, void>&}; T = wallet::SelectionResult; F = void]’ 5../../../src/wallet/test/fuzz/coinselection.cpp:152:95: required from here
6../../../src/util/result.h:243:43: error: no matching function for call to ‘wallet::SelectionResult::SelectionResult(<brace-enclosed initializer list>)’ 7243| void ConstructValue(Args&&... args) { new (&m_value) T{std::forward<Args>(args)...}; }
8|^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9../../../src/wallet/coinselection.h:352:14: note: candidate: ‘wallet::SelectionResult::SelectionResult(CAmount, wallet::SelectionAlgorithm)’10352| explicit SelectionResult(const CAmount target, SelectionAlgorithm algo)
11|^~~~~~~~~~~~~~~12../../../src/wallet/coinselection.h:352:14: note: candidate expects 2 arguments, 1 provided
13../../../src/wallet/coinselection.h:324:8: note: candidate: ‘wallet::SelectionResult::SelectionResult(const wallet::SelectionResult&)’14324| struct SelectionResult
15|^~~~~~~~~~~~~~~16../../../src/wallet/coinselection.h:324:8: note: no known conversion for argument 1 from ‘util::Result<wallet::SelectionResult>’ to ‘const wallet::SelectionResult&’17../../../src/wallet/coinselection.h:324:8: note: candidate: ‘wallet::SelectionResult::SelectionResult(wallet::SelectionResult&&)’18../../../src/wallet/coinselection.h:324:8: note: no known conversion for argument 1 from ‘util::Result<wallet::SelectionResult>’ to ‘wallet::SelectionResult&&’19In file included from /usr/include/c++/13.2.1/memory:69,
20 from ../../../src/serialize.h:17,
21 from ../../../src/policy/feerate.h:10,
22 from ../../../src/wallet/test/fuzz/coinselection.cpp:5:
23/usr/include/c++/13.2.1/bits/stl_uninitialized.h: In instantiation of ‘constexpr bool std::__check_constructible() [with _ValueType = util::Result<wallet::SelectionResult>; _Tp =const util::Result<wallet::SelectionResult>&]’:
24/usr/include/c++/13.2.1/bits/stl_uninitialized.h:182:4: required from ‘_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator =const util::Result<wallet::SelectionResult>*; _ForwardIterator = util::Result<wallet::SelectionResult>*]’25/usr/include/c++/13.2.1/bits/stl_uninitialized.h:373:37: required from ‘_ForwardIterator std::__uninitialized_copy_a(_InputIterator, _InputIterator, _ForwardIterator, allocator<_Tp>&) [with _InputIterator =const util::Result<wallet::SelectionResult>*; _ForwardIterator = util::Result<wallet::SelectionResult>*; _Tp = util::Result<wallet::SelectionResult>]’26/usr/include/c++/13.2.1/bits/stl_vector.h:1692:33: required from ‘void std::vector<_Tp, _Alloc>::_M_range_initialize(_ForwardIterator, _ForwardIterator, std::forward_iterator_tag) [with _ForwardIterator =const util::Result<wallet::SelectionResult>*; _Tp = util::Result<wallet::SelectionResult>; _Alloc = std::allocator<util::Result<wallet::SelectionResult>>]’27/usr/include/c++/13.2.1/bits/stl_vector.h:679:21: required from ‘std::vector<_Tp, _Alloc>::vector(std::initializer_list<_Tp>, const allocator_type&) [with _Tp = util::Result<wallet::SelectionResult>; _Alloc = std::allocator<util::Result<wallet::SelectionResult>>; allocator_type = std::allocator<util::Result<wallet::SelectionResult>>]’28../../../src/wallet/test/fuzz/coinselection.cpp:152:95: required from here
29/usr/include/c++/13.2.1/bits/stl_uninitialized.h:90:56: error: static assertion failed: result type must be constructible from input type
3090| static_assert(is_constructible<_ValueType, _Tp>::value,
31|^~~~~32/usr/include/c++/13.2.1/bits/stl_uninitialized.h:90:56: note: ‘std::integral_constant<bool, false>::value’ evaluates to false
33make[2]: *** [Makefile:16569: wallet/test/fuzz/test_fuzz_fuzz-coinselection.o] Error 1
DrahtBot removed review request from hernanmarino
on Aug 29, 2023
DrahtBot removed review request from w0xlt
on Aug 29, 2023
DrahtBot requested review from hernanmarino
on Aug 29, 2023
DrahtBot requested review from w0xlt
on Aug 29, 2023
DrahtBot removed review request from hernanmarino
on Aug 29, 2023
DrahtBot removed review request from w0xlt
on Aug 29, 2023
DrahtBot requested review from hernanmarino
on Aug 29, 2023
DrahtBot requested review from w0xlt
on Aug 29, 2023
DrahtBot added the label
CI failed
on Aug 29, 2023
in 0359510: clang-format new code to reduce this diff here?
I can do it if there’s another request, but I think simple accessor methods are more readable if declarations are on the left, implementations are on the right, to make it easier to find relevant information and notice differences between functions.
in
src/util/result.h:129
in
03595104ccoutdated
145+ //! message from one result object to another. The two result
146+ //! objects must have compatible success and failure types.
147+ template <bool Constructed, typename DstResult, typename SrcResult>
148+ static void MoveResult(DstResult& dst, SrcResult&& src)
149+ {
150+ if (Constructed) {
in 0359510: Doesn’t matter, but could add contexpr here, or remove it above, for consistency, if it compiles?
Good catch, done.
in
src/util/result.h:317
in
03595104ccoutdated
238+ Base::template MoveResult</*Constructed=*/false>(*this, std::move(other));
239+ }
240+
241+ //! Move success or failure values and any error message from another Result
242+ //! object to this object.
243+ Result& Set(Result&& other) LIFETIMEBOUND
in 0359510: Is there a unit test for this method? I am thinking about a test to check what happens when an error-result is Set a value-result, and vice-versa.
Good suggestion. Added a test to exercise the Set() method with success & failure values.
in 0359510: Why inline? IIUC every member is inline, so this seems confusing, no?
This is just a friend function that is able to access private fields of the Result class, not a member. So I think inline was required to avoid duplicate symbol errors from the linker. I would have preferred to just make the ErrorString function a friend function directly, but this did not seem to be possible because it has template parameters, so I declared _ErrorString to be a friend function which can called by ErrorString to specify the template parameters.
This complexity goes away later when GetWarnings() / GetErrors() accessors are added. My main goal for all this is just to make the Result class a container for error strings, and keep any code dealing with the strings separate.
in
src/util/result.h:39
in
03595104ccoutdated
35+//! void TryAddNumbers(int a, int b)
36+//! {
37+//! if (auto result = AddNumbers(a, b)) {
38+//! printf("%i + %i = %i\n", a, b, *result);
39+//! } else {
40+//! printf("Error: %s\n", util::ErrorString(result).translated.c_str());
nit in b21e2b0: 2022-present or no year for new code to avoid having to ever touch this again?
Just leaving alone for now. The “present” idea is interesting but doesn’t seem right. I’d expect “present” to mean “when I’m writing this,” or “when you’re reading this,” not “when this code was last changed,” which is what we want. Would be happy to drop the year or drop the whole line though.
nit in b21e2b0: Are the && on the right side needed? IIUC this should only affect Result& m_result;, which should be equal to Result && & m_result;, no?
nit in b21e2b0: Missing assert(m_info) to avoid UB, no?
This won’t be UB unless the assert is disabled because the assert is checking that m_info is non-null and contains a failure value (see operator bool above)
maflcko approved
maflcko
commented at 2:11 pm on August 30, 2023:
member
DrahtBot removed review request from hernanmarino
on Aug 30, 2023
DrahtBot removed review request from w0xlt
on Aug 30, 2023
DrahtBot requested review from w0xlt
on Aug 30, 2023
DrahtBot requested review from hernanmarino
on Aug 30, 2023
DrahtBot removed review request from w0xlt
on Aug 30, 2023
DrahtBot removed review request from hernanmarino
on Aug 30, 2023
DrahtBot requested review from hernanmarino
on Aug 30, 2023
DrahtBot requested review from w0xlt
on Aug 30, 2023
achow101 added the label
Needs rebase
on Sep 5, 2023
DrahtBot removed the label
Needs rebase
on Sep 5, 2023
ryanofsky force-pushed
on Sep 6, 2023
ryanofsky
commented at 6:02 pm on September 6, 2023:
contributor
Rebased 7f883b33bb89205a9d00c2d20d363a36a0167c7c -> 956bec1ecadcfef13e16f1364ae3a7043ff50e48 (pr/bresult2.40 -> pr/bresult2.41, compare) to fix silent conflict with #27585, and made many suggested improvements
TheCharlatan
commented at 7:19 pm on September 6, 2023:
contributor
DrahtBot removed review request from hernanmarino
on Sep 6, 2023
DrahtBot removed review request from w0xlt
on Sep 6, 2023
DrahtBot requested review from hernanmarino
on Sep 6, 2023
DrahtBot requested review from w0xlt
on Sep 6, 2023
DrahtBot requested review from maflcko
on Sep 6, 2023
DrahtBot requested review from achow101
on Sep 6, 2023
DrahtBot requested review from jonatack
on Sep 6, 2023
DrahtBot removed the label
CI failed
on Sep 7, 2023
DrahtBot added the label
CI failed
on Oct 5, 2023
maflcko
commented at 4:20 pm on October 14, 2023:
member
Needs rebase if still relevant
DrahtBot removed review request from hernanmarino
on Oct 14, 2023
DrahtBot removed review request from w0xlt
on Oct 14, 2023
DrahtBot requested review from hebasto
on Oct 14, 2023
DrahtBot requested review from w0xlt
on Oct 14, 2023
DrahtBot requested review from hernanmarino
on Oct 14, 2023
ryanofsky force-pushed
on Oct 18, 2023
ryanofsky
commented at 7:27 pm on October 18, 2023:
contributor
Rebased 956bec1ecadcfef13e16f1364ae3a7043ff50e48 -> 29f6cfdabecbdecafc52ccc86425a1c7bb7f5c40 (pr/bresult2.41 -> pr/bresult2.42, compare) due to silent conflict with #27596
DrahtBot removed the label
CI failed
on Oct 18, 2023
TheCharlatan approved
TheCharlatan
commented at 6:01 am on October 19, 2023:
contributor
Re-ACK29f6cfdabecbdecafc52ccc86425a1c7bb7f5c40
Changes are resolving a simple silent conflict and adapting for the new clang-tidy lint case.
DrahtBot removed review request from w0xlt
on Oct 19, 2023
DrahtBot removed review request from hernanmarino
on Oct 19, 2023
DrahtBot requested review from hernanmarino
on Oct 19, 2023
DrahtBot requested review from w0xlt
on Oct 19, 2023
maflcko
commented at 9:11 am on October 25, 2023:
member
Did you compile each commit locally and ran the tests? See CI:
0test/result_tests.cpp(112): error: in "result_tests/check_set": check util::ErrorString(result) == str has failed [bilingual_str('' , '') != bilingual_str('error' , 'error')]
DrahtBot removed review request from hernanmarino
on Oct 25, 2023
DrahtBot removed review request from w0xlt
on Oct 25, 2023
DrahtBot requested review from hernanmarino
on Oct 25, 2023
DrahtBot requested review from w0xlt
on Oct 25, 2023
ryanofsky force-pushed
on Oct 26, 2023
ryanofsky
commented at 0:54 am on October 26, 2023:
contributor
Did you compile each commit locally and ran the tests?
Thanks, I didn’t realize the test was broken in earlier commits. I backported it further and got it working in all commits with no changes to the final diff.
TheCharlatan
commented at 11:34 am on October 26, 2023:
contributor
Re-ACKf158686962e1a229d0382c739b78cd9644aa7ada
DrahtBot removed review request from hernanmarino
on Oct 26, 2023
DrahtBot removed review request from w0xlt
on Oct 26, 2023
DrahtBot requested review from w0xlt
on Oct 26, 2023
DrahtBot requested review from hernanmarino
on Oct 26, 2023
DrahtBot added the label
Needs rebase
on Dec 12, 2023
Fabcien referenced this in commit
e0df40da76
on Dec 15, 2023
ryanofsky force-pushed
on Jan 24, 2024
DrahtBot removed the label
Needs rebase
on Jan 24, 2024
TheCharlatan approved
TheCharlatan
commented at 7:35 pm on January 24, 2024:
contributor
Re-ACKf822ac9a24d684937f1258da89812e99c4b205ba
DrahtBot removed review request from w0xlt
on Jan 24, 2024
DrahtBot removed review request from hernanmarino
on Jan 24, 2024
DrahtBot requested review from w0xlt
on Jan 24, 2024
DrahtBot requested review from hernanmarino
on Jan 24, 2024
hernanmarino approved
hernanmarino
commented at 4:41 pm on February 5, 2024:
contributor
re ackf822ac9a24d684937f1258da89812e99c4b205ba
DrahtBot removed review request from w0xlt
on Feb 5, 2024
DrahtBot requested review from w0xlt
on Feb 5, 2024
DrahtBot requested review from hernanmarino
on Feb 5, 2024
TheCharlatan
commented at 9:17 am on February 13, 2024:
contributor
I think there is a silent merge conflict on the first commit.
DrahtBot removed review request from w0xlt
on Feb 13, 2024
DrahtBot removed review request from hernanmarino
on Feb 13, 2024
DrahtBot requested review from hernanmarino
on Feb 13, 2024
DrahtBot requested review from w0xlt
on Feb 13, 2024
maflcko
commented at 10:57 am on February 13, 2024:
member
At the risk of having asked this previously: Why not postpone the 1c7d8be commit to a later pull? This would also make review easier, because the code change comes with the changes to actually use it in real code, outside of just unit tests.
DrahtBot removed review request from hernanmarino
on Feb 13, 2024
DrahtBot removed review request from w0xlt
on Feb 13, 2024
DrahtBot requested review from w0xlt
on Feb 13, 2024
DrahtBot requested review from hernanmarino
on Feb 13, 2024
DrahtBot added the label
CI failed
on Feb 13, 2024
DrahtBot
commented at 6:23 pm on February 13, 2024:
contributor
🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.
Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.
Leave a comment here, if you need help tracking down a confusing failure.
ryanofsky
commented at 6:06 pm on February 21, 2024:
contributor
At the risk of having asked this previously: Why not postpone the 1c7d8be commit to a later pull? This would also make review easier, because the code change comes with the changes to actually use it in real code, outside of just unit tests.
Sure, I’m happy to split up this PR.
I don’t think this idea came up before (or I can’t remember if it did). From my perspective commit 1c7d8bea4f0b25f9adb89e402a130fa114220494 is basically the point of this PR, and the other commits are less important. Usually when we have errors we want to return descriptive error strings and fail, not return error codes and branch. So I think the part of this PR that returns better error and warning messages is more interesting than the part that returns error codes.
As far as using the changes in real code, not just tests, I opened #25722 at the same time as this PR to start using it throughout wallet code, so there are actually a lot of usages to look at outside of tests. The usage patterns in the test are also meant to be realistic, with test functions returning errors and warnings, accumulating them, and returning them as part of their own Results .
I don’t think it hurts anything to split this PR up, so I can try that, but I’m also curious if other reviewers want me to split this or leave it alone. According to draftbot this has 1 current ack and 6 stale acks, and it seems like this needs rebase due to a silent conflict. So maybe the problem is more that it hasn’t gotten enough simultaneous acks at the same time, and that I’ve been lazy about rebasing, than that it should be split up.
DrahtBot removed review request from w0xlt
on Feb 21, 2024
DrahtBot removed review request from hernanmarino
on Feb 21, 2024
DrahtBot requested review from w0xlt
on Feb 21, 2024
DrahtBot requested review from hernanmarino
on Feb 21, 2024
DrahtBot removed review request from w0xlt
on Feb 21, 2024
DrahtBot removed review request from hernanmarino
on Feb 21, 2024
DrahtBot requested review from hernanmarino
on Feb 21, 2024
DrahtBot requested review from w0xlt
on Feb 21, 2024
DrahtBot removed review request from hernanmarino
on Feb 21, 2024
DrahtBot removed review request from w0xlt
on Feb 21, 2024
DrahtBot requested review from w0xlt
on Feb 21, 2024
DrahtBot requested review from hernanmarino
on Feb 21, 2024
ryanofsky force-pushed
on Feb 21, 2024
ryanofsky
commented at 7:18 pm on February 21, 2024:
contributor
Rebased f822ac9a24d684937f1258da89812e99c4b205ba -> 4ec6b060a80045049adc53b4db0b0837ba169cfc (pr/bresult2.44 -> pr/bresult2.45, compare) due to silent conflict with #27877
DrahtBot removed the label
CI failed
on Feb 21, 2024
ryanofsky force-pushed
on Feb 21, 2024
ryanofsky
commented at 10:56 pm on February 21, 2024:
contributor
Updated 4ec6b060a80045049adc53b4db0b0837ba169cfc -> 20556598140030237861d21a61f646252002ddff (pr/bresult2.45 -> pr/bresult2.46, compare) making a few changes to improve compatibility with #26022
in
src/node/chainstate.cpp:241
in
0d33bcbf01outdated
TheCharlatan
commented at 9:30 am on February 22, 2024:
If I’m reading this right, not specifying a failure value will lead to it being default constructed, so this will have a value of ChainstateLoadError::FAILURE. Could omitting a failure value be a compile-time error if the failure type is non-void?
ryanofsky
commented at 11:14 am on February 22, 2024:
Could omitting a failure value be a compile-time error if the failure type is non-void?
Nice catch. That’s an interesting idea but it seems extreme because it would make it difficult to call a failure value’s default constructor, and impossible to call it the class is not copyable or movable.
I think in general if you don’t want a failure value to be default-constructed you should pass a failure type that doesn’t have a default constructor. But we could add a check for specifically for enums. The following seems to work:
But we could add a check for specifically for enums.
Latest push adds a slightly more general check that doesn’t allow any scalar failure value (int, float, enum, or pointer) to be default constructed. Could adjust this condition or make it an option if default-constructing these types of values seems useful in the future.
This check is arguably too heavy handed because it is enforcing that the result.GetFailure() value is not default-constructed even though it technically has a default constructor. Even without the check, the Result object would be still constructed in a failure state and if (result) and if (result.has_value()) would be false. But it seems safest to start off requiring explicit scalar failure values in case they were omitted by accident.
TheCharlatan
commented at 1:05 pm on February 22, 2024:
TheCharlatan
commented at 1:20 pm on February 22, 2024:
contributor
Re-ACKcdf7bb17563b92e48b0576a0975c168619c5aa34
DrahtBot removed review request from w0xlt
on Feb 22, 2024
DrahtBot removed review request from hernanmarino
on Feb 22, 2024
DrahtBot requested review from w0xlt
on Feb 22, 2024
DrahtBot requested review from hernanmarino
on Feb 22, 2024
DrahtBot added the label
CI failed
on Feb 23, 2024
DrahtBot removed the label
CI failed
on Feb 28, 2024
ryanofsky
commented at 10:02 pm on March 22, 2024:
contributor
Converting to draft. Working on #29700 and #29642 made me want to implement more improvements to the Result class (308e38e94fcac5aedf8ed1247a096c0d271fa666 if curious)
ryanofsky marked this as a draft
on Mar 22, 2024
ryanofsky force-pushed
on Mar 26, 2024
ryanofsky
commented at 7:23 pm on March 26, 2024:
contributor
The Result::Set() method is renamed Result::Update() and now has ability to merge success and failure values from different results together instead of just replacing them. This is used in #29700 to bubble up AbortFailure values indicating whether or not AbortNode calls happened as part of failures.
The result class now takes a generic MessagesType parameter and isn’t hardcoded to use vector<blingual_str>. This gets rid of some complexity in the implementation and lets it handle SuccessType FailureType and MessagesType fields in a more consistent way.
ryanofsky force-pushed
on Mar 26, 2024
DrahtBot added the label
CI failed
on Mar 26, 2024
DrahtBot
commented at 8:30 pm on March 26, 2024:
contributor
🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the
documentation.
Possibly this is due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.
Leave a comment here, if you need help tracking down a confusing failure.
DrahtBot removed the label
CI failed
on Mar 27, 2024
ryanofsky marked this as ready for review
on Mar 27, 2024
ryanofsky force-pushed
on Mar 28, 2024
in
src/util/result.h:342
in
9fe6a68170outdated
343 if constexpr (DstConstructed) {
344- if (dst) {
345+ if (dst && src) {
346+ // dst and src both hold success values, so merge them and return
347+ if constexpr (!std::is_same_v<typename SrcResult::SuccessType, void>) {
348+ ResultTraits<typename SrcResult::SuccessType>::MergeInto(*dst, *src);
TheCharlatan
commented at 9:17 pm on April 4, 2024:
Reviewers might be wondering what the benefits of the ResultTraits are. They are useful for potential future extensions of the move behaviour: 3951afc .
Thanks I added a comment to the traits struct to make this clearer. Traits are an old pattern in c++ and are commonly used. The standard library has std::allocator_traits, std::iterator_traits, std::pointer_traits, std::hash, std::formatter, std::tuple_element, and std::tuple_size trait structs which allow generic library functions to work with user defined types. They all work the same way, by defining a template struct in the library that user code can specialize to control the behavior of library functions when called with custom types. This is an old explanation of traits which may be helpful: http://erdani.org/publications/traits.html
TheCharlatan
commented at 9:30 pm on April 4, 2024:
I have not quite grasped yet what the immediate benefit of these more generic decorators on the Messages are. Am I missing something from your draft PRs? I like shiny generics though, so to me a clear benefit of this approach would be to future-proof the result type. I was asking myself if this could be a bit more readable if instead of a forward-declared struct, the MessagesTraits were made a concept. After implementing it, I think it is indeed a bit easier to parse at the call site. It also potentially allows the user to define their own types for the warnings and errors fields like this: https://github.com/TheCharlatan/bitcoin/commit/f5e5442cf80c58c0509562d8c689243c8d9becf2 . Could this also be leveraged in place of your future addition of an info type?
I have not quite grasped yet what the immediate benefit of these more generic decorators on the Messages are.
The benefit is letting the Result struct work with any possible MessagesType without placing requirements on it. The PR provides a simple default type to hold error and warning messages:
The type was defined this way because we currently have wallet code which returns lists of errors and warnings and wallet RPCs which return them in separate arrays. But I don’t want to hardcode the Result type to use this particular struct, because it shouldn’t care about MessagesType internals and because other representations of errors and warnings are reasonable too. Maybe a representation that preserved the relative order of errors and warnings instead of keeping them in separate arrays would be good. Maybe a representation that discarded translated strings, or only held errors without allowing warnings, or used a different data structure would be good. The MessagesTraits class lets the Result class not care about these things and work with any messages type.
I was asking myself if this could be a bit more readable if instead of a forward-declared struct, the MessagesTraits were made a concept. After implementing it, I think it is indeed a bit easier to parse at the call site. It also potentially allows the user to define their own types for the warnings and errors fields like this: TheCharlatan@f5e5442 . Could this also be leveraged in place of your future addition of an info type?
That is interesting to see but I’m not sure it is more readable if you are familiar with the traits pattern. Another drawback is it requires every MessagesType to be a struct that has AddError, AddWarning, HasMessages, ErrorType, and WarningType members, which is not the case right now. Right now MessagesType could be any type including the simple Messages struct above, a plain string, even something like a bool or a custom type without public members. The suggestion is also +57 -36 lines excluding tests, so it doesn’t really seem like a simplification, and the more complicated Messages struct definition also does not seem good to expose.
ryanofsky force-pushed
on Apr 18, 2024
ryanofsky
commented at 4:24 pm on April 18, 2024:
contributor
Updated 7a4741eaf892646e9d02e440c39fbbfa03f29fc3 -> 28e20812109757e307bc29a4adbef8aae90d94a6 (pr/bresult2.52 -> pr/bresult2.53, compare) just improving some comments
ryanofsky marked this as a draft
on Apr 18, 2024
ryanofsky
commented at 7:13 pm on April 18, 2024:
contributor
Converted to draft since first 2 commits were moved to a separate PR, #29906
Rebased 28e20812109757e307bc29a4adbef8aae90d94a6 -> 990f9d65c5e15ab26c341c21829a697f5cddfa6c (pr/bresult2.53 -> pr/bresult2.54, compare) on top of updated base PR #29906, also adding more comments, simplifying code a little bit by util::MoveFrom helper, and using Update() method more places for consistency.
ryanofsky force-pushed
on Apr 24, 2024
DrahtBot added the label
CI failed
on Apr 25, 2024
ryanofsky force-pushed
on Apr 26, 2024
DrahtBot removed the label
CI failed
on Apr 26, 2024
ryanofsky force-pushed
on Apr 26, 2024
stickies-v
commented at 1:22 pm on April 26, 2024:
contributor
Posting my response to part of [your comment on #29906](/bitcoin-bitcoin/29906/#issuecomment-2078452007) here, since Update() is no longer relevant to that PR:
I don’t think this is good usage of Update(), since there is no chaining happening here. I would find list initialization more readable and less error-prone:
0diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp
1index 0671f75515..8cc43c53e5 100644
2--- a/src/node/chainstate.cpp
3+++ b/src/node/chainstate.cpp
4@@ -40,7 +40,6 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
5 const CacheSizes& cache_sizes,
6 const ChainstateLoadOptions& options) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
7 {
8- util::Result<InterruptResult, ChainstateLoadError> result;
9 auto& pblocktree{chainman.m_blockman.m_block_tree_db};
10 // new BlockTreeDB tries to delete the existing file, which
11 // fails if it's still open from the previous loop. Close it first:
12@@ -61,8 +60,7 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
13 }
14 15 if (chainman.m_interrupt) {
16- result.Update(Interrupted{});
17- return result;
18+ return Interrupted{};
19 }
20 21 // LoadBlockIndex will load m_have_pruned if we've ever removed a
22@@ -71,26 +69,23 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
23 // From here on, fReindex and options.reindex values may be different!
24 if (!chainman.LoadBlockIndex()) {
25 if (chainman.m_interrupt) {
26- result.Update(Interrupted{});
27+ return Interrupted{};
28 } else {
29- result.Update({util::Error{_("Error loading block database")}, ChainstateLoadError::FAILURE});
30+ return {util::Error{_("Error loading block database")}, ChainstateLoadError::FAILURE};
31 }
32- return result;
33 }
34 35 if (!chainman.BlockIndex().empty() &&
36 !chainman.m_blockman.LookupBlockIndex(chainman.GetConsensus().hashGenesisBlock)) {
37 // If the loaded chain has a wrong genesis, bail out immediately
38 // (we're likely using a testnet datadir, or the other way around).
39- result.Update({util::Error{_("Incorrect or no genesis block found. Wrong datadir for network?")}, ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
40- return result;
41+ return {util::Error{_("Incorrect or no genesis block found. Wrong datadir for network?")}, ChainstateLoadError::FAILURE_INCOMPATIBLE_DB};
42 }
43 44 // Check for changed -prune state. What we are concerned about is a user who has pruned blocks
45 // in the past, but is now trying to run unpruned.
46 if (chainman.m_blockman.m_have_pruned && !options.prune) {
47- result.Update({util::Error{_("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")}, ChainstateLoadError::FAILURE});
48- return result;
49+ return {util::Error{_("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")}, ChainstateLoadError::FAILURE};
50 }
51 52 // At this point blocktree args are consistent with what's on disk.
53@@ -98,8 +93,7 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
54 // (otherwise we use the one already on disk).
55 // This is called again in ImportBlocks after the reindex completes.
56 if (!fReindex && !chainman.ActiveChainstate().LoadGenesisBlock()) {
57- result.Update({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
58- return result;
59+ return {util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE};
60 }
61 62 auto is_coinsview_empty = [&](Chainstate* chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
63@@ -133,17 +127,15 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
64 // Refuse to load unsupported database format.
65 // This is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
66 if (chainstate->CoinsDB().NeedsUpgrade()) {
67- result.Update({util::Error{_("Unsupported chainstate database format found. "
68+ return {util::Error{_("Unsupported chainstate database format found. "
69 "Please restart with -reindex-chainstate. This will "
70 "rebuild the chainstate database.")},
71- ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
72- return result;
73+ ChainstateLoadError::FAILURE_INCOMPATIBLE_DB};
74 }
75 76 // ReplayBlocks is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
77 if (!chainstate->ReplayBlocks()) {
78- result.Update({util::Error{_("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")}, ChainstateLoadError::FAILURE});
79- return result;
80+ return {util::Error{_("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")}, ChainstateLoadError::FAILURE};
81 }
82 83 // The on-disk coinsdb is now in a good state, create the cache
84@@ -153,8 +145,7 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
85 if (!is_coinsview_empty(chainstate)) {
86 // LoadChainTip initializes the chain based on CoinsTip()'s best block
87 if (!chainstate->LoadChainTip()) {
88- result.Update({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
89- return result;
90+ return {util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE};
91 }
92 assert(chainstate->m_chain.Tip() != nullptr);
93 }
94@@ -164,9 +155,8 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
95 auto chainstates{chainman.GetAll()};
96 if (std::any_of(chainstates.begin(), chainstates.end(),
97 [](const Chainstate* cs) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { return cs->NeedsRedownload(); })) {
98- result.Update({util::Error{strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
99- chainman.GetConsensus().SegwitHeight)}, ChainstateLoadError::FAILURE});
100- return result;
101+ return {util::Error{strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
102+ chainman.GetConsensus().SegwitHeight)}, ChainstateLoadError::FAILURE};
103 };
104 }
105106@@ -175,7 +165,7 @@ static util::Result<InterruptResult, ChainstateLoadError> CompleteChainstateInit
107 // on the condition of each chainstate.
108 chainman.MaybeRebalanceCaches();
109110- return result;
111+ return {};
112 }
113114 util::Result<InterruptResult, ChainstateLoadError> LoadChainstate(ChainstateManager& chainman, const CacheSizes& cache_sizes,
ryanofsky
commented at 3:27 pm on April 26, 2024:
contributor
I don’t think this is good usage of Update(), since there is no chaining happening here. I would find list initialization more readable and less error-prone:
git diff on 1376583
The diff you suggested is actually the way this function was implemented before #29700. The problem is that #29700 updates some validation functions to return util::Result instead of bool. So instead of CompleteChainstateInitialization being a function that just returns a util::Result value, it becomes a function that returns a util::Result value and also calls other functions returning util::Result. Which is the scenario where the result.Update() pattern is useful, because it allows returning warning and error messages from functions being called instead of discarding them, so more complete error information is returned by default when a problem happens.
If you think this pattern is not readable enough or too error prone, we could probably come up with an alternate pattern that doesn’t require updating a variable (but may require copying around data more manually). In general, I’d agree that all other things being equal, it’s better to initialize variables once than change them over time. But in this case, if the goal is to accumulate error and warning messages, I think a pattern where you declare a single variable at the top of the function holding the messages, and update the variable over the course of the function, and then return the variable at the end is a pretty simple and readable pattern. I’m sure other patterns could work too, though.
I guess I’d want to know if there’s another pattern you’d prefer, or what more specific problems you see with the current code.
DrahtBot added the label
CI failed
on Apr 26, 2024
stickies-v
commented at 2:57 pm on April 27, 2024:
contributor
The problem is that #29700 updates some validation functions to return util::Result instead of bool.
I see, that was not apparent in the link you shared earlier. Would it make sense to do that refactoring in #29700 then, instead of in this PR? Because in this PR, I don’t think the change makes a lot of sense on its own.
I think a pattern where you declare a single variable at the top of the function holding the messages, and update the variable over the course of the function, and then return the variable at the end is a pretty simple and readable pattern.
I agree, I think that’s a reasonable pattern. For this situation, I would prefer using two functions: Update() when chaining is happening, and Set(), Replace(), operator=(), … when we expect Result to be empty. This way, we can more easily identify potential bugs when e.g. Set() is used but the Result has already had a value assigned to it earlier. Potentially, could even do a run-time assertion (I don’t think compile time is possible?).
Combining both would be suitable in this case, I think. Pseudo-code diff (since Set() isn’t implemented) where we only keep Update() for the chaining bits:
0diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp
1index 014f6915ad..bc8d547153 100644
2--- a/src/node/chainstate.cpp
3+++ b/src/node/chainstate.cpp
4@@ -64,7 +64,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
5 }
6 7 if (chainman.m_interrupt) {
8- result.Update(Interrupted{});
9+ result.Set(Interrupted{});
10 return result;
11 }
1213@@ -85,14 +85,14 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
14 !chainman.m_blockman.LookupBlockIndex(chainman.GetConsensus().hashGenesisBlock)) {
15 // If the loaded chain has a wrong genesis, bail out immediately
16 // (we're likely using a testnet datadir, or the other way around).
17- result.Update({util::Error{_("Incorrect or no genesis block found. Wrong datadir for network?")}, ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
18+ result.Set({util::Error{_("Incorrect or no genesis block found. Wrong datadir for network?")}, ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
19 return result;
20 }
2122 // Check for changed -prune state. What we are concerned about is a user who has pruned blocks
23 // in the past, but is now trying to run unpruned.
24 if (chainman.m_blockman.m_have_pruned && !options.prune) {
25- result.Update({util::Error{_("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")}, ChainstateLoadError::FAILURE});
26+ result.Set({util::Error{_("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain")}, ChainstateLoadError::FAILURE});
27 return result;
28 }
2930@@ -101,7 +101,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
31 // (otherwise we use the one already on disk).
32 // This is called again in ImportBlocks after the reindex completes.
33 if (!fReindex && !chainman.ActiveChainstate().LoadGenesisBlock()) {
34- result.Update({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
35+ result.Set({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
36 return result;
37 }
3839@@ -136,7 +136,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
40 // Refuse to load unsupported database format.
41 // This is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
42 if (chainstate->CoinsDB().NeedsUpgrade()) {
43- result.Update({util::Error{_("Unsupported chainstate database format found. "
44+ result.Set({util::Error{_("Unsupported chainstate database format found. "
45 "Please restart with -reindex-chainstate. This will "
46 "rebuild the chainstate database.")},
47 ChainstateLoadError::FAILURE_INCOMPATIBLE_DB});
48@@ -145,7 +145,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
4950 // ReplayBlocks is a no-op if we cleared the coinsviewdb with -reindex or -reindex-chainstate
51 if (!chainstate->ReplayBlocks()) {
52- result.Update({util::Error{_("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")}, ChainstateLoadError::FAILURE});
53+ result.Set({util::Error{_("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate.")}, ChainstateLoadError::FAILURE});
54 return result;
55 }
5657@@ -156,7 +156,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
58 if (!is_coinsview_empty(chainstate)) {
59 // LoadChainTip initializes the chain based on CoinsTip()'s best block
60 if (!chainstate->LoadChainTip()) {
61- result.Update({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
62+ result.Set({util::Error{_("Error initializing block database")}, ChainstateLoadError::FAILURE});
63 return result;
64 }
65 assert(chainstate->m_chain.Tip() != nullptr);
66@@ -167,7 +167,7 @@ static FlushResult<InterruptResult, ChainstateLoadError> CompleteChainstateIniti
67 auto chainstates{chainman.GetAll()};
68 if (std::any_of(chainstates.begin(), chainstates.end(),
69 [](const Chainstate* cs) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { return cs->NeedsRedownload(); })) {
70- result.Update({util::Error{strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
71+ result.Set({util::Error{strprintf(_("Witness data for blocks after height %d requires validation. Please restart with -reindex."),
72 chainman.GetConsensus().SegwitHeight)}, ChainstateLoadError::FAILURE});
73 return result;
74 };
It’s kind of a similar situation to having a std::vector<>&out-parameter that we use here and there in the codebase. When that happens, first you need to check if the vector is supposed to be empty or not. Then, you need to check if it actually is empty. Sometimes we call .clear() in the beginning of the function, sometimes we don’t. It’s just confusing and time consuming and error prone (see e.g. #26289). With differentiated Update() and Set() functions we allow the developer to signal intent, which makes it easier to understand the code and to identify potential bugs (manually or through run-time/fuzzing checks).
I’ve ranted about this already a bit too long, so please do let me know if you (dis)agree but I’ll stop commenting further until I’ve (re)-reviewed the rest of this PR and I maybe get new insights.
achow101 referenced this in commit
2d3056751b
on Apr 30, 2024
DrahtBot added the label
Needs rebase
on Apr 30, 2024
ryanofsky force-pushed
on May 1, 2024
ryanofsky marked this as ready for review
on May 1, 2024
ryanofsky
commented at 5:46 pm on May 1, 2024:
contributor
Thanks @stickies-v. I think since Update() is not required immediately I might move the first and third commits of this PR to another PR without Update(). The only downside will be some churn in the CompleteChainstateInitialization function, since I believe at some point it will need to be changed again to use the Update() pattern or a similar pattern, when other functions it calls are changed to return util::Result.
On your latest feedback, I think I understand the scenario you are concerned about. It seems like you are worried about future uses of util::Result where sometimes completely resetting a result object is useful, and other times updating pieces of the result object is useful. And you want developers to clearly indicate their intent in both cases by calling Set() in the former case and Update() in the latter case. If developers were required to express their intent this way, it could avoid bugs when existing fields of a result object are unintentionally cleared by a Set() call, or unintentionaly left with stale values after an Update() call.
I think I might have agreed with that before writing #25722 and #29700. But after writing these PRs, I don’t think a Set method would be useful in practice. The most straightforward way to write functions that return util::Result and also pass along error messages from other functions returning util::Result is to use result.Update(). The most straightforward way to write functions that just return util::Result without forwarding messages from other functions is just to construct separate result objects and not mutate existing objects. I didn’t encounter scenarios where code would be clearer or safer if it reset an existing object instead just declaring a new object, or just updating the relevant fields in an existing object.
Basically, I think the result.Update() pattern is a safe, simple pattern to follow that was general enough to handle the cases I encountered in #25722 and #29700 without the need the need for set, reset, or assignment. If that statement is not convincing, I understand, and think we could move forward with a smaller PR that does not include Update(). You might also be interested to look at a random commit from #25722 and #29700 and see if there’s another way you think some of that code should be written. A lot of the code in #25722 especially was not using result.Update() pattern initially and was just using separate result objects for everything.
DrahtBot removed the label
Needs rebase
on May 1, 2024
DrahtBot removed the label
CI failed
on May 1, 2024
DrahtBot added the label
Needs rebase
on May 20, 2024
ryanofsky force-pushed
on Jun 17, 2024
DrahtBot removed the label
Needs rebase
on Jun 17, 2024
DrahtBot added the label
Needs rebase
on Jul 2, 2024
in
src/node/chainstate.cpp:66
in
79a970d4f1outdated
79a970d4f12458a175317c453e251db7846c3561: I am not really sure, what the point of Update here is. A simple return Interrupted{}; compiles as well. I fail to see why it would be wrong, because the default constructed monostate should not be returned either way.
(Same for all code below)
My recommendation would be to keep the code as simple as possible and not add complicated features or complication that isn’t required.
I wrote a simple patch to make the existing Result more flexible (allow inner error types other than bilingual_str), for places that want to use something like std::expected<A, B> in code today by using Result<A, B>.
I understand that the ErrorString helper won’t be working as-is anymore. But that seems fine, because a standalone function can be provided to turn B into a string.
I also understand that the approach does not allow for multiple failures, errors, and warnings.
Not sure how to proceed here. Should I submit my patch as alternative pull request (it can be reverted here, if merged sooner)? Or should I backport std::expected into a separate header file? Or should I just wait until this pull is merged or C++23 happens, whichever happens earlier?
For reference, my patch is:
0commit fac1bdf332ccb241dbebf7ee421bb885381242ab 1Author: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>
2Date: Fri Jul 5 09:54:12 2024 +0200
3 4 refactor: Add util::Result failure values
5 6 This allows to use an inner error type different than bilingual_str. For
7 example, an enum class.
8 9 The ErrorString helper will not compile in this case, and a GetFailure()
10 method is added to retrieve the error value.
1112diff --git a/src/util/result.h b/src/util/result.h
13index 122a7638fa..038ed6336e 100644
14--- a/src/util/result.h
15+++ b/src/util/result.h
16@@ -12,8 +12,9 @@
1718 namespace util {
1920+template <class Inner = bilingual_str>
21 struct Error {
22- bilingual_str message;
23+ Inner inner;
24 };
2526 //! The util::Result class provides a standard way for functions to return
27@@ -31,13 +32,14 @@ struct Error {
28 //! `std::optional<T>` can be updated to return `util::Result<T>` and return
29 //! error strings usually just replacing `return std::nullopt;` with `return
30 //! util::Error{error_string};`.
31-template <class M>
32+template <class M, class F = bilingual_str>
33 class Result
34 {
35 private:
36 using T = std::conditional_t<std::is_same_v<M, void>, std::monostate, M>;
37+ using Error = Error<F>;
3839- std::variant<bilingual_str, T> m_variant;
40+ std::variant<Error, T> m_variant;
4142 //! Disallow copy constructor, require Result to be moved for efficiency.
43 Result(const Result&) = delete;
44@@ -55,7 +57,7 @@ private:
45 public:
46 Result() : m_variant{std::in_place_index_t<1>{}, std::monostate{}} {} // constructor for void
47 Result(T obj) : m_variant{std::in_place_index_t<1>{}, std::move(obj)} {}
48- Result(Error error) : m_variant{std::in_place_index_t<0>{}, std::move(error.message)} {}
49+ Result(Error error) : m_variant{std::in_place_index_t<0>{}, std::move(error)} {}
50 Result(Result&&) = default;
51 ~Result() = default;
5253@@ -83,8 +85,10 @@ public:
54 return has_value() ? std::move(value()) : std::forward<U>(default_value);
55 }
56 explicit operator bool() const noexcept { return has_value(); }
57+ const auto& GetFailure() const LIFETIMEBOUND { assert(!has_value()); return std::get<0>(m_variant).inner; }
58 const T* operator->() const LIFETIMEBOUND { return &value(); }
59 const T& operator*() const LIFETIMEBOUND { return value(); }
60+ auto& GetFailure() LIFETIMEBOUND { assert(!has_value()); return std::get<0>(m_variant).inner; }
61 T* operator->() LIFETIMEBOUND { return &value(); }
62 T& operator*() LIFETIMEBOUND { return value(); }
63 };
64@@ -92,7 +96,7 @@ public:
65 template <typename T>
66 bilingual_str ErrorString(const Result<T>& result)
67 {
68- return result ? bilingual_str{} : std::get<0>(result.m_variant);
69+ return result ? bilingual_str{} : result.GetFailure();
70 }
71 } // namespace util
72
I wrote a simple patch to make the existing Result more flexible (allow inner error types other than bilingual_str), for places that want to use something like std::expected<A, B> in code today by using Result<A, B>.
I think it could be a good idea to introduce a util::Expected<A, B> class if you see places in our code that need to return structured failure information but do not need to return error messages along with the failure data. If we have a util::Expected class it should make it easier to transition code from util::Expected to std::expected (maybe even with a scripted-diff) than if that code were written util::Result.
I do see benefits of util::Result and std::expected as being pretty different. I think util::Result class provides a good way to return complete error information for GUI, RPC, and libbitcoinkernel users when code is doing a big operation like loading a wallet, connecting a block, or creating a transaction, that can fail in complicated ways. It provides a way for wallet code in #25722 and kernel code in #29700 to return error information in a standard format in a single return value without ad-hoc bilingual_string& errorstd::vector<bilingual_str>& warningsbool& fatal_errorbool& is_interrupted in/out arguments. I think Result class works best when you have a chain of Result functions calling other Result functions, because the class provides ways to safely merge information for types that can be merged, while triggering compile errors if there are attempts to combine incompatible results.
By contrast, std::expected<A, B> is basically syntax sugar over std::variant<A, B>, which is great, but I think doesn’t help as much with problems in our codebase of returning error messages with enough context to users and making sure fatal errors and SignalInterrupt statuses are bubbled up to libbitcoinkernel callers.
I’d very much encourage adding a util::Expected class which is a simple wrapper over std::variant like your patch, if you see potential uses for this. But if you think adding a separate util::Expected class would be overkill, and instead would prefer to build this functionality into util::Result that would also be fine with me. I just think it might make it harder to disentangle the different use-cases for util::Result and std::expected later when we update to c++23, if we are using one class instead of two.
79a970d: I am not really sure, what the point of Update here is. A simple return Interrupted{}; compiles as well. I fail to see why it would be wrong, because the default constructed monostate should not be returned either way.
(Same for all code below)
My recommendation would be to keep the code as simple as possible and not add complicated features or complication that isn’t required.
I don’t see a big difference in complexity here, but I’ll make the suggested change since two people asked for it. Stickies suggested the same change in #25665 (comment) and posted a diff.
For background, a good reason to use the result.Update() pattern is when you have a function performing multiple operations, and you want to return warnings and errors from all operations, not just the last one.
When I wrote more code using the Result class in #25722 and #29700, I found that whenever you have functions returning Result and calling other functions returning Result, a good way to write them was to declare a single Result variable at the top, and accumulate error and warning messages in it, and return it after updating it with a final success or failure value. Other patterns could work too, but this pattern was easy to follow, and worked in all cases including more complicated ones with loops, and nested if statements, and #ifdef code. In #25722 and #29700, for simpler Result functions that don’t call other Result functions, I did use simpler pattern of just returning success values or util::Error directly, as you suggest.
In CompleteChainstateInitialization right now, either pattern works fine, because it does not currently call any other functions returning Result. I just thought result.Update pattern was appropriate because it is a pretty long function, and later in #29700LoadBlockIndex and MaybeRebalanceCaches will be updated to return Result values, so using result.Update now could avoid code churn later.
ryanofsky force-pushed
on Jul 9, 2024
ryanofsky
commented at 10:39 pm on July 9, 2024:
contributor
Rebased db91dbb5a9a0413d6ee22ed6e32d1221d5b6d996 -> b08548336eda489ad5be9e25542d2b73e7606204 (pr/bresult2.57 -> pr/bresult2.58, compare) due to conflicts with #30255 and #30132
Rebased b08548336eda489ad5be9e25542d2b73e7606204 -> 4eb36d1c41704f947f22d3b52f7799be23e4261c (pr/bresult2.58 -> pr/bresult2.59, compare) to fix conflict with #30344. Also took suggestions to simplify usage in CompleteChainstateInitialization.
Updated 4eb36d1c41704f947f22d3b52f7799be23e4261c -> 15b673d122532d44fea2e6d026172ac90929da14 (pr/bresult2.59 -> pr/bresult2.60, compare) adding wallet clang-tidy fix
Rebased 15b673d122532d44fea2e6d026172ac90929da14 -> 7e2b35711e11aa404eb0bd86776beb52916d354d (pr/bresult2.60 -> pr/bresult2.61, compare) due to various conflicts
DrahtBot removed the label
Needs rebase
on Jul 10, 2024
ryanofsky referenced this in commit
89bc54da3b
on Jul 10, 2024
ryanofsky referenced this in commit
cc0a369361
on Jul 18, 2024
ryanofsky referenced this in commit
3afda47f93
on Jul 18, 2024
ryanofsky force-pushed
on Jul 19, 2024
ryanofsky referenced this in commit
3523c12195
on Jul 19, 2024
ryanofsky referenced this in commit
39194c6534
on Jul 19, 2024
ryanofsky referenced this in commit
dfa16d7d92
on Aug 7, 2024
ryanofsky referenced this in commit
57b1f09b30
on Aug 7, 2024
hebasto added the label
Needs CMake port
on Aug 16, 2024
DrahtBot added the label
CI failed
on Aug 29, 2024
DrahtBot
commented at 5:47 am on August 29, 2024:
contributor
Make sure to run all tests locally, according to the documentation.
The failure may happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being
incompatible with the current code in the target branch). If so, make sure to rebase on the latest
commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the
affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
maflcko removed the label
Needs CMake port
on Aug 29, 2024
DrahtBot added the label
Needs rebase
on Sep 2, 2024
refactor: Add util::Result failure values
Add util::Result support for returning more error information. For better error
handling, adds the ability to return a value on failure, not just a value on
success. This is a key missing feature that made the result class not useful
for functions like LoadChainstate() which produce different errors that need to
be handled differently.
This change also makes some more minor improvements:
- Smaller type size. util::Result<int> is 16 bytes, and util::Result<void> is 8
bytes. Previously util::Result<int> and util::Result<void> were 72 bytes.
- More documentation and tests.
Previous commit causes the warning to be triggered, but the suboptimal return
from const statement was added earlier in 517e204bacd9d.
Warning was causing CI failure https://cirrus-ci.com/task/6159914970644480
90e5ee45bb
refactor: Add util::Result::Update() method
Add util::Result Update method to make it possible to change the value of an
existing Result object. This will be useful for functions that can return
multiple error and warning messages, and want to be able to change the result
value while keeping existing messages.
c03778deb7
refactor: Use util::Result class in LoadChainstate and VerifyLoadedChainstatea1aa9c8449
refactor: Add util::Result multiple error and warning messages
Add util::Result support for returning warning messages and multiple errors,
not just a single error string. This provides a way for functions to report
errors and warnings in a standard way, and simplifies interfaces.
The functionality is unit tested here, and put to use in followup PR
https://github.com/bitcoin/bitcoin/pull/25722
b23df5e070
test: add static test for util::Result memory usage
Suggested by Martin Leitner-Ankerl <martin.ankerl@gmail.com>
https://github.com/bitcoin/bitcoin/pull/25722#discussion_r1174298529
Co-authored-by: Martin Leitner-Ankerl <martin.ankerl@gmail.com>
7e2b35711e
ryanofsky force-pushed
on Nov 4, 2024
DrahtBot removed the label
Needs rebase
on Nov 4, 2024
ryanofsky force-pushed
on Nov 4, 2024
ryanofsky referenced this in commit
0e038bff5b
on Nov 4, 2024
ryanofsky referenced this in commit
5ce613ee2c
on Nov 4, 2024
DrahtBot removed the label
CI failed
on Nov 4, 2024
ryanofsky referenced this in commit
6697161ef5
on Nov 4, 2024
This is a metadata mirror of the GitHub repository
bitcoin/bitcoin.
This site is not affiliated with GitHub.
Content is generated from a GitHub metadata backup.
generated: 2024-11-21 12:12 UTC
This site is hosted by @0xB10C More mirrored repositories can be found on mirror.b10c.me