Chain Optionals in C++
Asked Answered
E

6

9

How to avoid nested if statements with chained optionals in C++?

For example, if type A contains an std::optional<B> b and type B an std::optional<C> c, I would like to be able to write something like:

const auto v = if_exists(if_exists(a->b)->c);

And v would get the value from c or an empty optional if either b or c are empty optionals.

I think this would be nicer that nested ifs like this:

if (a->b) {
 const auto b = *(a->b);
 if (b->c) {
  const auto c = *(b->c);
 }
}

The following question seems to go in this direction but I am not sure how to adapt it to my use-case: Haskell style "Maybe" type & *chaining* in C++11

Evocative answered 21/2, 2018 at 12:7 Comment(5)
std::optional and c++11 doesn't sound right to me.Ringleader
But question You reference gives very good answer how to do it or am I getting Your question wrong?Platy
You could write it as if (a->b && a->b->c) { const auto c = *a->b->c; // do stuff with c }Quintan
Example from Martin Fowler: refactoring.com/catalog/…Ansell
open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0798r0.html so will be solved since C++23Girardi
P
4

You can do something like this (pseudocode-ish; link to buildable code is provided below):

// wrap std::optional for chaining
template <class T> class Maybe {
  std::optional<T> t;

  // ... constructors etc

  // Maybe chaining 
  // If A has a member named m of type M, 
  // then Maybe<A>.fetch(&A::m) returns a Maybe<M>

  template <class M>
  Maybe<M> fetch(M T::*mem_ptr) {
     return (bool(t)) ? Maybe<M>((*t).*mem_ptr) : Maybe<M>() ;
  }

  // Maybe chaining special case 
  // If A has a member named m, which is itself a Maybe<M>,
  // then return it without wrapping it in an additional Maybe

  template <class M>
  Maybe<M> fetch(Maybe<M> T::*mem_ptr) {
     return (bool(t)) ? ((*t).*mem_ptr) : Maybe<M>() ;
  }

};

Now if you have this:

 struct C { int d ; }
 struct B { C c; }
 struct A { B b; }
 A a;
 Maybe<A> ma;

and you can do this

 int d = a.b.c.d;

you cannot do the same with ma, but you can use the next best thing, namely:

 Maybe<int> md = ma.fetch(&A::b).fetch(&B::c).fetch(&C::d);

And you can still use this if you Maybe-ify any or all struct members above:

 struct C { Maybe<int> d ; }
 struct B { Maybe<C> c; }
 struct A { Maybe<B> b; }

Live example (not production quality but it builds).

Pyre answered 21/2, 2018 at 14:9 Comment(1)
This comes close to what I had in mind. At least it shows how to achieve the desired effect.Unfortunately, it requires to use Maybe instead of optional. Which is not usable when you are given types which already contains optional members, themselves containing further optional members. In that case using the improved optional from C++23 would be the best option if possible.Evocative
G
3

C++23 introduces and_then or or_else to address this inconvenience.

Here is some paper with proposal.

Before we can use C++23 you can try write some template which could resolve this.

My attempt:

namespace detail {
template <auto Field, class T>
struct field_from_opt;

template<typename T, typename FieldType, FieldType T::*ptr>
struct field_from_opt<ptr, T>
{
    static auto get(const std::optional<T>& x) -> std::optional<FieldType>
    {
        if (x) return (*x).*ptr;
        return {};
    }
};
}

template<auto Field, typename T>
auto if_exists(const std::optional<T>& x)
{
    return detail::field_from_opt<Field, T>::get(x);
}

https://godbolt.org/z/dscjYqrx1

Girardi answered 3/6, 2022 at 16:27 Comment(0)
B
2

You might use

template <typename T, typename F>
auto convert_optional(const std::optional<T>& o, F&& f)
-> std::optional<std::decay_t<decltype(std::invoke(std::forward<F>(f), *o))>>
{
    if (o)
        return std::invoke(std::forward<F>(f), *o);
    else
        return std::nullopt;
}

template <typename T, typename F>
auto convert_optional(std::optional<T>& o, F&& f)
-> std::optional<std::decay_t<decltype(std::invoke(std::forward<F>(f), *o))>>
{
    if (o)
        return std::invoke(std::forward<F>(f), *o);
    else
        return std::nullopt;
}

template <typename T, typename F>
auto convert_optional(std::optional<T>&& o, F&& f)
-> std::optional<std::decay_t<decltype(std::invoke(std::forward<F>(f), *std::move(o)))>>
{
    if (o)
        return std::invoke(std::forward<F>(f), *std::move(o));
    else
        return std::nullopt;
}

or

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

template <typename O, typename F>
auto convert_optional(O&& o, F&& f)
-> std::enable_if_t<
    is_optional<std::decay_t<O>>::value,
    std::optional<std::decay_t<decltype(std::invoke(std::forward<F>(f),
                                                    *std::forward<O>(o)))>>>
{
    if (o)
        return std::invoke(std::forward<F>(f), *o);
    else
        return std::nullopt;
}

and your example becomes:

auto c = convert_optional(convert_optional(a, &A::b).value_or(std::nullopt),
                          &B::c).value_or(std::nullopt);

convert_optional(a, &A::b) will return std::optional<std::optional<B>>

You might even simplify by additional function:

template <typename O, typename F>
auto convert_optional_fact(O&& o, F&& f)
-> decltype(convert_optional(std::forward<O>(o),
                             std::forward<F>(f)).value_or(std::nullopt))
{
    return convert_optional(std::forward<O>(o),
                            std::forward<F>(f)).value_or(std::nullopt);
}

and then

auto c = convert_optional_fact(convert_optional_fact(a, &A::b), &B::c);

Demo

Biography answered 21/2, 2018 at 21:15 Comment(0)
M
2

This can be achieved with a simple macro.

#define CHAIN(OPTIONAL, MEMBER)                                                 \
  ([](auto &&opt) {                                                             \
    return opt ? std::optional{opt->MEMBER} : std::nullopt;                     \
  }(OPTIONAL))

const auto v = CHAIN(CHAIN(a, b), c);
Melena answered 8/1, 2020 at 1:9 Comment(0)
E
1

Note that there is a proposal to allow this optional chaining in C++:

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0798r3.html

I like this part:

This is common in other programming languages. Here is a list of programming languages which have a optional-like type with a monadic interface or some similar syntactic sugar:

  • Java: Optional
  • Swift: Optional
  • Haskell: Maybe
  • Rust: Option
  • OCaml: option
  • Scala: Option
  • Agda: Maybe
  • Idris: Maybe
  • Kotlin: T?
  • StandardML: option
  • C#: Nullable

Here is a list of programming languages which have a optional-like type without a monadic interface or syntactic sugar:

  • C++

  • I couldn’t find any others

The proposal would allow for code like that:

std::optional<image> get_cute_cat (const image& img) {
    return crop_to_cat(img)
        .and_then(add_bow_tie)
        .and_then(make_eyes_sparkle)
        .transform(make_smaller)
        .transform(add_rainbow);
}
Evocative answered 3/6, 2022 at 15:4 Comment(0)
P
0

Using the code given in referenced question, You can do something like this

maybe_do(a->b, [](B &b){ 
    return maybe_do(b.c, [](C &c){ 
        //do what You want to do with the C optional
    })
});
Platy answered 21/2, 2018 at 12:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.