简体   繁体   English

C++ 安全递归解引用

[英]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.我想要一种优雅的方式来安全地读取包含在“可空类型”(例如 std::optional 和 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.这将基于此recursive_dereference实现。

The trouble is, it would crash if an optional or shared_ptr is not set.问题是,如果未设置可选或 shared_ptr,它会崩溃。 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?有没有办法修改recursive_dereference以便它返回一个可选的结果,当沿途的一个字段未设置时该可选的结果留空?

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.我认为我们可以使用std::enable_if_t<std::is_constructible<bool, T>::value来检查该字段是否可以用作if中的 bool(可选和共享指针就是这种情况)这将让我们检查它们是否已设置。 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".该解决方案最多应限于“C++14 with optionals”。

Update:更新:

First a remark.先说一句。 I realized that using std::is_constructible<bool, T> is unnecessary.我意识到使用std::is_constructible<bool, T>是不必要的。 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) . recursive_dereference检查一个类型是否可以被取消引用以及何时可以然后我们可以检查它是否被设置为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.我发现的另一种方法是首先单独检查取消引用该值是否安全,然后调用未修改的recursive_dereference

So we can do:所以我们可以这样做:

    if (is_safe(container.entry)) {
        const auto entry = recursive_dereference(container.entry);
        // use entry
    }

Implementation of is_safe : 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更新 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: Output:

1
0

There are two solutions I can recommend you:我可以向您推荐两种解决方案:

  1. if_valid(value, thenLambda, elseLambda) construct: if_valid(value, thenLambda, elseLambda)构造:
#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* )带有 then 和 else 路径的通用解析器:(这样做的好处是您可以简单地将.name作为解析器以及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:结合recursive_dereferenceconvert_optional_fact ,我最终得到了这个:

#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: Output:

1
0
name
empty

Play with it in Online GDB .网上玩 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) std::invoke 是 C++17,我需要 C++14(允许的 std::optional 除外)
  • 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.如果我们可以在deref中自动推导出最终类型,这样我们就不必在上一行的deref<Entry>中指定 Entry ,那就太好了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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