On Wed, Aug 18, 2021 at 10:36 AM Francesco Pretto via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
On Wed, 18 Aug 2021 at 14:01, Bo Persson via Std-Proposals
<std-proposals@lists.isocpp.org> wrote:
> The fact that we already have some pretty useless operators is a very
> weak argument for adding more special operators.
>

I think you misunderstood my words in the sense I consider at least
the safe navigation operator very valuable, and several language
designers clearly think the same because the list of languages
implementing it is growing. The off-topic question was if it was
formally proposed for C++ as well.

The problem with safe navigation in C++ is that we're not object-oriented enough to have nil and we're not functional enough to have language optional, so it seems like it would be difficult to specify.

For pointers:

struct C {
    int mem;
    int val();
    int& ref();
    int&& rref();
};
C* p = /* whatever */;
p?.mem; // this is the straightforward happy case, would be an int*
p?.val(); // this seems not possible?
p?.ref(); // this could be an int*?
p?.rref(); // this could be an int* but that's a bit weird and loses information?

This is an even harder question for class types, because how do you get this to work

optional<C> o = /* whatever */;
o?.mem;

operator->() kind of works this way now but we generally side-step the question of "what is actually the member" by returning a pointer. But here, we can't just return a C* since we need to know what the type of mem is so we can provide the correct return type. But also the compiler needs to know that it's looking in C for these members. o?.val() is totally reasonable here and could return optional<int>, how do we do that?

At least in the optional case, this sort of thing does come up, and the current solution is to use map (or transform, in C++23), but in these simple cases it's... pretty verbose:

o?.mem;
o.map([](C& c) { return c.mem; })

But also o?.mem should probably give you an Optional<int&>, so in order to really mean the same thing, the fair comparison is:

o?.mem;
o.map([](C& c) -> decltype(auto) { return (c.mem); })

And that's not just long but also very complicated. This only gets worse when you want to invoke a member function with some parameters.

I think the closest thing I've seen that would make this work (and I think it would actually completely make this work, simply by using the same idea for a different operator) is P0060, which would work here as

template <typename T>
class Optional {
    template <typename F, typename... Args>
    auto operator?.(F&& f, Args&&... args) {
        using U = std::invoke_result_t<F, T&, Args...>;
        return (*this)
            ? Optional<U>(std::in_place, std::invoke(FWD(f), **this, FWD(args)...))
            : Optional<U>();
    }
};

That is, o?.mem evaluates as o.operator?.(/* synthetic unary function that returns .name on its operand */) and o?.foo(1, 2) evaluates as o.operator?.(/* synthetic n-ary function that returns .foo(args...) on its operands */, 1, 2).

That paper never got a follow-up though.

Barry