Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util::Result has confusing interface for std::*_ptr T #26004

Open
luke-jr opened this issue Sep 4, 2022 · 9 comments
Open

util::Result has confusing interface for std::*_ptr T #26004

luke-jr opened this issue Sep 4, 2022 · 9 comments

Comments

@luke-jr
Copy link
Member

luke-jr commented Sep 4, 2022

util::Result<std::unique_ptr<...>> treats a nullptr as a true value. This has caused at least one bug so far.

Can we easily forbid passing nullptr in this way? Is there a better/safer solution?

@fanquake
Copy link
Member

fanquake commented Sep 4, 2022

cc @ryanofsky

@luke-jr
Copy link
Member Author

luke-jr commented Sep 4, 2022

(For the mentioned bug, see bitcoin-core/gui#661 and fix in #26005)

@ryanofsky
Copy link
Contributor

Good catch, and I think it would be good to do something to prevent this. Few ideas:

  1. Result class could drop operator bool and force you to write if (result.has_value()) instead of if (result). I don't thnk this would really solve problem because Result<unique_ptr> could still be assigned nullptr, and result.has_value() would still return true in that case.

  2. Return type could be changed from Result<unique_ptr> to Result<not_null<unique_ptr>> or Result<nn_unique_ptr> using a not-null helper class like one from https://github.com/bitwizeshift/not_null#references. I think this would be a potentially good solution because it would make it a compile time error to assign a nullptr or plain unique_ptr to the result. Only a non-nullable pointer could be assigned to avoid the compiler error.

  3. Could introduce util::NullableResult to complement util::Result. Nullable result class would have the same behavior as normal result class except operator bool would be implemented as return has_value() && value() instead of return has_value(). The name NullableResult would clearly communicate that a null result is possible and not an error, and bool operator would make it easier to check for.

  4. Could do nothing. Result<unique_ptr>{nullptr} being true is not any different from optional<unique_ptr>{nullptr} being true or optional<int>{0} being true or const char* s{""} being true. It's kind of just how indirection works in C/C++ with multiple layers of indirection.

I tend to think (2) is nicest solution, but I'm not sure if there are other use-cases for non-nullable pointers that would justify the complexity since they don't seem completely trivial to implement. I don't like solution (1) because it creates an unnecessary difference between std::optional and util::Result and IMO would makes code using the result class look ugly. Solution (3) I think could be implemented in a few lines and I think would be better than the status quo, because it communicates that the value can be null and makes it easy to check for, even if it doesn't prevent null from being assigned.

@maflcko
Copy link
Member

maflcko commented Sep 5, 2022

See also #24423

@ryanofsky
Copy link
Contributor

See also #24423

Forgot about that discussion. This does seem like a genuine use-case for not_null.

Another idea for the list:

  1. Could replace Result<unique_ptr<Wallet>> with ResultPtr<Wallet> where a new ResultPtr class basically acts like unique_ptr except it has a util::Error constructor so it can hold error information, and is always set to an error or non-null pointer, never a plain null pointer. ResultPtr<Wallet> would be semantically equivalent to Result<not_null<unique_ptr<Wallet>>> except it would have nicer syntax and not require double dereferencing to call wallet methods.

@luke-jr
Copy link
Member Author

luke-jr commented Sep 5, 2022

The issue with not_null seems to be that in some cases, it might not detect it until runtime, and then throw an exception - which would have been a bug in this case (bitcoin-core/gui#661) too.

Can we maybe throw an exception in debug builds, but in production builds simply treat nullptr assignments as clearing?

@luke-jr
Copy link
Member Author

luke-jr commented Sep 5, 2022

  1. Could replace Result<unique_ptr> with ResultPtr where a new ResultPtr class basically acts like unique_ptr except it has a util::Error constructor so it can hold error information, and is always set to an error or non-null pointer, never a plain null pointer. ResultPtr would be semantically equivalent to Result<not_null<unique_ptr>> except it would have nicer syntax and not require double dereferencing to call wallet methods.

Would this impede having a constant Wallet object per wallet?

ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue Sep 6, 2022
This is a partial solution to bitcoin#26004. It makes it easier to avoid the mistake
of assuming that just because a function returning `util::Result<unique_ptr>`
succeeded, that the pointer it returns is also non-null. Using the `ResultPtr`
return type, `if (result)` will only be true if the function was successful
_and_ the result is non-null.

This is not a complete solution to bitcoin#26004 because a more complete solution
would use a non-nullable unique_ptr so a nullptr result is not possible at all.
This change makes the nullptr state more straightforward to check for, but does
not prevent it from occuring.

Another motivation for using the `ResultPtr` class is to make calling pointer
methods easier. For example in bitcoin#25722 if `wallet` is a
`ResultPtr<unique_ptr<CWallet>>` it is possible to call `wallet->GetName()`
instead of (*wallet)->GetName().
@ryanofsky
Copy link
Contributor

The issue with not_null seems to be that in some cases, it might not detect it until runtime, and then throw an exception - which would have been a bug in this case (bitcoin-core/gui#661) too.

Yes, we would want to use a strict not_null implementation that makes it a compile error to try to assign a nullable pointer to a non-nullable one. Strict non_null pointers can only be initialized with special constructors, and if you call those constructors correctly, the compiler can guarantee the pointers are safe to dereference after that. The benefit of this is that it is easier to verify not_null pointers are constructed properly than it is to verify nullable pointers are safe to dereference every place they are dereferenced.

Can we maybe throw an exception in debug builds, but in production builds simply treat nullptr assignments as clearing?

It would be an implementation detail of the not_null class, but it could provide a constructor that only throws in debug builds.

Would this impede having a constant Wallet object per wallet?

I'm not even sure we have this currently. interfaces::Wallet objects are disposable wrappers around a more permanent wallet::CWallet object, but I think as long as pointers can be moved from, then object lifetimes / constness should be unaffected by things proposed here.

@luke-jr
Copy link
Member Author

luke-jr commented Sep 6, 2022

I'm not even sure we have this currently. interfaces::Wallet objects are disposable wrappers around a more permanent wallet::CWallet object, but I think as long as pointers can be moved from, then object lifetimes / constness should be unaffected by things proposed here.

We don't, but I think we should. It would be nice to put the abstraction interface between all accesses - trying to make CWallet itself abstract has proven impractical (trying to add support for a Lightning wallet).

ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue Sep 20, 2022
This is a partial solution to bitcoin#26004. It makes it easier to avoid the mistake
of assuming that just because a function returning `util::Result<unique_ptr>`
succeeded, that the pointer it returns is also non-null. Using the `ResultPtr`
return type, `if (result)` will only be true if the function was successful
_and_ the result is non-null.

This is not a complete solution to bitcoin#26004 because a more complete solution
would use a non-nullable unique_ptr so a nullptr result is not possible at all.
This change makes the nullptr state more straightforward to check for, but does
not prevent it from occuring.

Another motivation for using the `ResultPtr` class is to make calling pointer
methods easier. For example in bitcoin#25722 if `wallet` is a
`ResultPtr<unique_ptr<CWallet>>` it is possible to call `wallet->GetName()`
instead of (*wallet)->GetName().
ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue Sep 20, 2022
This is a partial solution to bitcoin#26004. It makes it easier to avoid the mistake
of assuming that just because a function returning `util::Result<unique_ptr>`
succeeded, that the pointer it returns is also non-null. Using the `ResultPtr`
return type, `if (result)` will only be true if the function was successful
_and_ the result is non-null.

This is not a complete solution to bitcoin#26004 because a more complete solution
would use a non-nullable unique_ptr so a nullptr result is not possible at all.
This change makes the nullptr state more straightforward to check for, but does
not prevent it from occuring.

Another motivation for using the `ResultPtr` class is to make calling pointer
methods easier. For example in bitcoin#25722 if `wallet` is a
`ResultPtr<unique_ptr<CWallet>>` it is possible to call `wallet->GetName()`
instead of (*wallet)->GetName().
ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue Oct 26, 2023
This is a partial solution to bitcoin#26004. It makes it easier to avoid the mistake
of assuming that just because a function returning `util::Result<unique_ptr>`
succeeded, that the pointer it returns is also non-null. Using the `ResultPtr`
return type, `if (result)` will only be true if the function was successful
_and_ the result is non-null.

This is not a complete solution to bitcoin#26004 because a more complete solution
would use a non-nullable unique_ptr so a nullptr result is not possible at all.
This change makes the nullptr state more straightforward to check for, but does
not prevent it from occuring.

Another motivation for using the `ResultPtr` class is to make calling pointer
methods easier. For example in bitcoin#25722 if `wallet` is a
`ResultPtr<unique_ptr<CWallet>>` it is possible to call `wallet->GetName()`
instead of (*wallet)->GetName().
ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue Oct 26, 2023
This is a partial solution to bitcoin#26004. It makes it easier to avoid the mistake
of assuming that just because a function returning `util::Result<unique_ptr>`
succeeded, that the pointer it returns is also non-null. Using the `ResultPtr`
return type, `if (result)` will only be true if the function was successful
_and_ the result is non-null.

This is not a complete solution to bitcoin#26004 because a more complete solution
would use a non-nullable unique_ptr so a nullptr result is not possible at all.
This change makes the nullptr state more straightforward to check for, but does
not prevent it from occuring.

Another motivation for using the `ResultPtr` class is to make calling pointer
methods easier. For example in bitcoin#25722 if `wallet` is a
`ResultPtr<unique_ptr<CWallet>>` it is possible to call `wallet->GetName()`
instead of (*wallet)->GetName().
ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue Feb 21, 2024
The util::ResultPtr class is a wrapper for util::Result providing syntax sugar
to make it less awkward to use with returned pointers. Instead of needing to be
deferenced twice with **result or (*result)->member, it only needs to be
dereferenced once with *result or result->member. Also when it is used in a
boolean context, instead of evaluating to true when there is no error and the
pointer is null, it evaluates to false so it is straightforward to determine
whether the result points to something.

This PR is only partial a solution to bitcoin#26004 because while it makes it easier
to check for null pointer values, it does not make it impossible to return a
null pointer value inside a successful result. It would be possible to enforce
that successful results always contain non-null pointers by using a not_null
type (bitcoin#24423) in combination with
ResultPtr, though.
ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue Feb 21, 2024
This is a partial solution to bitcoin#26004. It makes it easier to avoid the mistake
of assuming that just because a function returning `util::Result<unique_ptr>`
succeeded, that the pointer it returns is also non-null. Using the `ResultPtr`
return type, `if (result)` will only be true if the function was successful
_and_ the result is non-null.

This is not a complete solution to bitcoin#26004 because a more complete solution
would use a non-nullable unique_ptr so a nullptr result is not possible at all.
This change makes the nullptr state more straightforward to check for, but does
not prevent it from occuring.

Another motivation for using the `ResultPtr` class is to make calling pointer
methods easier. For example in bitcoin#25722 if `wallet` is a
`ResultPtr<unique_ptr<CWallet>>` it is possible to call `wallet->GetName()`
instead of (*wallet)->GetName().
ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue May 1, 2024
The util::ResultPtr class is a wrapper for util::Result providing syntax sugar
to make it less awkward to use with returned pointers. Instead of needing to be
deferenced twice with **result or (*result)->member, it only needs to be
dereferenced once with *result or result->member. Also when it is used in a
boolean context, instead of evaluating to true when there is no error and the
pointer is null, it evaluates to false so it is straightforward to determine
whether the result points to something.

This PR is only partial a solution to bitcoin#26004 because while it makes it easier
to check for null pointer values, it does not make it impossible to return a
null pointer value inside a successful result. It would be possible to enforce
that successful results always contain non-null pointers by using a not_null
type (bitcoin#24423) in combination with
ResultPtr, though.
ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue May 1, 2024
The util::ResultPtr class is a wrapper for util::Result providing syntax sugar
to make it less awkward to use with returned pointers. Instead of needing to be
deferenced twice with **result or (*result)->member, it only needs to be
dereferenced once with *result or result->member. Also when it is used in a
boolean context, instead of evaluating to true when there is no error and the
pointer is null, it evaluates to false so it is straightforward to determine
whether the result points to something.

This PR is only partial a solution to bitcoin#26004 because while it makes it easier
to check for null pointer values, it does not make it impossible to return a
null pointer value inside a successful result. It would be possible to enforce
that successful results always contain non-null pointers by using a not_null
type (bitcoin#24423) in combination with
ResultPtr, though.
ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue Jun 17, 2024
The util::ResultPtr class is a wrapper for util::Result providing syntax sugar
to make it less awkward to use with returned pointers. Instead of needing to be
deferenced twice with **result or (*result)->member, it only needs to be
dereferenced once with *result or result->member. Also when it is used in a
boolean context, instead of evaluating to true when there is no error and the
pointer is null, it evaluates to false so it is straightforward to determine
whether the result points to something.

This PR is only partial a solution to bitcoin#26004 because while it makes it easier
to check for null pointer values, it does not make it impossible to return a
null pointer value inside a successful result. It would be possible to enforce
that successful results always contain non-null pointers by using a not_null
type (bitcoin#24423) in combination with
ResultPtr, though.
ryanofsky added a commit to ryanofsky/bitcoin that referenced this issue Jun 17, 2024
The util::ResultPtr class is a wrapper for util::Result providing syntax sugar
to make it less awkward to use with returned pointers. Instead of needing to be
deferenced twice with **result or (*result)->member, it only needs to be
dereferenced once with *result or result->member. Also when it is used in a
boolean context, instead of evaluating to true when there is no error and the
pointer is null, it evaluates to false so it is straightforward to determine
whether the result points to something.

This PR is only partial a solution to bitcoin#26004 because while it makes it easier
to check for null pointer values, it does not make it impossible to return a
null pointer value inside a successful result. It would be possible to enforce
that successful results always contain non-null pointers by using a not_null
type (bitcoin#24423) in combination with
ResultPtr, though.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants