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.
C# 6.0 includes a new null-conditional operator (sometimes called the Safe Navigation Operator): In previous versions null checking for any kind of objects manually we need to write if condition then we need to write our required properties or else we need to write our business logic.
Use the safe navigation operator ( ?. ) to replace explicit, sequential checks for null references. This operator short-circuits expressions that attempt to operate on a null value and returns null instead of throwing a NullPointerException.
In object-oriented programming, the safe navigation operator (also known as optional chaining operator, safe call operator, null-conditional operator, null-propagation operator) is a binary operator that returns null if its first argument is null; otherwise it performs a dereferencing operation as specified by the ...
Safe navigation operator¶ ↑ &. , called “safe navigation operator”, allows to skip method call when receiver is nil . It returns nil and doesn't evaluate method's arguments if the call is skipped.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With