-
Notifications
You must be signed in to change notification settings - Fork 269
[SUGGESTION] Null(ptr/opt) coalescing for safer pointer dereferencing and more pleasant optional usage #646
-
The canonical case
Smart pointers can be nullptr. Dereferencing them is therefore always dangerous, and the following is the main way of guaranteeing safety:
struct P { int innerVal; }; std::shared_ptr<P> x; std::optional<int> inner = x ? std::optional(x->innerVal) : std::optional<int>();
This is a pain that stems from two issues:
- std::shared_ptr does not have first-class optional semantics
- C++ does not have sugared optional operators for null coalescing
Optional sugar
Modern languages like Typescript and Rust provide ways to really nicely work with optional types using different forms of null coalescing to values.
Typescript:
let x: string | undefined; y = x! // throw if undefined. y must have type string y = x ?? 'hello' // shorthand for x ? x : 'hello'. y must have type string y = x?.endsWith('ello') // If x is undefined, return undefined. Otherwise do the thing. y must have type bool | undefined
The last one, in particular, allows for method chaining with early-exit if the return is undefined at any point along the chain.
This sugared way of working with optional types means that there is basically no reason not to do so whenever it makes sense because the barrier to usage is so low. The TSC tooling in particular makes it a breeze to use these things in a modern text editor, doing stuff like converting . to ?. if the left hand side of the dot access is an optional type. It's awesome.
The proposal
Two ideas (can be decoupled).
- Sugary operators for null coalescing. Could just use the typescript ones directly to apply to std optional IMO.
x: std::optional<std::string>;
y = x!; // x.value()
// Alternatively could be:
y = x as std::string;
y = x ?? "hello"; // x.value_or("hello")
y = x?->ends_with("ello"); // x ? std::optional<bool>(x->ends_with("ello")) : std::optional<bool>();
- First-class optional support for smart pointers. Consider the following code:
struct P { int innerVal; };
x: std::shared_ptr<P>;
inner = x?->innerVal;
IMO this would make working with both boxed values and optional types a real pleasure compared to where we are now in C++.
Other options
- Some way to encourage people to use not_null smart pointers instead of regular nullable smart pointers. Maybe offer
not_null_shared_ptrandoptional_shared_ptr = std::optional<std::shared_ptr>
Tooling support
One could easily imagine a clang-tidy-like rule that checks for unchecked access to smart pointers and suggests changing -> to ?->. This would make finding and preventing potential segfaults an automated process, with the only manual effort going into handling the nullopt cases of the optional dereferencing operator.
Let me know if this sounds intriguing and I can work on a PR!
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 11 comments 6 replies
-
I think as can be related to this topic as described in this PR.
Beta Was this translation helpful? Give feedback.
All reactions
-
I think we had a discussion where Herb talked about how he talked with the C# team and that there were issues with having the nullable support throughout the language, and that he didn't want to tackle that just yet. I tried searching for it but couldn't find it.
Beta Was this translation helpful? Give feedback.
All reactions
-
That's #27 (comment).
Beta Was this translation helpful? Give feedback.
All reactions
-
Thanks @JohelEGP!
Beta Was this translation helpful? Give feedback.
All reactions
-
I'm especially wary of the C# treatment of optional and null, because for years now I've been hearing from the C# design team that plumbing nullability through the C# language cost more than it was worth. We should learn from the experience of other languages, not only what they did but how it worked out for them and whether they would do it again. (I don't know if that experience about language support for nullability was the same as their language support for optionality.)
I can see "plumbing nullability through the C# language" as being very different than "syntactic sugar for dealing with null/optional". However, it does have some pitfalls that need to be dealt with as far as short circuiting and single execution that could be difficult to deal with in the conversion to C++1. I could also see wanting to combine nullability with the general concept of using as to determine "emptyness".
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 2
-
This issue comment is related to this topic.
So it seems C++2 will use is and as instead of ??, ?. and nullable types...
Beta Was this translation helpful? Give feedback.
All reactions
-
Some way to encourage people to use not_null smart pointers instead of regular nullable smart pointers. Maybe offer not_null_shared_ptr and optional_shared_ptr = std::optionalstd::shared_ptr
I would say that cpp2 could (or should) provide non-nullable smart pointers by default and have the user use the smart pointers in an optional otherwise. That way you could use the optional interface for null checking.
Also, if cpp2 has terser syntax for error handling other than try-catch blocks, maybe the same could be extended for null checking.
P2561 proposes a control flow operator which could be considered to compose with deterministic exceptions.
Beta Was this translation helpful? Give feedback.
All reactions
-
Modern languages like Typescript and Rust provide ways to really nicely work with optional types using different forms of null coalescing to values.
Typescript:
let x: string | undefined; y = x! // throw if undefined. y must have type string y = x ?? 'hello' // shorthand for x ? x : 'hello'. y must have type string y = x?.endsWith('ello') // If x is undefined, return undefined. Etc
Saying "throw if undefined" and "return undefined" is probably a bit too casual. You're right that both Typescript and Rust offer sugar for dealing with optional-T types, but their semantics differ on whether the undefined/None case causes an implicit throw/return from the enclosing function. (I'd also be surprised if C++2's is and as would imply control flow like this).
I'll also note that, even though the OP mainly brought up TypeScript and its equivalent of C++'s std::optional<T>, Rust's ? operator can also apply to its Result equivalent of C++'s std::expected<T, E>.
I don't know of an idiomatic TypeScript equivalent of std::expected but, in Rust, its Option and Result types are ubiquitous.
Do we want C++2 to encourage std::expected use? @hsutter in #27 (comment) did say:
Do we have data that this is what we encourage C++ developers to do today by hand? I'm a fan of making default the things we already teach.
It's hard to measure what "we already teach" regarding std::expected since that only debuted in C++23. However:
- The Chromium web browser's C++ codebase defines
RETURN_IF_ERRORandASSIGN_OR_RETURNmacros (with Rust-style "escape from the enclosing function if there's an error (and propagating that error)" semantics) for dealing withstd::expectedvalues. Chromium'sbase::expectedis a C++17 compatible backport of C++23'sstd::expected. - The STX library similarly defines
TRY_OK(etc, result_expr)andTRY_SOME(etc, option_expr)macros that implicitly return on error/None.
As their capitalization suggests, RETURN_IF_ERROR and TRY_SOME are macros, because they need to be able to return. If C++2 doesn't have macros, then IIUC having equivalent ergonomics (when dealing with std::expected) would mean having to bake something (a ? operator or equivalent) into the language itself.
Beta Was this translation helpful? Give feedback.
All reactions
-
Do we want C++2 to encourage std::expected use?
While we don't want to encourage use of std::expected in cpp2, deterministic exceptions (error handling mechanisms in cpp2) is just expected built at a language level, so maybe we want to have something similar to rust's ? in cpp2. If we get that, then it'll be very easy to extend it to work with optional. Thats why I said,
Also, if cpp2 has terser syntax for error handling other than try-catch blocks, maybe the same could be extended for null checking.
Other than that, std::optional has a nice interface in cpp23 and cpp2 may want to capitalise on that.
Beta Was this translation helpful? Give feedback.
All reactions
-
we don't want to encourage use of std::expected in cpp2
Can you elaborate on why we don't? It seems weird to add std::expected to the latest-and-greatest C++ only to discourage its use. Is it because cpp2 targets C++20, not C++23?
maybe we want to have something similar to rust's
?in cpp2
Ah, I missed that you'd already mentioned P2561, "A control flow operator". That can't use a bare ? because of ambiguity with cpp1's ternary x?y:z, but cpp2 is less constrained.
Beta Was this translation helpful? Give feedback.
All reactions
-
Can you elaborate on why we don't? It seems weird to add std::expected to the latest-and-greatest C++ only to discourage its use
Because the intended error handling mechanism in cpp2 is supposed to be deterministic exceptions (or Herbceptions as many call them).
See Herb's P0709 for details.
Ah, I missed that you'd already mentioned P2561, "A control flow operator". That can't use a bare ? because of ambiguity with cpp1's ternary x?y:z, but cpp2 is less constrained.
I think I'm missing your point here but I'm not worried about syntax (as long as its short), more about if the feature has a place in cpp2 or not.
Beta Was this translation helpful? Give feedback.
All reactions
-
Ah, I'd seen P0709 some years ago but had since forgotten about it.
Well, now I feel a bit silly trying to tell hsutter and the rest of you things like how std::expected works.
Beta Was this translation helpful? Give feedback.
All reactions
-
I re-read P0709. The note in §4.5.1 mentions that try-expressions could unify exceptions and std::expected. You could say try expr when expr throws, per P0709, but also when expr has type std::expected (and that type implements operator try).
Given that std::expected is actually part of standard C++ now (and the existing C++ code that cpp2 aims to interop with is starting to use it), there might still be an ergonomic benefit to cpp2 allowing try an_expr_of_type_expected without first requiring our C++ compilers support Herbceptions, with syntax and semantics still consistent with future support of a Herbceptional try an_expr_that_throws.
At the risk of bike-shedding, cpp2 may eventually have none, either or both of try expr syntax and the original suggestion's expr? or expr?? syntax. Regardless, perhaps try expr should be control flow (like return and like the Rust example) and ? (or postfix-! or similar operators) should be short circuiting (like && and like the TypeScript example) but not control flow. This would deliberately differ from Rust's postfix-? syntax being control flow.
Beta Was this translation helpful? Give feedback.
All reactions
-
How about just using UFCS and monadic operations that are aware of the desired idiom?
This was inspired by the success of #741 (comment) while reading 2c03e41#diff-43ba71198da8225f205041c699dba495883d7d416c0b58916bf226aae97bafaeR4025-R4027:
if (type.index() == a_function) { std::get<a_function>(type).get()->set_default_return_type_to_forward_wildcard(); }
(type as *function_typeid).and_then(:(inout x) = x.set_default_return_type_to_forward_wildcard());
(type as *function_typeid)IIRC,std::optionalis pointer-like, soas Tis throwing andas *Tfails withnullptr..and_then(:(inout x) =Thisand_thenworks on pointers tostd::optional, also acting as-if unengaged onnullptr.x.set_default_return_type_to_forward_wildcard());We're given a reference to the object, so access it normally.
Beta Was this translation helpful? Give feedback.
All reactions
-
😄 1
-
y = x?->ends_with("ello"); // x ? std::optional<bool>(x->ends_with("ello")) : std::optional<bool>();
You can get partway there with a nice short function that signifies that.
y = x.c(:(z) z.ends_with("ello"));
// `c` for coalesce
c: (o, f) -> decltype(o.value().f()) = {
if (o.has_value()) {
return o.value().f();
}
return std::nullopt;
}
Not as succinct as first class support, of course.
Not good on code auto completion, either.
Abusing unary operator overloading should get you much closer.
But that gets in the way of short-circuiting.
So perhaps abusing a binary operator would be better.
Beta Was this translation helpful? Give feedback.
All reactions
-
I tried building an example (https://cpp2.godbolt.org/z/KaKqhhWcr):
expensive := :(o) -> _ = ((z = (0..<(1'000'000'000+std::rand()%9)).sum(), o));
std::cout << (x | expensive | expensive).to_string() << '\n';
std::cout << (x | :(y) -> _ = y.ends_with("ello"))* << '\n'
Beta Was this translation helpful? Give feedback.
All reactions
-
y = x?->ends_with("ello"); // x ? std::optional<bool>(x->ends_with("ello")) : std::optional<bool>();
Without blessing this particular use case,
in order to support completion,
while still abusing unary operator overloading,
it'd be necessary to allow hooking into the UFCS mechanism.
The purpose of the UFCS hook is to check whether the optional is engaged before proceeding with the UFCS call.
That's similar to a contract predicate.
Of course, you still need to return std::nullopt.
So it'd be like some predicate_or (contract predicate behavior + failure case value).
Maybe the UFCS hook would also support some use cases for smart references.
I'd have to reread some proposals or articles to remember about that.
Beta Was this translation helpful? Give feedback.