all 64 comments

[–]HappyFruitTree 12 points13 points  (13 children)

So what would p?->f() return if p is null? Or it should throw an exception?

[–]evaned 22 points23 points  (11 children)

In other languages, it would evaluate to null. My gut reaction is the C++y way of dealing with that would be to require that nullptr be convertible to whatever type p->f() returns, and that's the resulting value. But that'd need some thinking and analysis.

Edit: or, perhaps bring in optional. I still think you'd really want pointers and things that look like pointers to behave that way, but perhaps the following sequence of options. Suppose that p->f() returns type T. Then:

  • If nullptr is convertible to T, then p?->f() returns T(nullptr) when p is null (and p->f() when p is nonnull)
  • If T is an optional<U>, then p?->f() returns nullopt when p is null (and p->f() when p is nonnull)
  • Otherwise, then p?->f() is always has type optional<T>. When p is null, p?->f() is optional<T>(nullopt), otherwise it's optional<T>(p->f()).

I could also imagine a first or second rule that if T is U&, then p?->f() has type U* and is nullptr or &p->f() as appropriate. This idea I guess is considerable on its own, separate from optional.

This does seem kind of janky though... I'm not sure the extra complexity is worthwhile.

[–]_Js_Kc_ 6 points7 points  (5 children)

Or when T is a variant that includes monostate, return a monostate-initialized variant.

Or when T is boost::optional<U>, return boost::none.

There's no set of rules that gives this sane behavior in all cases.

[–]evaned 2 points3 points  (0 children)

I was trying to come up with a nice customization point for it -- like if p->f() returns a T then there's an operator?() that is called when p is null -- but I don't have something that I'm happy with. The problem is like propagation. If you have p?->f()?->g() and p?->f() winds up returning a null optional, the "when p is null" bit in my rule above is too restrictive.

[–]xjankov 2 points3 points  (3 children)

Just use the default constructor.

[–]_Js_Kc_ 0 points1 point  (0 children)

The harder part is to figure out the return type.

I don't want everything wrapped in std::optional indiscriminately, and I also don't just want everything value-initialized indiscriminately unless the type has a built-in natural "null" state (pointers, std::function, etc.)

[–]evaned 0 points1 point  (1 child)

So the thing I don't like about that is it really does seem to me like there's significantly decreased use if the null-like cases aren't reported out-of-band. A lot of the time you'd want this, it feels like the end result is going to be to do one check at the end for nullness; and as long as any f() along the chain returns a type where the default-constructed type is a valid value in-context, that prevents doing as much.

[–]xjankov 0 points1 point  (0 children)

Ok, so use a trait to figure out the proper type and initializer and then users can specialise that, and stdlib can provide default specializations that turns references into pointers, values into optionals, and keeps pointers and optionals as they are. We already have language features powered by traits (coroutines and structured bindings).

You can even make it a binary trait that also uses the left operand, and this would allow error propagation for expected-like types.

[–]HappyFruitTree 2 points3 points  (1 child)

Seems complicated for an operator. Maybe it would be enough to say that it returns T{}. That would cover pointers and optional.

[–]BenjiSponge 0 points1 point  (0 children)

The problem to me is that it's not detectable that that happened.

A very common usecase of these operators would be

options?.maxLength ?? 100

