einpoklum
einpoklum

Reputation: 131385

C++ equivalent of Javascript's '?.' operator for optional-chaining?

In Javascript, if I have a potentially null object obj, which, if not null, will have a field x - I can write obj?.x. This is called "optional chaining" or "safe navigation": If obj is null, or otherwise not an object with accessible fields - it won't throw, just produce null. (It also won't throw if obj is an object but doesn't have an x field, although that's less relevant for this question).

Now let us turn to C++, which since C++17 has standard-library std::optional<T>'s. Suppose we have:

struct Foo { Bar x; }

and I have an optional<Foo> object named obj. Naively, I might write the expression:

obj ? obj->x : optional<Bar>{}

... but this won't work, since the types of the two result expression is different, and the trinary operator doesn't harmonize them. Godbolt confirms, with this code:

#include <optional>

using Bar = int;
struct Foo { Bar x; };

std::optional<Bar> f()
{
    std::optional<Foo> obj {};
    return obj ? obj->x : std::nullopt;
}

So, what short idiom is there for expressing obj?.x in C++?


Note: Any language standard version is fine, but the older the better obviously.

Upvotes: 8

Views: 1657

Answers (6)

Ahmed AEK
Ahmed AEK

Reputation: 17436

C++23 introduced Monadic operations for optional, so you can use std::optional::transform

#include <optional>

using Bar = int;
struct Foo { Bar x; };

std::optional<Bar> f()
{
    std::optional<Foo> obj {};
    return obj.transform([](auto&& o) { return o.x;});
}

for lower C++ versions you could use C++11 optional library which has a similar operation (map), or write your own version of transform (not recommended).

godbolt demo

I don't recommend you write you own transform because to make it chainable you need to inherit from std::optional, and it is likely UB to inherit from the standard library objects.


A shorter version would define the lambda as a struct externally, if you are using it everywhere and need to reduce the number of letters per use. (with the exact same assembly)

template <typename Member>
struct grab
{
    Member member;
    auto operator()(auto& obj) const 
    { return obj.*member; }
};

std::optional<Bar> f()
{
    std::optional<Foo> obj {};
    return obj.transform(grab{&Foo::x});
}

Upvotes: 11

MarkB
MarkB

Reputation: 1988

Here's a variation that works in c++17 (if you delete the concepts) and supports chaining. I opted for return by value. One could incorporate perfect forwarding if performance is a concern.

#include <cassert>
#include <optional>
#include <concepts>

template <typename T>
struct is_optional : std::false_type {};

template <typename T>
struct is_optional<std::optional<T>> : std::true_type {};

template <typename T>
inline constexpr bool is_optional_v = is_optional<T>::value;

template <typename T>
concept IsOptional = is_optional_v<T>;

template<typename T, class O>
IsOptional auto operator |(const std::optional<T>& obj, O const T::*member)
{
    if constexpr (is_optional_v<O>) {
        return obj ? (*obj).*member : std::nullopt;
    }
    else {
        return obj ? std::optional<O>{(*obj).*member} : std::nullopt;
    }
}

template<typename T, class O>
IsOptional auto operator |(const T& obj, O const T::*member)
{
    if constexpr (is_optional_v<O>) {
        return obj.*member;
    }
    else {
        return std::optional<O>{obj.*member};
    }
}


int main()
{
    using Bar = int;
    struct Foo {
        Bar x;
    };

    Foo foo{123};
    std::optional<Foo> foo2{Foo{123}};
    std::optional<Foo> foo3{};

    assert((foo | &Foo::x) == 123);
    assert((foo2 | &Foo::x) == 123);
    assert((foo3 | &Foo::x) == std::nullopt);
}

void chain()
{
    struct B {
        int x;
    };

    struct A {
        std::optional<B> b;
    };

    std::optional<A> val1;
    std::optional<A> val2 = A{};
    std::optional<A> val3 = A{B{123}};
    A val4 = A{B{123}};
    auto x1 = val1 | &A::b | &B::x;
    assert(!x1);
    auto x2 = val2 | &A::b | &B::x;
    assert(!x2);
    auto x3 = val3 | &A::b | &B::x;
    assert(*x3 == 123);
    auto x4 = (val4 | &A::b) | &B::x;
    assert(*x4 == 123);
}

Upvotes: 2

Eljay
Eljay

Reputation: 5321

(C++23) With a little operator| overloading to do piping, and taking a page out of the monad playbook, could do something like...

#include <iostream>
#include <optional>
#include <type_traits>
#include <utility>

using Bar = int;

struct Foo { Bar x; };

struct FFoo { std::optional<Bar> x{}; };

