Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[SUGGESTION] Null(ptr/opt) coalescing for safer pointer dereferencing and more pleasant optional usage #646

torshepherd started this conversation in Suggestions
Discussion options

The canonical case

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#i12-declare-a-pointer-that-must-not-be-null-as-not_null

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:

  1. std::shared_ptr does not have first-class optional semantics
  2. 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).

  1. 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>();
  1. 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_ptr and optional_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!

You must be logged in to vote

Replies: 11 comments 6 replies

Comment options

I think as can be related to this topic as described in this PR.

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Comment options

That's #27 (comment).

You must be logged in to vote
0 replies
Comment options

Thanks @JohelEGP!

You must be logged in to vote
0 replies
Comment options

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".

You must be logged in to vote
0 replies
Comment options

This issue comment is related to this topic.

So it seems C++2 will use is and as instead of ??, ?. and nullable types...

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
0 replies
Comment options

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:


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.

You must be logged in to vote
0 replies
Comment options

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.

You must be logged in to vote
1 reply
Comment options

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.

Comment options

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.

You must be logged in to vote
2 replies
Comment options

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.

Comment options

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.

Comment options

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::optional is pointer-like, so as T is throwing and as *T fails with nullptr.
  • .and_then(:(inout x) = This and_then works on pointers to std::optional, also acting as-if unengaged on nullptr.
  • x.set_default_return_type_to_forward_wildcard()); We're given a reference to the object, so access it normally.
You must be logged in to vote
3 replies
Comment options

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.

Comment options

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'
Comment options

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Converted from issue

This discussion was converted from issue #320 on August 30, 2023 22:43.

AltStyle によって変換されたページ (->オリジナル) /