12

In Modern C++, is there a way to do safe navigation?

For example, instead of doing...

if (p && p->q && p->q->r)
    p->q->r->DoSomething();

...having a succinct syntax by using some sort of short-circuiting smart pointer, or some other kind of syntax leveraging operator overloading, or something in the Standard C++ Library, or in Boost.

p?->q?->r?->DoSomething(); // C++ pseudo-code.

Context is C++17 in particular.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
Eljay
  • 4,648
  • 3
  • 16
  • 27
  • 6
    Not as far as i know, and hopefully it will not appear. This style of coding a->b->c->d leads to big problems, tight coupling, and inability to separate pieces of code from each other. – Bogolt Jul 17 '17 at 17:06
  • What should the safe pointer do in which case? –  Jul 17 '17 at 17:06
  • 2
    You could overload `operator->` but the question is what it should return. – Hatted Rooster Jul 17 '17 at 17:11
  • 1
    @Bogolt - I agree with the "big problems" aspect on all points mentioned. Unfortunately, my situation is working with a very large and very old code base that uses the first example as a pattern pervasively. Except in the places that were missed. I have first hand experience of the big problems. Given time, the code base should become better encapsulated and employ the "tell, don't ask" principle. But that is in the future. – Eljay Jul 17 '17 at 17:11
  • 1
    @manni66 - the "safe navigation operator" should short-circuit and be a no-op. – Eljay Jul 17 '17 at 17:12
  • With the optional monad and `boost::make_optional` shortcut to `b::mo`, you could probably code this as `b::mo(p, p) >>= [](auto *p) { return b::mo(p->q, p->q) >>= [](auto *q) { return b::mo(q->r, q->r) >>= [](auto *r) { return r->DoSomething(), b::none(); }; }; }` – Johannes Schaub - litb Jul 18 '17 at 10:42
  • I must say, that I'm not sure whether that's any better. With macros, you could maybe shorten it to `if(auto o = OPT(p, p->q, q->r)) (*o)->DoSomething(); ` ? Where the `MODO` would generate that nested lambda lattice and insert the macro arguments into the second and third arguments of `b::mo` respectively? – Johannes Schaub - litb Jul 18 '17 at 10:49
  • @JohannesSchaub-litb - The boost::optional is an interesting and cool facility. Thanks for bringing it to my attention! A small page out of the OCaml or Haskell playbooks: monads (or a C++ facsimile thereof)! It would fast-fail, much like Richard Hodges' answer. But the callsite would be as cluttered as Barry's answer... I concur with what you said "I'm not sure whether that's any better." – Eljay Jul 18 '17 at 12:53
  • A coworker said "So the problem is getting a segmentation fault. How about a smart pointer that throws an exception that you could handle gracefully. You could even put a breakpoint in it if it is in a nullptr state and being dereferenced, and have guaranteed nullptr initialization. The exception would short-circuit the rest of the pointer chain." Well it isn't a safe navigation operator, but may very well be a reasonable solution... perhaps I was blinded by CoffeeScript / Groovy / C# envy. – Eljay Jul 18 '17 at 13:06
  • The C++ standard library smart pointers, and the boost smart pointers don't throw. I'll just have to roll my own as a one-off. q.v. [Why doesn't std::shared_ptr dereference throw a null pointer exception (or similar)?](https://stackoverflow.com/questions/34409299/why-doesnt-stdshared-ptr-dereference-throw-a-null-pointer-exception-or-simil) – Eljay Jul 18 '17 at 13:12

3 Answers3

9

The best you can do is collapse all the member accesses into one function. This assumes without checking that everything is a pointer:

template <class C, class PM, class... PMs>
auto access(C* c, PM pm, PMs... pms) {
    if constexpr(sizeof...(pms) == 0) {
        return c ? std::invoke(pm, c) : nullptr;
    } else {
        return c ? access(std::invoke(pm, c), pms...) : nullptr;
    }
}

Which lets you write:

if (auto r = access(p, &P::q, &Q::r); r) {
    r->doSomething();
}

That's ok. Alternatively, you could go a little wild with operator overloading and produce something like:

template <class T>
struct wrap {
    wrap(T* t) : t(t) { }
    T* t;

    template <class PM>
    auto operator->*(PM pm) {
        return ::wrap{t ? std::invoke(pm, t) : nullptr};
    }

    explicit operator bool() const { return t; }
    T* operator->() { return t; }
};

which lets you write:

if (auto r = wrap{p}->*&P::q->*&Q::r; r) {
    r->doSomething();
}

That's also ok. There's unfortunately no ->? or .? like operator, so we kind of have to work around the edges.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 18
    :)) While absolutely correct, `auto r = wrap{p}->*&P::q->*&Q::r; r` is about as legible as an ancient manuscript in a long-forgotten tongue, buried under thousands of years of dust, having been feasted on by two hundred generations of maggots - in a locked crypt that no-one has yet discovered. – Richard Hodges Jul 18 '17 at 06:49
  • I took your comment to heart, and replaced my answer with a better one - for comedy effect if nothing else :) – Richard Hodges Jul 18 '17 at 06:50
  • Would an overload `access(C &, PM pm. PMs... pms)` allow it to chain references? (and similar for value returning members?) – Caleth Jul 18 '17 at 08:48
  • That's cool. Does it allow calling member functions, with `->*std::bind(&P::f, _1, args)` ? – Johannes Schaub - litb Jul 18 '17 at 11:00
  • @Barry - that's very cool template metaprogramming! And I learned something! I slightly prefer the 1st "access" approach you posted over the 2nd "wrap" approach (for the humorous reason Richard Hodges mentioned), but I grok that the 2nd may be arguably better C++ since the relationship is explicit. But the callsite use case is not any better than the "by hand" C++ to check for a valid pointer chain. – Eljay Jul 18 '17 at 12:39
  • 2
    I marking Barry's answer as the accepted answer because of this part "There's unfortunately no ->? or .? like operator..." Thanks everyone for thinking about my question! If someone comes up with a way to better mimic a safe navigation operator in C++, that'd be awesome. – Eljay Jul 18 '17 at 13:17
  • @Eljay Yeah frankly what you had originally is what I would actually use in production code. – Barry Jul 18 '17 at 13:36