inline auto f(std::optional<Foo> obj) -> std::optional<Bar> {
    return obj ? obj->x : std::nullopt;
}

inline auto ff(std::optional<FFoo> obj) -> std::optional<Bar> {
    return obj ? obj->x : std::nullopt;
}

inline void println(std::optional<Bar> obj) {
    if (!obj) {
        std::cout << "(empty)\n";
    } else {
        std::cout << *obj << "\n";
    }
}

// C++23 magic to use operator| to pipe calls.
template <typename T, typename FN> requires (std::invocable<FN, T>)
constexpr auto operator|(T&& t, FN&& f) -> typename std::invoke_result_t<FN, T> {
    return std::invoke(std::forward<FN>(f), std::forward<T>(t));
}

int main() {
    std::optional<FFoo> no_foo;
    std::optional<FFoo> foo_no_bar{ Foo{} };
    std::optional<FFoo> foo_bar{ Foo{ 10 } };
    std::optional<Foo> original_foo{ Foo{ 20 } };
    std::optional<Foo> original_no_foo{};


    no_foo | ff | println;
    foo_no_bar | ff | println;
    foo_bar | ff | println;
    original_foo | f | println;
    original_no_foo | f | println;
}

Update: renamed my modifed Foo to FFoo, f to ff, added the original Foo and original f back in, and added original_foo and original_no_foo objects to demonstrate use.

Upvotes: 1

Red.Wave
Red.Wave

Reputation: 4183

Depending on use case you can use either of 4 monadic functions:

  1. Get the intact value, if exists or provide a default value: optional::value_or. This method expects a default value.
std::optional<Foo> optfoo;
Foo foo = foo.value_or(Foo{});
  1. Replace current value, in case it exists or return nullopt with optional::transform:
std::optional<Foo> optfoo;
std::optional<Bar> optbar = foo.transform(&Foo::x);

This method can change the result type from optional<foo> to optional<bar>.

  1. Convert current value to an optional -which may be null- via optional::and_then.
std::optional<Foo> optfoo;
std::optional<Bar> optbar = foo.and_then([](Foo& foo){
    return std::optional{foo.x};
});

The difference from transform is that the lambda returns an optional.

  1. Replace the value with an optional of same type, if it is null with optional::or_else:
std::optional<Foo> optfoo;
std::optional<Foo> optfoo2 = foo.or_else([]{
    return std::optional{Foo{}};
});

This method keeps the type of optional<foo>, but tries to replace the value(like value_or).

Option 1 is the only C++17 solution and the only one that doesn't take a lambda/function as its input, and the only one that doesn't necessarily return an optional.Other options need C++23, return optional values and need function inputs.

Upvotes: 1

einpoklum
einpoklum

Reputation: 131385

A hacky, but arguably more fun, tweak of Mooing Duck's answer would let us use:

map(obj, x)

instead of Javascript's obj?.x.

Yes, it's this simple because macros are involved >:-)

#include <optional>
#include <type_traits>

using Bar = int;
struct Foo { Bar x; };

template<class I, class O>
std::optional<O> map_impl(const std::optional<I>& val, O const I::*member)
{ return val ? std::optional<O>{*val.*member} : std::nullopt; }

#define map(obj_, field_) map_impl(obj_, &std::remove_reference_t<decltype(*obj_)> :: field_ )

Notes:

  • See this working on GodBolt.
  • Would need some adaptation to also work for methods rather than just data fields, but I'm pretty sure some TMP can make that happen.
  • We would probably not use this name, to avoid clashing with std::map and other uses of the word map - a macro is much more "syntactically-imperialistic". So maybe something like qdot.
  • If we apply @milleniumbug's infix operator generator, we might even be able to make it into a proper infix operator, which we would get us close to nirvana with obj qdot x.

Upvotes: 3

Mooing Duck
Mooing Duck

Reputation: 66912

I was surprised to discover C++ option lacked a map method, but after a few minutes thought, it seems easy to emulate.

template<class I, class O>
std::optional<O> map(const std::optional<I>& val, O const I::*member)
{ return val ? std::optional<O>{*val.*member} : std::nullopt; }

template<class I, class O, class...Args>
std::optional<O> map(const std::optional<I>& val, O (I::*method)() const, Args&&...args)
{ return val ? std::optional<O>{(*val.*method)(std::forward<Args>(args)...)} : std::nullopt; }

And then usage is terse:

map(obj, &Foo::x);
map(obj, &Foo::getX); //if you need to pass parameters to this, you can

This only works on members and methods, and not arbitrary function calls, but arbitrary functinon calls would require lambdas, which are annoyingly verbose in C++.

Upvotes: 5

Related Questions