I would like an elegant way to safely read data in a field which is wrapped in "nullable types" such as std::optional and std::shared_ptr. Take as example:
#include <iostream>
#include <memory>
#include <optional>
struct Entry
{
std::optional<std::string> name;
};
struct Container
{
std::optional<std::shared_ptr<Entry>> entry;
};
int main()
{
Entry entry{"name"};
Container container{std::make_shared<Entry>(entry)};
// ...
return 0;
}
To read the "name" field from Entry given a Container, I could write:
std::cout << *((*container.entry)->name) << std::endl;
But I don't find this particularly easy to read or write. And since the optionals and shared pointers may not be set, I can't anyway.
I want to avoid code like this:
if (container.entry)
{
const auto ptr = *container.entry;
if (ptr != nullptr)
{
const auto opt = ptr->name;
if (opt)
{
const std::string name = *opt;
std::cout << name << std::endl;
}
}
}
And I am looking for something more like this:
const auto entry = recursive_dereference(container.entry);
const auto name = recursive_dereference(entry.name);
std::cout << name.value_or("empty") << std::endl;
This would be based on this recursive_dereference implementation.
The trouble is, it would crash if an optional or shared_ptr is not set. Is there a way to modify recursive_dereference
so that it returns its result in an optional which is left empty when a field along the way is unset?
I think we could use std::enable_if_t<std::is_constructible<bool, T>::value
to check if the field can be used as a bool in an if
(which would be the case for optionals and shared pointers) which would allow us to check if they are set. If they are set we can continue the dereferencing recursion. If one is not set, we can interrupt the recursion and return an empty optional of the final type.
Unfortunately, I couldn't formulate this into working code. The solution should at best be limited to "C++14 with optionals".
Update:
First a remark. I realized that using std::is_constructible<bool, T>
is unnecessary. recursive_dereference
checks if a type can be dereferenced and when it can be then we can check if it is set with if (value)
. At least it would work with optionals and shared pointers.
An alternative I found is first separately checking if it is safe to dereference the value and then call recursive_dereference
unmodified.
So we can do:
if (is_safe(container.entry)) {
const auto entry = recursive_dereference(container.entry);
// use entry
}
Implementation of is_safe
:
template<typename T>
bool is_safe(T&& /*t*/, std::false_type /*can_deref*/)
{
return true;
}
// Forward declaration
template<typename T>
bool is_safe(T&& t);
template<typename T>
bool is_safe(T&& t, std::true_type /*can_deref*/)
{
if (t)
{
return is_safe(*std::forward<T>(t));
}
return false;
}
template<typename T>
bool is_safe(T&& t)
{
return is_safe(std::forward<T>(t), can_dereference<T>{});
}
I'm still open for a better solution that would avoid checking and deferencing separately. So that we get a value or "empty" in one pass.
Update 2
I managed to get a version that does not need a separate check. We have to explicitly give the final type that we expect as template parameter though. It returns an optional with the value or an empty optional if one reference along the way is not set.
template <typename FT, typename T>
auto deref(T&& t, std::false_type) -> std::optional<FT>
{
return std::forward<T>(t);
}
template <typename FT, typename T>
auto deref(T&& t) -> std::optional<FT>;
template <typename FT, typename T>
auto deref(T&& t, std::true_type) -> std::optional<FT>
{
if (t)
{
return deref<FT>(*std::forward<T>(t));
}
return std::nullopt;
}
template <typename FT, typename T>
auto deref(T&& t) -> std::optional<FT>
{
return deref<FT>(std::forward<T>(t), can_dereference<T>{});
}
Usage:
std::cout << deref<Entry>(container.entry).has_value() << std::endl;
std::cout << deref<Entry>(emptyContainer.entry).has_value() << std::endl;
Output:
1
0