6

"With a little Boilerplate..."

We can get to this:

p >> q >> r >> doSomething();

Here's the boilerplate...

#include <iostream>

struct R {
    void doSomething()
    {
        std::cout << "something\n";
    }
};

struct Q {
    R* r;
};

struct P {
    Q* q;
};

struct get_r {};
constexpr auto r = get_r{};

struct get_q {};
constexpr auto q = get_q{};

struct do_something {
    constexpr auto operator()() const {
        return *this;
    }
};
constexpr auto doSomething = do_something {};

auto operator >> (P* p, get_q) -> Q* {
    if (p) return p->q;
    else return nullptr;
}

auto operator >> (Q* q, get_r) -> R* {
    if (q) return q->r;
    else return nullptr;
}

auto operator >> (R* r, do_something)
{
    if (r) r->doSomething();
}

void foo(P* p)
{
//if (p && p->q && p->q->r)
//    p->q->r->DoSomething();
    p >> q >> r >> doSomething();
}

The resulting assembly is very acceptable. The journey to this point may not be...

foo(P*):
        test    rdi, rdi
        je      .L21
        mov     rax, QWORD PTR [rdi]
        test    rax, rax
        je      .L21
        cmp     QWORD PTR [rax], 0
        je      .L21
        mov     edx, 10
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:std::cout
        jmp     std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
.L21:
        ret
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 1
    Doesn't short-circuit, but it does fast-fail. The boilerplate could be stuffed into a macro (albeit I'm not a fan of macros). I like how it is used by the caller at the callsite -- very clean, even though it wouldn't "idiomatic C++" for pointer dereferencing. – Eljay Jul 18 '17 at 12:10
  • 2
    @Eljay The optimiser does the short-circuiting for us, so that's not a problem. You'll probably want a code generator to do it for an entire project. You could build that quite quickly with a tool like `yacc`. https://en.wikipedia.org/wiki/Yacc – Richard Hodges Jul 18 '17 at 13:10
2

although it's 2022 now, there's still no language level support for this. a very close simulation I can figure out:

template <typename T, typename F>
auto operator->*(T&& t, F&& f) {
  return f(std::forward<T>(t));
}

#define pcall(fn)                                                            \
  [&](auto&& __p) {                                                          \
    if constexpr (std::is_pointer_v<std::remove_reference_t<decltype(__p)>>) \
      return __p ? __p->fn : decltype(__p->fn){};                            \
    else                                                                     \
      return __p ? __p.fn : decltype(__p.fn){};                              \
  }

struct A {
  int v;
  int foo(int a, int b) { return a + b + v; }
};
struct B {
  A* getA() { return &a; }
  A a{100};
};
struct C {
  B* getB() { return &b; }
  B b;
  operator bool() { return true; }
};

int main(){
    int v = 3;

    C by_val;
    int t1 = by_val->*pcall(getB())->*pcall(getA())->*pcall(foo(1, v));

    C* by_ptr = &by_val;
    int t2 = by_ptr->*pcall(getB())->*pcall(getA())->*pcall(foo(1, v));
}

https://godbolt.org/z/zPhzTTv9s

crazybie
  • 101
  • 8
  • Nice, I like employing the `->*` overload. (In toy programs (not real programs!), I've (ab)used that to facilitate C#-like extension methods.) – Eljay Sep 20 '22 at 17:27