How to flatten the nested std::optional?

19

note: this question was briefly marked as a duplicate of this, but it is not an exact duplicate since I am asking about std::optionals specifically. Still a good question to read if you care about general case.

Assume I have nested optionals, something like this(dumb toy example):

struct Person{
    const std::string first_name;
    const std::optional<std::string> middle_name;
    const std::string last_name;
};
struct Form{
    std::optional<Person> person;
};

and this spammy function:

void PrintMiddleName(const std::optional<Form> form){
    if (form.has_value() && form->person.has_value() && form->person->middle_name.has_value()) {
        std::cout << *(*(*form).person).middle_name << std::endl; 
    } else {
        std::cout << "<none>"  << std::endl; 
    }
}

What would be the best way to flatten this optional check? I have made something like this, it is not variadic, but I do not care that much about that(I can add one more level(overload with membr3) if really necessary, and everything beyond that is terrible code anyway).

template<typename T, typename M>
auto flatten_opt(const std::optional<T> opt, M membr){
    if (opt.has_value() && (opt.value().*membr).has_value()){
        return std::optional{*((*opt).*membr)};
    }
    return decltype(std::optional{*((*opt).*membr)}){};
}

template<typename T, typename M1, typename M2>
auto ret_val_helper(){
    // better code would use declval here since T might not be 
    // default constructible.
    T t;
    M1 m1;
    M2 m2;
    return ((t.*m1).value().*m2).value();
}

template<typename T, typename M1, typename M2>
std::optional<decltype(ret_val_helper<T, M1, M2>())> flatten_opt(const std::optional<T> opt, M1 membr1, M2 membr2){
    if (opt.has_value() && (opt.value().*membr1).has_value()){
        const auto& deref1 = *((*opt).*membr1);
        if ((deref1.*membr2).has_value()) {
            return std::optional{*(deref1.*membr2)};
        }
    }
    return {};
}

void PrintMiddleName2(const std::optional<Form> form){
    auto flat  = flatten_opt(form, &Form::person, &Person::middle_name);
    if (flat) {
        std::cout << *flat;
    }
    else {
        std::cout << "<none>"  << std::endl; 
    }
}

godbolt

notes:

  • I do not want to switch away from std::optional to some better optional.
  • I do not care that much about perf, unless I return a pointer I must make copy(unless arg is temporary) since std::optional does not support references.
  • I do not care about flatten_has_value function(although it is useful), since if there is a way to nicely flatten the nested optionals there is also a way to write that function.
  • I know my code looks like it works, but it is quite ugly, so I am wondering if there is a nicer solution.
Share
Improve this question
17
  • 12
    A less-spamy if (form.has_value() && form->person.has_value() && form->person->middle_name.has_value()) would be if (form && form->person && form->person->middle_name). A less-spamy *(*(*form).person).middle_name would be form->person->middle_name. – Drew Dormann Apr 2 at 17:15
  • It's a bit confusing to me that you want to use optional, but you're OK with getting a default-constructed value from it if it is empty. Wouldn't that mean that you would be unable to distinguish between an optional that's empty and an optional that happens to contain a default-constructed value? So why use optional at all? – Nicol Bolas Apr 2 at 17:16
  • 2
    Using std::optional for std::string rarely makes sense. Certainly in this case, as there is no need to differentiate between a missing middle name vs a blank middle name. So I would change const std::optional<std::string> middle_name; to const std::string middle_name; and then use if (form && form->person && !form->person->middle_name.empty()) { */ use form->person->middle_name as needed */ } – Remy Lebeau Apr 2 at 17:28
  • 2
    @NoSenseEtAl: Consider middle_name. What is the difference between middle_name which has a value of an empty string and middle_name which has no value? If the answer is "there is never a difference", if none of your code ever treats these two cases as different, then it shouldn't be optional. – Nicol Bolas Apr 2 at 17:29
  • 1
    @RemyLebeau it is a toy example, real code could have been a complex struct/class and calling code could notice difference between default constructed and nullopt. – NoSenseEtAl Apr 2 at 17:30

Comments

Popular posts from this blog

Meaning of `{}` for return expression

Get current scroll position of ScrollView in React Native

React Native - Image Cache