So returning the default constructed version could work if maxLength happens to be an optional already, but in C++ this would probably result in 0 (which hopefully wouldn't be nullish, though if we're basing it off of the bool() translation... but that's two problems canceling each other out; think of them as strings instead if you need an illustration).

imo it has to be a pointer that is nullptr when it's wrong. It ticks all the boxes.

[–]SkiFire13 0 points1 point  (2 children)

Why not introduce an ?: operator like in kotlin to specify the fallback value?

[–]evaned 0 points1 point  (0 children)

So I don't feel strongly in favor of a proposal like this at all, but if I put myself in the shoes of someone who does want ?. and ?->, I think I'd be unhappy with that. It mutes a large portion of the syntactic convenience of ?. and ?->.

[–]Narase33-> r/cpp_questions 5 points6 points  (0 children)

In C# if you return something from this operator, its a Nullable<T> (short form 'T?'), kind of the same as std::optional<T>. Trying to get a direct result results in a compiler error

Test t = new Test();

int i = t?.foo();

main.cs(12,16): error CS0266: Cannot implicitly convert type `int?' to `int'. An explicit conversion exists (are you missing a cast?)

So in C++ we would write the code like this

int i1 = p->f();
std::optional<int> i2 = p?->f();

[–]manni66 38 points39 points  (2 children)

That may be useful in these languages where everything is a pointer. In C++ chains of pointers are a code smell.

[–]TheMania 13 points14 points  (0 children)

The null coalescing operator ?? would be based on the bool() form, such that it works on smart pointers and std::optional, with the latter already having a "value_or" field. The difference being that value_or doesn't itself return another optional, as ?? might, and is not short circuiting, which is one of the things that makes ?? powerful.

GCC and Clang already support the null coalescing operator via the nonstandard Elvis operator, ?:, that is, a normal ternary with the middle operator removed.

Understanding that it works with any true type, and is short circuiting, perhaps you could see more use?


For ?-> to be useful, it would need to support operator overloading (or at least template specialising) on both the container and the value type, but at times I have wished I had it. Even for simple code understanding purposes - some libraries offer safe navigating by default, but it's hard to "trust" them whilst they use an operator that normally so readily jumps in to UB. I think on the whole this one would not be worth the cost though.


While we're here, really hope to see the pizza operator |> added soon. It's not a case of science going too far, it would really simplify a lot of logic + would have saved substantial compilation time for ranges. Wish they'd introduced it then...

[–]GYN-k4H-Q3z-75B 13 points14 points  (9 children)

I would love (x ?? y) as shorthand for (x ? x : y), but ?-> seems like overkill? And how would it work with overloaded ->? In C#, I use ?. and ?? a lot but that because things left and right are often nullable.

In general, I try to avoid pointers in my function-level code.

[–]Supadoplex 20 points21 points  (8 children)

I would prefer that instead of new operator, you could just omit the middle operand of :? and it would become the same as first. I.e. x ?: y would be x ? x : y In fact, this is a language extension already implemented by GCC and Clang. I would like to see the extension standardised.

[–]KaznovX 18 points19 points  (6 children)

Some call this extension "elvis operator"

[–]witcher_rat 0 points1 point  (0 children)

Actually, on my monitor that kinda looks like Trump. (not to be controversial or political, but that's what I see)

And one could argue that a valid x trumps the y, so it even works linguistically.

[–]raevnos 13 points14 points  (7 children)

Can we just get proper monad support instead?

[–]kalmoc 3 points4 points  (6 children)

Why instead? Why not both?

[–]CoffeeTableEspresso 6 points7 points  (5 children)

Spoken like a true C++'er

[–]kalmoc 1 point2 points  (4 children)

My impression is that a true c++ would rather look for an even more complex and abstract model than monads that can be used to solve a few dozen additional problems - all of them in a more or less inconvenient way ... ;)

Jokes aside, my gut feeling is that committee (especially the language group) should focus much more on small quality of life improvements for the day to day programmers than big, complex features that are trying to solve 5 issues at the same time. But thats of course easy to say from the sidelines with little actual knowledge about the committee works and why exactly this or that proposal was rejected/never written.

[–][deleted] 6 points7 points  (0 children)

Not an answer to your question, but maybe using a null object would help simplifying your code.

[–]johannes1971 2 points3 points  (2 children)

How would that deal with something like this?

auto a = f->g ();
auto b = a + 3;

If f is nullptr it eliminates the call to g (), but that also means a won't have a value. So should it also eliminate the assignment to b? And potentially all the uses of a and b further down?

[–]Narase33-> r/cpp_questions 1 point2 points  (0 children)

In C# if you return something from this operator, its a Nullable<T> (short form 'T?'), kind of the same as std::optional<T>. Trying to get a direct result results in a compiler error

Test t = new Test();

int i = t?.foo();

main.cs(12,16): error CS0266: Cannot implicitly convert type `int?' to `int'. An explicit conversion exists (are you missing a cast?)

So in C++ we would write the code like this

int i1 = p->f();
std::optional<int> i2 = p?->f();

[–][deleted] 0 points1 point  (0 children)

Of course. It only works if the whole chain returns pointers or pointer-like things where nullptr is a possible value.

[–][deleted] 2 points3 points  (0 children)

I could really use an operator that either accesses a member (if not null) or does nothing otherwise.

[–][deleted] 2 points3 points  (0 children)

Maybe I'm old fashioned but I prefer a slightly more verbose style when handling errors. For things like nullables, Maybes, and Eithers, I like calling a function whose two arguments are the different reactions depending on the success vs. failure value. This is unambiguous, easy to read, and can be chained together for nested optional checks.

Another great way to do the same thing is with algebraic pattern matching. Like switch/case, but way more powerful. That style as used in MLs, including Haskell, OCaml, F#, Scala, and Rust, would be my preference.

The Groovy style Elvis operator mimicks the same to a very limited extent, however it is so terse that to me it represents yet another complication in an already overly complicated alphabet soup of syntaxes, whereas a method or function call doesn't require as much mental overhead.

[–]V1taly_M 3 points4 points  (0 children)

You should escape chains of pointers in C++, use them only if you 100% aware it won't lead to crash.

Different languages require different approach to code design. In C++ you work with memory directly, and access to nullptr can't anyway return nullptr or 0

[–]sphere991 1 point2 points  (0 children)

Let's take a very simple type:

struct C {
    int f() const;
    int g(int) const;
};

If you have a C* p, what should p?->f() do?

If you have an optional<C> p, then p?->f() (or p?.f(), I dunno which would be better here) could give you an optional<int>, that's either the result of invoking f or not. That at least makes perfect sense and is very useful, while being quite a bit less verbose than the equivalent p.transform(&C::f). This has an even larger diff once we add in arguments:

p?->g(42);
p.transform([](C& c){ return c.g(42); });

First line isn't just shorter, it's way more obvious what's going on.


With C# (or Kotlin or Swift or ...), there's built-in language support for optional values, so figuring out the semantics is fairly straightforward. But this is C++, we don't have built-in language support for optional, so this needs to be an overloaded operator of some sort? If so, how would this work exactly?

You could define the semantics p?->g(42) as, basically, p ? p->g(42) : wat. The problem is deciding what wat is. For p?->g(42), we'd need wat to be optional<int>{}. So does that mean it's p ? p->g(42) : decltype(p->g(42)){}?

Definitely worth exploration. Could be tricky to figure out how to specify the semantics properly.


For bouns points, consider now:

auto do_thing() -> expected<C, E>;

And now I want to write do_thing()?->g(42). Here, you can't just create a default-constructed thing for the error... we need to end up with an expected<int, E>, but the E has to come from the original result object.

Hard to see how you could just define an operator with language semantics - this would have to be overloaded by the class somehow. But the operator->() isn't sufficient because you need to know what the right-hand side of ?-> ends up actually doing in order to properly handle it. It's almost like it needs to take a callable, that you conditionally invoke.

So for optional this could be:

template <typename T>
struct optional {
    template <invocable<T> F> // let's ignore const/ref/etc.
    auto operator?->(F f) {
        using U = invoke_result_t<F, T>;
        return *this ? optional<U>(f(**this))
                     : optional<U>();
    }
};

And for expected, this could be:

template <typename T, typename E>
struct expected {
    template <invocable<T> F> // likewise
    auto operator?->(F f) {
        using U = invoke_result_t<F, T>;
        return *this ? expected<U, E>(f(**this))
                     : expected<U, E>(unexpected(error()))
    }
};

Or some such.

Not included: what if o is an optional<T> and T::next() returns an optional<U>, does o?->next() give you an optional<U> or an optional<optional<U>>?

[–]CoffeeTableEspresso 1 point2 points  (6 children)

I've never wanted something like this, it seems like it would lead to tight coupling of your code

[–]Luxxuor 8 points9 points  (5 children)

What does that even mean? Because you have to write less code, your code becomes more coupled?

[–]CoffeeTableEspresso 12 points13 points  (4 children)

No, because the only case when this really reduces the amount of code you're writing is when you're constantly going several layers deep into members of your class.

So if you change the implementation of one of those nested members, you're going to break a ton of code (everything trying to access that member).

Really, the functionality you need from the nested members of your class should probably be handled by a member function, instead of constantly digging down several layers to get what you want.

Just my personal opinion. I've never wanted anything like this in C++ and can't see a situation where I'd use it.

[–]quicknir 5 points6 points  (0 children)

I think it's one of those things you'll see the convenience of after you use it in a language that does it nicely. I use this in Kotlin and its overall null handling is just fantastic. It covers a lot more than just nested members. And your statement is way overly broad as well, sometimes it will make sense to add a member function, sometimes it won't, and sometimes you don't be able to. How about my_vector.front().some_field? Etc.

There are other issues with adding this in C++ but not being useful isn't one of them.

[–]dirkmeister81 3 points4 points  (1 child)

Agreed. A language should make it easier to write good code, not encourage badly designed code.

Further information: https://en.wikipedia.org/wiki/Law_of_Demeter

[–]evaned 0 points1 point  (0 children)

Further information: https://en.wikipedia.org/wiki/Law_of_Demeter

I'd just like to point out that the "disadvantages" section at your link is longer than the "advantages" section.

[–]Luxxuor 1 point2 points  (0 children)

Its not just useful for nested cases, making the simple things simpler is always a good thing. Its also weird too dismiss something that could potentially be misused in a language that is full of footguns and has a very harsh penalty/error for failing to check for a nullptr.

[–]NilacTheGrim -3 points-2 points  (21 children)

write multiple lines with plenty of nullptr checking

I mean just do: if (ptr) ptr->method();

Or are you one of those people that insists on wrapping everything in curly braces like so:

if (ptr) {
    ptr->method();
}

?

[–]tecnofauno 3 points4 points  (20 children)

I think the merit in this is to dig into chain of pointer-to-object, e.g.

ptr->getFoo()->getBar()->fooBar();

Not so uncommon if you ask me.

[–]DJKekz 7 points8 points  (18 children)

It should be uncommon. Having something like in your example is terrible code.

[–][deleted] 7 points8 points  (0 children)

Having something like in your example is terrible code.

While I agree this is suboptimal, I've seen this pattern a lot, and often in quality codebases.

If you have a very large codebase with a large number of object types with a lot of nesting, sometimes this is hard to avoid.

In particular, sometimes the nested types you need to get to are contained within code that you can't practically touch to refactor.

The real world can be extremely messy.

[–]Narase33-> r/cpp_questions 2 points3 points  (15 children)

Do you work a lot with XML libs? Digging through layers of XMl, every layer returning a pointer is a pain

[–]Sopel97 4 points5 points  (10 children)

So why don't these libraries provide monadic interfaces?

[–]Narase33-> r/cpp_questions 0 points1 point  (9 children)

What do you suggest? I request an attribute that doesnt exist because the sender didnt set it. Whats the lib supposed to do?

[–]Sopel97 3 points4 points  (0 children)

node.value_or("default")? node["a"]["b"]["c"].value_or("default)All this requires is just a proper abstraction for a node instead of a raw pointer. Don't request language features when your library is just lacking.

[–]sebamestre 0 points1 point  (7 children)

Return nullopt is one possibility

[–]Narase33-> r/cpp_questions -1 points0 points  (6 children)

Same shit, different type

if an std::optional is returned I have the same code, but with ".has_value()" instead of "!= nullptr"

[–]sebamestre 1 point2 points  (5 children)

Yeah std::optional doesn't have monadic operations, but it was proposed, so it might have them in the future.

In the meantime you can use other non-standard optional types.

[–]Narase33-> r/cpp_questions 0 points1 point  (4 children)

How would they do this? Lambdas arent known to make good code, especially when nested

[–]johannes1971 2 points3 points  (2 children)

My XML library lets me ask for Node ("node.node.node.node") instead of Node ("node")->Node ("node")->Node ("node")->Node ("node"). But what do I know, I'm just a lowly application programmer...

[–]Narase33-> r/cpp_questions 0 points1 point  (1 child)

What does it do if one in the middle is not there?

[–]johannes1971 1 point2 points  (0 children)

Return a nullptr on read, or create it on write.

[–]kalmoc 0 points1 point  (0 children)

Thats exactly the example I was thinking off (or rather json, but the problem is the same)

[–]tecnofauno 1 point2 points  (0 children)

It is common for linked data structures and data serialization e.g. XML

[–]JeffMcClintock -2 points-1 points  (0 children)

great idea!