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:
std::optional
to some better optional.std::optional
does not support references.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.The operation you're looking for is called the monadic bind operation, and is sometimes spelled and_then
(as it is in P0798 and Rust).
You're taking an optional<T>
and a function T -> optional<U>
and want to get back an optional<U>
. In this case the function is a pointer to data member, but it really does behave as a function in this sense. &Form::person
takes a Form
and gives back an optional<Person>
.
You should write this in a way that is agnostic to the kind of function. The fact that it's specifically a pointer to member data isn't really important here, and maybe tomorrow you'll want a pointer to member function or even a free function. So that's:
template <typename T,
typename F,
typename R = std::remove_cvref_t<std::invoke_result_t<F, T>>,
typename U = mp_first<R>>
requires SpecializationOf<R, std::optional>
constexpr auto and_then(optional<T> o, F f) -> optional<U>
{
if (o) {
return std::invoke(f, *o);
} else {
return std::nullopt;
}
}
This is one of the many kinds of function declarations that are just miserable to write in C++, even with concepts. I'll leave it as an exercise to properly add references into there. I choose to specifically write it as -> optional<U>
rather than -> R
because I think it's important for readability that you can see that it does, in fact, return some kind of optional
.
Now, the question is how do we chain this to multiple functions. Haskell uses >>=
for monadic bind, but in C++ that has the wrong association (o >>= f >>= g
would evaluate f >>= g
first and require parentheses). So the next closest chose of operator would be >>
(which means something different in Haskell, but we're not Haskell, so it's okay). Or you could implement this borrowing the |
model that Ranges does.
So we'd either end up syntactically with:
auto flat = form >> &Form::person >> &Person::middle_name;
or
auto flat = form | and_then(&Form::person)
| and_then(&Person::middle_name);
A different way to compose multiple monadic binds together is an operation that Haskell spells >=>
, which is called Kleisli composition. In this case, it takes a function T -> optional<U>
and a function U -> optional<V>
and produces a function T -> optional<V>
. This is something that is exceedingly annoying to write constraints for so I'm just going to skip it, and it would look something like this (using the Haskell operator spelling):
template <typename F, typename G>
constexpr auto operator>=>(F f, G g) {
return [=]<typename T>(T t){
using R1 = std::remove_cvref_t<std::invoke_result_t<F, T>>;
static_assert(SpecializationOf<R1, std::optional>);
using R2 = std:remove_cvref_t<std::invoke_result_t<G, mp_first<R1>>>;
static_assert(SpecializationOf<R2, std::optional>);
if (auto o = std::invoke(f, t)) {
return std::invoke(g, *o);
} else {
// can't return nullopt here, have to specify the type
return R2();
}
};
}
And then you could write (or at least you could if >=>
were an operator you could use):
auto flat = form | and_then(&Form::person >=> &Person::middle_name);
Because the result of >=>
is now a function that takes a Form
and returns an optional<string>
.
Let's look at what the optimal form of a flatten function would look like. By "optimal" in this case, I mean the smallest presentation.
Even in the optimal case, at the point of performing the flatten operation, you would need to provide:
optional<T>
object to flatten.Your code is very close to optimal. The only issue is that each name in the "list of names" must contain the typename of the member you're accessing at that level, which is something that hypothetically could be computed using knowledge of T
.
C++ has no mechanism to do any better than this. If you want to access a member of an object, you must provide the type of that object. If you want to indirectly do this, C++ allows member pointers, but getting such a pointer requires knowing the type of the object at the point when the member is extracted. Even offsetof
gymnastics would require using the type name when you're getting the offset.
Reflection would allow for something better, as you could pass compile-time strings that static reflection could use to fetch member pointers from the type currently in use. But C++20 has no such feature.
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