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;
}
}
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.
if (form.has_value() && form->person.has_value() && form->person->middle_name.has_value())
would beif (form && form->person && form->person->middle_name)
. A less-spamy*(*(*form).person).middle_name
would beform->person->middle_name
. – Ahneroptional
, 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 anoptional
that's empty and an optional that happens to contain a default-constructed value? So why useoptional
at all? – Centesimooptional<T>
whenT
is default constructible is to recognize a distinction betweenT{}
and not being present. Your code obliterates that distinction. – Centesimostd::optional
forstd::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 changeconst std::optional<std::string> middle_name;
toconst std::string middle_name;
and then useif (form && form->person && !form->person->middle_name.empty()) { */ use form->person->middle_name as needed */ }
– Hulsemiddle_name
. What is the difference betweenmiddle_name
which has a value of an empty string andmiddle_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 beoptional
. – Centesimoflatten_opt
wouldn't do that. At least, not your first one. – Centesimostd::optional
. – Tynishatynwaldaccess
function in Barry's answer. Here's how you can make it work forstd::optional
trivially. I think it's an exact duplicate, tackling the exact same issue. changingT*
tostd::optional<T>
orstd::unique_ptr<T>
or some other fancy pointer doesn't warrant a new question imo. – Lenhard