Disable util::Result copying and assignment #29906

pull ryanofsky wants to merge 2 commits into bitcoin:master from ryanofsky:pr/saferesult changing 9 files +60 −45
  1. ryanofsky commented at 7:09 pm on April 18, 2024: contributor

    This PR just contains the first two commits of #25665.

    It disables copying of util::Result objects because unnecessary copies are inefficient and not possible after #25665, which makes util::Result object move-only.

    It disables the assignment operator and replaces it with an Update() method, because #25665 adds more information to util::Result objects (warning and error messages and failure values) and having an assignment operator that overwrites data instead of merging it would make it easy to accidentally erase existing information while trying to assign new information.

  2. DrahtBot commented at 7:09 pm on April 18, 2024: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage

    For detailed information about the code coverage, see the test coverage report.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK stickies-v, furszy, achow101
    Stale ACK TheCharlatan

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #29936 (fuzz: wallet: add target for CreateTransaction by brunoerg)
    • #29700 (kernel, refactor: return error status on all fatal errors by ryanofsky)
    • #29442 (wallet: Target a pre-defined utxo set composition by adjusting change outputs by remyers)
    • #28984 (Cluster size 2 package rbf by instagibbs)
    • #28676 ([WIP] Cluster mempool implementation by sdaftuar)
    • #26022 (Add util::ResultPtr class by ryanofsky)
    • #25722 (refactor: Use util::Result class for wallet loading by ryanofsky)
    • #25665 (refactor: Add util::Result failure values, multiple error and warning messages by ryanofsky)

    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.

  3. in src/wallet/test/fuzz/coinselection.cpp:297 in 974b16b258 outdated
    290@@ -291,7 +291,10 @@ FUZZ_TARGET(coinselection)
    291     }
    292 
    293     std::vector<COutput> utxos;
    294-    std::vector<util::Result<SelectionResult>> results{result_srd, result_knapsack, result_bnb};
    295+    std::vector<util::Result<SelectionResult>> results;
    296+    results.emplace_back(std::move(result_srd));
    297+    results.emplace_back(std::move(result_knapsack));
    298+    results.emplace_back(std::move(result_bnb));
    


    stickies-v commented at 4:27 pm on April 19, 2024:

    nit: Any reason not to just wrap the args in std::move? Seems like the more intuitive approach, and probably slightly more performant?

    0    std::vector<util::Result<SelectionResult>> results {
    1        std::move(result_srd), std::move(result_knapsack), std::move(result_bnb)
    2    };
    

    ryanofsky commented at 7:11 pm on April 19, 2024:

    re: #29906 (review)

    nit: Any reason not to just wrap the args in std::move? Seems like the more intuitive approach, and probably slightly more performant?

    Agreed that’s much better. Applied suggestion.


    ryanofsky commented at 4:48 pm on April 24, 2024:

    re: #29906 (comment)

    nit: Any reason not to just wrap the args in std::move? Seems like the more intuitive approach, and probably slightly more performant?

    Actually this turns out not to work because std::initializer_list list does not work with move-only types. So unfortunately vector initialization needs to be a little more verbose. I updated the PR now to disable the util::Result move constructor to catch this and any related problems.

  4. in src/wallet/spend.cpp:869 in 974b16b258 outdated
    865@@ -866,7 +866,7 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
    866         if (CAmount total_amount = available_coins.GetTotalAmount() - total_discarded < value_to_select) {
    867             // Special case, too-long-mempool cluster.
    868             if (total_amount + total_unconf_long_chain > value_to_select) {
    869-                return util::Result<SelectionResult>({_("Unconfirmed UTXOs are available, but spending them creates a chain of transactions that will be rejected by the mempool")});
    870+                return util::Error{_("Unconfirmed UTXOs are available, but spending them creates a chain of transactions that will be rejected by the mempool")};
    


    stickies-v commented at 4:34 pm on April 19, 2024:

    nit: nice, but maybe best to clean up all 3 instances in one go?

     0diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
     1index 8b62cddb02..9a7e166e68 100644
     2--- a/src/wallet/spend.cpp
     3+++ b/src/wallet/spend.cpp
     4@@ -868,7 +868,7 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
     5             if (total_amount + total_unconf_long_chain > value_to_select) {
     6                 return util::Error{_("Unconfirmed UTXOs are available, but spending them creates a chain of transactions that will be rejected by the mempool")};
     7             }
     8-            return util::Result<SelectionResult>(util::Error()); // General "Insufficient Funds"
     9+            return util::Error{}; // General "Insufficient Funds"
    10         }
    11 
    12         // Walk-through the filters until the solution gets found.
    13@@ -894,7 +894,7 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
    14 
    15 
    16         // General "Insufficient Funds"
    17-        return util::Result<SelectionResult>(util::Error());
    18+        return util::Error{};
    19     }
    20 }
    21 
    

    ryanofsky commented at 7:11 pm on April 19, 2024:

    re: #29906 (review)

    nit: nice, but maybe best to clean up all 3 instances in one go?

    Thanks! Applied patch

  5. in src/util/result.h:93 in 974b16b258 outdated
    84@@ -75,6 +85,9 @@ class Result
    85     const T& operator*() const LIFETIMEBOUND { return value(); }
    86     T* operator->() LIFETIMEBOUND { return &value(); }
    87     T& operator*() LIFETIMEBOUND { return value(); }
    88+
    89+    //! Update this result by moving from another result object.
    90+    Result& Update(Result&& other) LIFETIMEBOUND { return *this = std::move(other); }
    


    stickies-v commented at 4:43 pm on April 19, 2024:
    Update is a bit of an ambiguous name imo, it doesn’t clearly indicate if it means replacing or merging into the existing result. I think for this commit, where we can only overwrite the existing result, I think Replace would be better. For the next commits in #25665 where we actually add the merging functionality, I think Merge or Combine would be a better choice.

    ryanofsky commented at 7:11 pm on April 19, 2024:

    re: #29906 (review)

    Update is a bit of an ambiguous name imo, it doesn’t clearly indicate if it means replacing or merging into the existing result. I think for this commit, where we can only overwrite the existing result, I think Replace would be better. For the next commits in #25665 where we actually add the merging functionality, I think Merge or Combine would be a better choice.

    We could definitely have a Replace method in the future. The only reason I didn’t add one now is that so far after updating a lot of code to use the Result class in PR’s #29700 and #25722, I haven’t seen uses for a Replace method. If you think having a Replace() method would good to add here for a future use-case, or for the sake of completeness, I’d be happy to add it. However, I don’t think any changed lines here should be calling Replace() because they are just trying to update result values, not reset and replace the entire result object.

    I did originally consider calling this method Merge instead of Update but I liked Update better for 2 reasons. First, I liked consistency with python’s dict.update method which takes fields from two different dict objects and combines them, just like this Update function does. Second, I think the name Merge would be misleading in the majority of cases where individual values are not merged, only different fields are combined, and new field values overwrite old values. 1

    Your suggestion Combine could also work instead of Update but I think is less clear syntactically. For example, I think a.Update(b) maps pretty well to “update object a with contents of object b”, but it’s less obvious what a.Combine(b) would be supposed to do.


    1. It is true that with #25722, you can go out of your way to pass a custom type to util::Result and specialize ResultTraits so field values are merged when Update() is called. I haven’t seen cases where this is useful for success types, only for some failure types where multiple things can fail and you want to return information about all the failures. I think name Update is generic enough to cover atypical cases where values are merged without implying that values are merged typically. ↩︎


    furszy commented at 1:11 pm on April 25, 2024:
    q: why uppercase? Do you wanted to differentiate own functions vs standard ones?

    ryanofsky commented at 2:54 pm on April 25, 2024:

    re: #29906 (review)

    Personally I think using lowercase for instance methods and uppercase for other methods and functions is good practice because it makes it obvious when you are passing an implicit this parameter to function calls. (It is useful for the same reason m_ prefixes are useful.) However, I got pushback when I tried to allow this in the style guide in #14635 so now I usually try to follow the guide.


    stickies-v commented at 7:55 pm on April 25, 2024:

    I haven’t seen uses for a Replace method.

    I agree. The use cases I was thinking of should just be addressed with list initialization, so I think not implementing the method until we actually need it is the way to go, thanks.

    Second, I think the name Merge would be misleading … only different fields are combined

    That’s a good point, I agree Update > Merge

    it’s less obvious what a.Combine(b) would be supposed to do

    I don’t think I agree, but I do agree there’s no clear winner, so I’m happy to leave this as is.

  6. stickies-v commented at 5:03 pm on April 19, 2024: contributor

    Concept ACK, breaking it out in a smaller PR seems like a good idea, thanks.

    I’m unsure about removing the assignment operator altogether. I think it’s good to be careful about footguns, but I’m worried about making the interface overly unintuitive.

    Would it make sense to only merge another Result when using the Merge (currently called Update) method, and use the assignment operator to overwrite the current result? I think when people want to merge results, they’ll probably want to be explicit about that, and in other cases overwriting/clearing everything seems like expected behaviour?

  7. laanwj added the label Refactoring on Apr 21, 2024
  8. ryanofsky force-pushed on Apr 24, 2024
  9. ryanofsky commented at 3:52 pm on April 24, 2024: contributor

    Updated 974b16b2580f4f09c8936fbead96fdbee637cb14 -> c45cb13b9e4f6eae50ab6dc42acf471921980591 (pr/saferesult.1 -> pr/saferesult.2, compare) with suggestions

    re: #29906#pullrequestreview-2011883406

    I’m unsure about removing the assignment operator altogether. I think it’s good to be careful about footguns, but I’m worried about making the interface overly unintuitive.

    We could add back a safer, more limited form of operator= in the future if not having it causes friction. I rewrote commit message to be clearer and mention that. But I think having a named Update() method should make it easier to figure out what code is doing and avoid mistakes.

    Would it make sense to only merge another Result when using the Merge (currently called Update) method, and use the assignment operator to overwrite the current result? I think when people want to merge results, they’ll probably want to be explicit about that, and in other cases overwriting/clearing everything seems like expected behaviour?

    Hmm, I’m actually not sure what this is suggesting. If there is a need to overwrite/clear results, IMO, the best way to do that would be to add a Reset() method. I haven’t seen an actual need to do that anywhere though. The intended use for Update is to be able to write code like:

    0util::Result<SuccessType, FailureType> result;
    1if (do_something()) {
    2  result.Update(success_value);
    3} else {
    4  result.Update({util::Error{_("error message"), failure_value});
    5}
    
  10. ryanofsky force-pushed on Apr 24, 2024
  11. ryanofsky commented at 7:20 pm on April 24, 2024: contributor
    Updated c45cb13b9e4f6eae50ab6dc42acf471921980591 -> 66022315641934bda301d61f009dae9cb3b45da0 (pr/saferesult.2 -> pr/saferesult.3, compare) adding change to actually disable the copy constructor
  12. TheCharlatan approved
  13. TheCharlatan commented at 12:41 pm on April 25, 2024: contributor
    ACK 66022315641934bda301d61f009dae9cb3b45da0
  14. DrahtBot requested review from stickies-v on Apr 25, 2024
  15. in src/qt/addresstablemodel.cpp:385 in d293d0ba52 outdated
    376@@ -377,7 +377,7 @@ QString AddressTableModel::addRow(const QString &type, const QString &label, con
    377                 editStatus = WALLET_UNLOCK_FAILURE;
    378                 return QString();
    379             }
    380-            op_dest = walletModel->wallet().getNewDestination(address_type, strLabel);
    381+            op_dest.Update(walletModel->wallet().getNewDestination(address_type, strLabel));
    382             if (!op_dest) {
    383                 editStatus = KEY_GENERATION_FAILURE;
    384                 return QString();
    


    furszy commented at 1:44 pm on April 25, 2024:
    Not for this PR but.. we could remove this Update call pretty easily (e8a330584571bf0270c692676a57ebcf7d7c7697). We are calling getNewDestination twice only to verify if the wallet is locked or not.

    ryanofsky commented at 2:54 pm on April 25, 2024:

    re: #29906 (review)

    It seems like the goal of the code is to get a new address without unlocking. But I haven’t looked into this. It definitely looks a little clumsy, though, because it isn’t actually using the error message that is returned.

  16. furszy commented at 2:30 pm on April 25, 2024: member
    ACK 6602231564
  17. refactor: Drop util::Result operator=
    `util::Result` objects are aggregates that can hold multiple fields with
    different information. Currently Result objects can only hold a success value
    of an arbitrary type or a single bilingual_str error message. In followup PR
    https://github.com/bitcoin/bitcoin/pull/25722, Result objects may be able to
    hold both success and failure values of different types, plus error and warning
    messages.
    
    Having a Result::operator= assignment operator that completely erases all
    existing Result information before assigning new information is potentially
    dangerous in this case. For example, code that looks like it is assigning a
    warning value could erase previously-assigned success or failure values.
    Conversely, code that looks like it is just assigning a success or failure
    value could erase previously assigned error and warning messages.
    
    To prevent potential bugs like this, disable Result::operator= assignment
    operator.
    
    It is possible in the future we may want to re-enable operator= in limited
    cases (such as when implicit conversions are not used) or add a Replace() or
    Reset() method that mimicks default operator= behavior. Followup PR
    https://github.com/bitcoin/bitcoin/pull/25722 also adds a Result::Update()
    method providing another way to update an existing Result object.
    
    Co-authored-by: stickies-v <stickies-v@protonmail.com>
    834f65e824
  18. refactor: Avoid copying util::Result values
    Copying util::Result values is less efficient than moving them because they
    allocate memory and contain strings. Also this is needed to avoid compile
    errors in https://github.com/bitcoin/bitcoin/pull/25722 which adds a
    std::unique_ptr member to util::Result which implicity disables copying.
    6a8b2befea
  19. in src/init.cpp:996 in 6602231564 outdated
    992@@ -993,7 +993,7 @@ bool AppInitParameterInteraction(const ArgsManager& args)
    993     // ********************************************************* Step 3: parameter-to-internal-flags
    994     auto result = init::SetLoggingCategories(args);
    995     if (!result) return InitError(util::ErrorString(result));
    996-    result = init::SetLoggingLevel(args);
    997+    result.Update(init::SetLoggingLevel(args));
    


    stickies-v commented at 7:24 pm on April 25, 2024:

    I think using Update here is confusing and makes the code less clear.

     0diff --git a/src/init.cpp b/src/init.cpp
     1index 8cb593754c..678bb2a39c 100644
     2--- a/src/init.cpp
     3+++ b/src/init.cpp
     4@@ -991,10 +991,8 @@ bool AppInitParameterInteraction(const ArgsManager& args)
     5         InitWarning(strprintf(_("Reducing -maxconnections from %d to %d, because of system limitations."), nUserMaxConnections, nMaxConnections));
     6 
     7     // ********************************************************* Step 3: parameter-to-internal-flags
     8-    auto result = init::SetLoggingCategories(args);
     9-    if (!result) return InitError(util::ErrorString(result));
    10-    result.Update(init::SetLoggingLevel(args));
    11-    if (!result) return InitError(util::ErrorString(result));
    12+    if (auto result{init::SetLoggingCategories(args)}; !result) return InitError(util::ErrorString(result));
    13+    if (auto result{init::SetLoggingLevel(args)}; !result) return InitError(util::ErrorString(result));
    14 
    15     nConnectTimeout = args.GetIntArg("-timeout", DEFAULT_CONNECT_TIMEOUT);
    16     if (nConnectTimeout <= 0) {
    

    ryanofsky commented at 9:02 pm on April 25, 2024:

    re: #29906 (review)

    Thanks, applied patch

  20. in src/test/mempool_tests.cpp:263 in 6602231564 outdated
    259@@ -260,7 +260,7 @@ BOOST_AUTO_TEST_CASE(MempoolIndexingTest)
    260     tx10.vout[0].scriptPubKey = CScript() << OP_11 << OP_EQUAL;
    261     tx10.vout[0].nValue = 10 * COIN;
    262 
    263-    ancestors_calculated = pool.CalculateMemPoolAncestors(entry.Fee(200000LL).Time(NodeSeconds{4s}).FromTx(tx10), CTxMemPool::Limits::NoLimits());
    264+    ancestors_calculated.Update(pool.CalculateMemPoolAncestors(entry.Fee(200000LL).Time(NodeSeconds{4s}).FromTx(tx10), CTxMemPool::Limits::NoLimits()));
    


    stickies-v commented at 7:29 pm on April 25, 2024:

    I don’t think Update is appropriate here, would e.g. use:

     0diff --git a/src/test/mempool_tests.cpp b/src/test/mempool_tests.cpp
     1index 2858494625..41ffb76762 100644
     2--- a/src/test/mempool_tests.cpp
     3+++ b/src/test/mempool_tests.cpp
     4@@ -202,9 +202,11 @@ BOOST_AUTO_TEST_CASE(MempoolIndexingTest)
     5     tx7.vout[1].scriptPubKey = CScript() << OP_11 << OP_EQUAL;
     6     tx7.vout[1].nValue = 1 * COIN;
     7 
     8-    auto ancestors_calculated{pool.CalculateMemPoolAncestors(entry.Fee(2000000LL).FromTx(tx7), CTxMemPool::Limits::NoLimits())};
     9-    BOOST_REQUIRE(ancestors_calculated.has_value());
    10-    BOOST_CHECK(*ancestors_calculated == setAncestors);
    11+    {
    12+        auto ancestors_calculated{pool.CalculateMemPoolAncestors(entry.Fee(2000000LL).FromTx(tx7), CTxMemPool::Limits::NoLimits())};
    13+        BOOST_REQUIRE(ancestors_calculated.has_value());
    14+        BOOST_CHECK(*ancestors_calculated == setAncestors);
    15+    }
    16 
    17     pool.addUnchecked(entry.FromTx(tx7), setAncestors);
    18     BOOST_CHECK_EQUAL(pool.size(), 7U);
    19@@ -260,9 +262,12 @@ BOOST_AUTO_TEST_CASE(MempoolIndexingTest)
    20     tx10.vout[0].scriptPubKey = CScript() << OP_11 << OP_EQUAL;
    21     tx10.vout[0].nValue = 10 * COIN;
    22 
    23-    ancestors_calculated.Update(pool.CalculateMemPoolAncestors(entry.Fee(200000LL).Time(NodeSeconds{4s}).FromTx(tx10), CTxMemPool::Limits::NoLimits()));
    24-    BOOST_REQUIRE(ancestors_calculated);
    25-    BOOST_CHECK(*ancestors_calculated == setAncestors);
    26+    {
    27+        auto ancestors_calculated{pool.CalculateMemPoolAncestors(entry.Fee(200000LL).Time(NodeSeconds{4s}).FromTx(tx10), CTxMemPool::Limits::NoLimits())};
    28+        BOOST_REQUIRE(ancestors_calculated);
    29+        BOOST_CHECK(*ancestors_calculated == setAncestors);
    30+    }
    31+
    32 
    33     pool.addUnchecked(entry.FromTx(tx10), setAncestors);
    34 
    

    ryanofsky commented at 9:02 pm on April 25, 2024:

    re: #29906 (review)

    Thanks, applied patch

  21. in src/wallet/test/fuzz/notifications.cpp:113 in 6602231564 outdated
    107@@ -108,9 +108,9 @@ struct FuzzedWallet {
    108         auto type{fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)};
    109         util::Result<CTxDestination> op_dest{util::Error{}};
    110         if (fuzzed_data_provider.ConsumeBool()) {
    111-            op_dest = wallet->GetNewDestination(type, "");
    112+            op_dest.Update(wallet->GetNewDestination(type, ""));
    113         } else {
    114-            op_dest = wallet->GetNewChangeDestination(type);
    115+            op_dest.Update(wallet->GetNewChangeDestination(type));
    


    stickies-v commented at 7:39 pm on April 25, 2024:

    Alternative approach without Update:

     0diff --git a/src/wallet/test/fuzz/notifications.cpp b/src/wallet/test/fuzz/notifications.cpp
     1index d0b06b726e..d9b4cdfdf2 100644
     2--- a/src/wallet/test/fuzz/notifications.cpp
     3+++ b/src/wallet/test/fuzz/notifications.cpp
     4@@ -106,13 +106,12 @@ struct FuzzedWallet {
     5     CTxDestination GetDestination(FuzzedDataProvider& fuzzed_data_provider)
     6     {
     7         auto type{fuzzed_data_provider.PickValueInArray(OUTPUT_TYPES)};
     8-        util::Result<CTxDestination> op_dest{util::Error{}};
     9         if (fuzzed_data_provider.ConsumeBool()) {
    10-            op_dest.Update(wallet->GetNewDestination(type, ""));
    11+            return *Assert(wallet->GetNewDestination(type, ""));
    12         } else {
    13-            op_dest.Update(wallet->GetNewChangeDestination(type));
    14+            return *Assert(wallet->GetNewChangeDestination(type));
    15         }
    16-        return *Assert(op_dest);
    17+        assert(false);
    18     }
    19     CScript GetScriptPubKey(FuzzedDataProvider& fuzzed_data_provider) { return GetScriptForDestination(GetDestination(fuzzed_data_provider)); }
    20     void FundTx(FuzzedDataProvider& fuzzed_data_provider, CMutableTransaction tx)
    

    ryanofsky commented at 9:02 pm on April 25, 2024:

    re: #29906 (review)

    Thanks, applied patch

  22. in src/wallet/test/spend_tests.cpp:105 in 6602231564 outdated
    101@@ -102,7 +102,7 @@ BOOST_FIXTURE_TEST_CASE(wallet_duplicated_preset_inputs_test, TestChain100Setup)
    102 
    103     // Second case, don't use 'subtract_fee_from_outputs'.
    104     recipients[0].fSubtractFeeFromAmount = false;
    105-    res_tx = CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control);
    106+    res_tx.Update(CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control));
    


    stickies-v commented at 7:46 pm on April 25, 2024:

    And also here, I think Update is confusing and can e.g. be simplified with:

     0diff --git a/src/wallet/test/spend_tests.cpp b/src/wallet/test/spend_tests.cpp
     1index 640e63903a..963c0f838b 100644
     2--- a/src/wallet/test/spend_tests.cpp
     3+++ b/src/wallet/test/spend_tests.cpp
     4@@ -97,13 +97,11 @@ BOOST_FIXTURE_TEST_CASE(wallet_duplicated_preset_inputs_test, TestChain100Setup)
     5     // so that the recipient's amount is no longer equal to the user's selected target of 299 BTC.
     6 
     7     // First case, use 'subtract_fee_from_outputs=true'
     8-    util::Result<CreatedTransactionResult> res_tx = CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control);
     9-    BOOST_CHECK(!res_tx.has_value());
    10+    BOOST_CHECK(!CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control));
    11 
    12     // Second case, don't use 'subtract_fee_from_outputs'.
    13     recipients[0].fSubtractFeeFromAmount = false;
    14-    res_tx.Update(CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control));
    15-    BOOST_CHECK(!res_tx.has_value());
    16+    BOOST_CHECK(!CreateTransaction(*wallet, recipients, /*change_pos=*/std::nullopt, coin_control));
    17 }
    18 
    19 BOOST_AUTO_TEST_SUITE_END()
    

    ryanofsky commented at 9:02 pm on April 25, 2024:

    re: #29906 (review)

    Thanks, applied patch

  23. stickies-v commented at 8:11 pm on April 25, 2024: contributor

    Approach ACK, code LGTM 66022315641934bda301d61f009dae9cb3b45da0 with some non-blocking suggestions.

    We could add back a safer, more limited form of operator= in the future if not having it causes friction.

    I made my previous comment because I thought there are some instances of somewhat awkward Update usage in this PR. The majority of util::Result usage is/should probably not require Update() at all, because we’re just returning either a success value or an error string. But, in those simple cases, we should just use list initialization, imo, and not assignment initialization. So, I’ve left a couple of code comments that remove all the instances I find awkward.

    The intended use for Update is to be able to write code like: …

    Hmmm. In the code example you give, I’d find assignment much more intuitive, since the goal doesn’t seem to be combining results. If Update() is aimed to be used exclusively (?) when we intend to combine results, doesn’t that make for a more clear interface?

  24. ryanofsky commented at 8:49 pm on April 25, 2024: contributor

    Thanks @stickies-v, I agree with your suggestions to avoid updating existing Result objects where possible and will apply them.

    But I wonder if there are is anything else I can do to help make the interface more intuitive though. I’m not exactly clear on why it is not intuitive currently. A few places you mentioned “Update here is confusing” but I don’t understand why it is confusing. Saying result.Update(success_value) is just meant to update the result object with a success value while keeping other result data intact. Saying result.Update({util::Error{_("error message"), failure_value}); is meant to update the result object with an error message and failure value, again keeping other result data intact.

    I think it’s very similar to python dict.update function where if you start with an existing dictionary:

    0d = {'a': 1, 'b': 2, 'c': 3}
    

    and call the update method with new values:

    0d.update({'a': 4, 'd': 5})
    

    And the dictionary will be updated with the new values overwriting the old ones:

    0{'a': 4, 'b': 2, 'c': 3, 'd': 5}
    

    In the code example you give, I’d find assignment much more intuitive, since the goal doesn’t seem to be combining results. If Update() is aimed to be used exclusively (?) when we intend to combine results, doesn’t that make for a more clear interface?

    Update is just intended to update some data in a result object without necessarily erasing other data in the result object. Assigning is conceptually different and would imply that result being assigned to would be cleared and completely replaced by value being assigned. In this PR, the result object only has two mutually exclusive fields (success value and error string), so assigning and updating are the same thing. In #25665, result objects contain more fields so assigning and updating are different things.

  25. ryanofsky force-pushed on Apr 25, 2024
  26. ryanofsky commented at 9:10 pm on April 25, 2024: contributor
    Updated 66022315641934bda301d61f009dae9cb3b45da0 -> 4fb08c28ba0f14782fdab4ac54387aae0aba82ed (pr/saferesult.3 -> pr/saferesult.4, compare) with suggested changes using new result objects to avoid updating existing ones where possible Update 4fb08c28ba0f14782fdab4ac54387aae0aba82ed -> 88aa093356ff142efe60df989323eb1ed1aa9e18 (pr/saferesult.4 -> pr/saferesult.5, compare) with no changes overall, just moving changes out of first commit that belonged in second commit
  27. ryanofsky force-pushed on Apr 25, 2024
  28. stickies-v commented at 9:34 pm on April 25, 2024: contributor

    Sorry, I’ve been doing a poor job explaining myself. I am fully onboard with how Update works. When I used the word “confusion”, I was talking about usage, not interface*. Prior to this push, Update() was used in a couple of places where we know or expect the Result to be empty, and in those case I think we should just be explicit about that and use list initialization. When I see result.Update() being called, I have to look up which values are in result already and ensure that’s correct, whereas with auto result{...}; I know right away that we’re starting from an empty result.

    To stick with the python analogy: using update when we know or expect a to be an empty dictionary, is imo an anti-pattern and should be avoided.

    0a = {}
    1// do some logic to calculate `value`
    2a.update({1: value})
    

    Instead, it is much clearer to use assignment, so the reader doesn’t have to figure out which values a held already.

    0// do some logic to calculate `value`
    1a = {1: value}
    

    In short: I think it would be helpful if we only use result.Update() when result (potentially) has had fields set already, and use list initialization (or if necessary assignment initialization) for when we know (and thus enforce that) result is empty. This is also the concern I had with the code example at the end of this comment.

    I hope I did a better job explaining myself, but otherwise I’m happy to talk about it more offline to not pollute this PR more.

    • well, except for making sure the method names align with how we intend people to use them, hence my earlier renaming suggestions.
  29. ryanofsky force-pushed on Apr 26, 2024
  30. ryanofsky commented at 1:11 am on April 26, 2024: contributor

    Rebased 88aa093356ff142efe60df989323eb1ed1aa9e18 -> 0cbcb66257db65f0a68d82d17a2d1a83d8654b38 (pr/saferesult.5 -> pr/saferesult.6, compare) dropping Result::Update() method in this PR, and last 2 uses of it. Update 0cbcb66257db65f0a68d82d17a2d1a83d8654b38 -> e1be443315be6ba6c80267e0e6be59deee77de50 (pr/saferesult.6 -> pr/saferesult.7, compare) just changing a commit message to no longer mention Result::Update().


    re: #29906 (comment)

    Thanks for clarifying. I think we might disagree whether certain uses of an update function would be antipatterns, but I completely agree with all the code changes you suggested, and that in general if you have a choice between initializing values once or letting them change over time, you should prefer initializing them once so there’s less changing state to reason about. I also looked at remaining two update calls in this PR, and thought they both were not good uses, so I removed them as well.

    My update example was very simple because I wanted to show minimal code that demonstrated how to use the update function, but the code was probably too simple to demonstrate why you would want to use it. The use-case for update is when you have a function that is performing multiple operations and you want to accumulate warnings and errors from all the operations and return them. Examples would be the BlockManager::SaveBlockToDisk function from #29700 or the CompleteChainstateInitialization function from #25665. I think these are reasonable uses of the update function, but it’s possible there could be better ways to write this code, and you might have another idea what it should look like.

  31. ryanofsky force-pushed on Apr 26, 2024
  32. in src/test/mempool_tests.cpp:270 in e1be443315 outdated
    268+    {
    269+        auto ancestors_calculated{pool.CalculateMemPoolAncestors(entry.Fee(200000LL).Time(NodeSeconds{4s}).FromTx(tx10), CTxMemPool::Limits::NoLimits())};
    270+        BOOST_REQUIRE(ancestors_calculated);
    271+        BOOST_CHECK(*ancestors_calculated == setAncestors);
    272+    }
    273+
    


    stickies-v commented at 12:55 pm on April 26, 2024:
    nit: unnecessary extra newline

    ryanofsky commented at 10:11 pm on April 26, 2024:

    re: #29906 (review)

    nit: unnecessary extra newline

    Thanks, removed

  33. stickies-v approved
  34. stickies-v commented at 1:23 pm on April 26, 2024: contributor

    ACK e1be443315be6ba6c80267e0e6be59deee77de50

    The use-case for update is when you have a function that is performing multiple operations and you want to accumulate warnings and errors from all the operations and return them.

    I agree, and from what I’ve seen so far, I would prefer that to be the only use-case.

    I also looked at remaining two update calls in this PR, and thought they both were not good uses, so I removed them as well.

    Ah, nice one. Given that chaining results is not actually implemented yet, it makes sense that there should be no existing usage of Update in these first commits. I looked at the initial implementation of these 2 new updates to make sure we’re not missing anything and indeed, chaining is not relevant.


    I’ve posted my response to the second part of your comment about Update() usage on #25665, since Update() is no longer in this PR.

  35. DrahtBot requested review from TheCharlatan on Apr 26, 2024
  36. DrahtBot requested review from furszy on Apr 26, 2024
  37. in src/wallet/test/fuzz/notifications.cpp:114 in e1be443315 outdated
    113         } else {
    114-            op_dest = wallet->GetNewChangeDestination(type);
    115+            return *Assert(wallet->GetNewChangeDestination(type));
    116         }
    117-        return *Assert(op_dest);
    118+        assert(false);
    


    maflcko commented at 1:27 pm on April 26, 2024:
    nit: Is this line needed?

    ryanofsky commented at 10:14 pm on April 26, 2024:

    re: #29906 (review)

    nit: Is this line needed?

    Nope, dropped this

  38. in src/validation.cpp:974 in 616572cbb4 outdated
    970@@ -970,11 +971,13 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
    971         if (ws.m_vsize > EXTRA_DESCENDANT_TX_SIZE_LIMIT) {
    972             return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "too-long-mempool-chain", error_message);
    973         }
    974-        ancestors = m_pool.CalculateMemPoolAncestors(*entry, cpfp_carve_out_limits);
    975-        if (!ancestors) return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "too-long-mempool-chain", error_message);
    976+        if (auto ancestors{m_pool.CalculateMemPoolAncestors(*entry, cpfp_carve_out_limits)}) {
    


    furszy commented at 1:54 pm on April 26, 2024:
    This should be shadowing the previous ancestors variable (why the compiler isn’t complaining about this?). The else path can also access the variable when it is declared inside the if statement.

    ryanofsky commented at 2:20 pm on April 26, 2024:

    This should be shadowing the previous ancestors variable (why the compiler isn’t complaining about this?). The else path can also access the variable when it is declared inside the if statement.

    I can rename the variable to something else if you have a suggestion. Shadowing is also present in the qt/addresstablemodel.cpp change. The shadowing is intended, and I don’t think shadowing in cases like these is necessarily bad. I think shadowing is sometimes better than having multiple similarly-named variables that could be confused and lead to bugs when the wrong variable is used.


    furszy commented at 2:35 pm on April 26, 2024:

    I guess the “issue” is how people reading this in the future would differentiate an intended shadowing from an unintended one.-Wshadow will output a warning for this. Adding a special suppression tag just for this seems excessive to me.

    But, in any case, it’s not a big deal if you don’t think it’s worth it. I also don’t think this is a problem anyway.


    ryanofsky commented at 10:14 pm on April 26, 2024:

    re: #29906 (review)

    But, in any case, it’s not a big deal if you don’t think it’s worth it. I also don’t think this is a problem anyway.

    I’m happy to go with any suggestion. I don’t see shadowing as a problem in all cases, so this is just the best way to write the code that I could think of.


    ryanofsky commented at 2:19 pm on April 30, 2024:

    re: #29906 (review)

    I’m happy to go with any suggestion. I don’t see shadowing as a problem in all cases, so this is just the best way to write the code that I could think of.

    Not sure why I couldn’t think of a good way to write this without shadowing last week. But added _retry suffixes now so the code should be clearer and shadowing should be gone :sunglasses:

  39. DrahtBot requested review from furszy on Apr 26, 2024
  40. achow101 commented at 6:37 pm on April 26, 2024: member
    ACK e1be443315be6ba6c80267e0e6be59deee77de50
  41. in src/util/result.h:50 in e1be443315 outdated
    45+    //! Disallow operator= to avoid confusion in the future when the Result
    46+    //! class gains support for richer error reporting, and callers should have
    47+    //! ability to set a new result value without clearing existing error
    48+    //! messages.
    49+    Result& operator=(const Result&) = default;
    50+    Result& operator=(Result&&) = default;
    


    achow101 commented at 6:47 pm on April 26, 2024:
    Can you briefly explain why these operator= are made private as opposed to deleting them?

    ryanofsky commented at 10:11 pm on April 26, 2024:

    re: #29906 (review)

    Can you briefly explain why these operator= are made private as opposed to deleting them?

    Changed these to delete. The default operators were needed in an earlier version of the PR which had an Update() method which called them privately.

  42. ryanofsky force-pushed on Apr 26, 2024
  43. ryanofsky commented at 10:38 pm on April 26, 2024: contributor
    Updated e1be443315be6ba6c80267e0e6be59deee77de50 -> 9552619961049d7673f84c40917b0385be70b782 (pr/saferesult.7 -> pr/saferesult.8, compare) with small suggested cleanups
  44. stickies-v commented at 2:12 pm on April 27, 2024: contributor
    Re-ACK 9552619961049d7673f84c40917b0385be70b782 , just addressing nits, no other changes
  45. DrahtBot requested review from achow101 on Apr 27, 2024
  46. furszy commented at 12:37 pm on April 29, 2024: member
    ACK 9552619961049d7673f84c40917b0385be70b782
  47. TheCharlatan approved
  48. TheCharlatan commented at 2:03 pm on April 30, 2024: contributor
    ACK 9552619961049d7673f84c40917b0385be70b782
  49. ryanofsky force-pushed on Apr 30, 2024
  50. ryanofsky commented at 2:23 pm on April 30, 2024: contributor
    Updated 9552619961049d7673f84c40917b0385be70b782 -> 6a8b2befeab25e4e92d8e947a23e78014695e06c (pr/saferesult.8 -> pr/saferesult.9, compare) tweaking variables to avoid shadowing
  51. stickies-v commented at 2:54 pm on April 30, 2024: contributor
    re-ACK 6a8b2befeab25e4e92d8e947a23e78014695e06c
  52. DrahtBot requested review from furszy on Apr 30, 2024
  53. DrahtBot requested review from TheCharlatan on Apr 30, 2024
  54. achow101 commented at 4:04 pm on April 30, 2024: member
    ACK 6a8b2befeab25e4e92d8e947a23e78014695e06c
  55. achow101 merged this on Apr 30, 2024
  56. achow101 closed this on Apr 30, 2024


github-metadata-mirror

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