简体   繁体   中英

C++ Safe Recursive Dereference

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

There are two solutions I can recommend you:

  1. if_valid(value, thenLambda, elseLambda) construct:
#include <iostream>
#include <memory>
#include <optional>
 
struct Entry
{
    std::optional<std::string> name;
};
 
struct Container
{
    std::optional<std::shared_ptr<Entry>> entry;
};
 
template<typename V, typename Then, typename Else>
auto if_valid(const V& v, Then then, Else els)
{
    return then(v);
}
 
template<typename V, typename Then, typename Else>
auto if_valid(const std::optional<V>& iv, Then then, Else els)
{
    if (iv) {
        return if_valid(*iv, std::move(then), std::move(els));
    } else {
        return els();
    }
}
 
template<typename V, typename Then, typename Else>
auto if_valid(const std::shared_ptr<V>& iv, Then then, Else els)
{
    if (iv) {
        return if_valid(*iv, std::move(then), std::move(els));
    } else {
        return els();
    }
}
 
int main()
{
    Entry entry{"name"};
    Container container{std::make_shared<Entry>(entry)};
 
    std::cout
        << if_valid(
            container.entry,
            /* then */ [&](auto&& entry1) { return entry1.name;                  },
            /* else */ [] ()              { return std::optional<std::string>(); }
        ).value_or("empty") << std::endl;

    return 0;
}

  1. A generic resolver with then and else path: (this has the benefit that you might simply have .name as a resolver as well as operator* )
#include <iostream>
#include <memory>
#include <optional>
#include <tuple>
#include <type_traits>

struct Entry
{
    std::optional<std::string> name;
};
 
struct Container
{
    std::optional<std::shared_ptr<Entry>> entry;
};

struct resolve_shared_ptr
{
    template<typename T, typename Then, typename Else>
    auto operator()(const std::shared_ptr<T>& t, Then then, Else els) const
    {
        if (t) {
            then(*t);
        } else {
            els();
        }
    }
};

struct resolve_optional
{
    template<typename T, typename Then, typename Else>
    auto operator()(const std::optional<T>& t, Then then, Else els) const
    {
        if (t) {
            then(*t);
        } else {
            els();
        }
    };
};

static_assert(std::is_invocable_v<
    resolve_optional,
    const std::optional<std::string>&,
    decltype([](const auto&) {}),
    decltype([]() {})
>);

template<typename T, typename Then, typename Else, size_t r, typename... Resolvers>
void resolve_r(const T& t, Then then, Else els, std::integral_constant<size_t, r>, const std::tuple<Resolvers...>& resolvers)
{
    if constexpr(r < sizeof...(Resolvers)) {
        if constexpr (std::is_invocable_v<decltype(std::get<r>(resolvers)), const T&, decltype([](auto&&) {}), Else>) {
            std::get<r>(resolvers)(
                t,
                /* then */ [&](const auto& next_t) { resolve(next_t, then, els, resolvers); },
                els
            );
        } else {
            resolve_r(t, then, els, std::integral_constant<size_t, r + 1>(), resolvers);
            //return resolve_r(t, then, els, r + 1, resolvers);
        }
    } else {
        then(t);
    }
}

template<typename T, typename Then, typename Else, typename... Resolvers>
void resolve(const T& t, Then then, Else els, const std::tuple<Resolvers...>& resolvers)
{
    resolve_r(t, then, els, std::integral_constant<size_t, 0>(), resolvers);
}

 
int main()
{
    Entry entry{"name"};
    Container container{std::make_shared<Entry>(entry)};
 
    resolve(
        container.entry,
        /* then */ [](const auto& res) { std::cout << res;     },
        /* else */ []()                { std::cout << "empty"; },
        std::make_tuple(
            resolve_optional(),
            resolve_shared_ptr(),
            [](const Entry& entry1, auto then, auto els) { then(entry1.name); }
        )
    );
    std::cout << std::endl;
    
    return 0;
}

Combining recursive_dereference and convert_optional_fact , I end-up with this:

#include <functional>
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>

// can_dereference

template <typename T>
struct can_dereference_helper
{
    template <typename U, typename = decltype(*std::declval<U>())>
    static std::true_type test(U);
    template <typename...U>
    static std::false_type test(U...);
    using type = decltype(test(std::declval<T>()));
};

template <typename T>
struct can_dereference : can_dereference_helper<typename std::decay<T>::type>::type {};

// deref

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>{});
}

// get_field

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);
    } 
    return std::nullopt;
}

template <typename O, typename F>
auto get_field(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);
}

// Test data

struct Entry
{
    std::optional<std::string> name;
};

struct Container
{
    std::optional<std::shared_ptr<Entry>> entry;
};

// main

int main()
{
    Container emptyContainer{};
    
    Entry entry{"name"};
    Container container{std::make_shared<Entry>(entry)};

    std::cout << deref<Entry>(container.entry).has_value() << std::endl;
    std::cout << deref<Entry>(emptyContainer.entry).has_value() << std::endl;

    const auto name = get_field(deref<Entry>(container.entry), &Entry::name);
    std::cout << name.value_or("empty") << std::endl;

    const auto emptyName = get_field(deref<Entry>(emptyContainer.entry), &Entry::name);
    std::cout << emptyName.value_or("empty") << std::endl;

    return 0;
}

Output:

1
0
name
empty

Play with it in Online GDB .

With this, we can get from the container to the field in one line:

get_field(deref<Entry>(container.entry), &Entry::name)

We get an optional with the string for "name" or and empty optional if something is not set.

Still open:

  • std::invoke is C++17 and I need C++14 (except for std::optional which is allowed)
  • It would be nice if we could deduce the final type automatically in deref so that we don't have to specify Entry in deref<Entry> in the line above.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM