C++安全递归取消引用

gv8xihay  于 2022-11-19  发布在  其他
关注(0)|答案(2)|浏览(165)

我想要一个优雅的方法来安全地读取一个被 Package 在“可空类型”中的字段中的数据,比如std::optional和std::shared_ptr。

#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;
}

要从给定Container的Entry中读取“name”字段,我可以写:

std::cout << *((*container.entry)->name) << std::endl;

但是我觉得这并不容易读或写,而且由于可选的和共享的指针可能没有被设置,我也不能。
我想避免这样的代码:

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

而我在寻找更像这样的东西:

const auto entry = recursive_dereference(container.entry);
    const auto name = recursive_dereference(entry.name);
    std::cout << name.value_or("empty") << std::endl;

这将基于此recursive_dereference实现。
问题是,如果一个可选的或者shared_ptr没有设置,它就会崩溃。有没有办法修改recursive_dereference,使它在一个可选的返回结果,当一个字段没有设置时,这个可选的返回结果是空的?
我想我们可以使用std::enable_if_t<std::is_constructible<bool, T>::value来检查该字段是否可以在if中作为bool使用(对于可选项和共享指针也是如此),这将允许我们检查它们是否被设置。如果它们被设置,我们可以继续解引用递归。如果没有设置,我们可以中断递归并返回一个最终类型的空可选项。
不幸的是,我无法将其公式化为工作代码。解决方案最多只能限于“C++14 with optionals”。

更新日期:

首先,我意识到使用std::is_constructible<bool, T>是不必要的。recursive_dereference检查一个类型是否可以被解除引用,当它可以的时候,我们可以检查它是否用if (value)设置。至少它可以与可选和共享指针一起工作。
我发现的另一种方法是首先单独检查解引用该值是否安全,然后不修改地调用recursive_dereference
所以我们可以做到:

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

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

我仍然希望有一个更好的解决方案,可以避免分别检查和引用,这样我们就可以在一次传递中得到一个值或“空”。

更新2

我设法得到了一个不需要单独检查的版本。我们必须显式地给予我们期望的最终类型作为模板参数。它返回一个可选的值,或者一个空的可选的,如果沿着没有设置一个引用的话。

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

用法:

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

输出量:

1
0
t9aqgxwy

t9aqgxwy1#

我可以向您推荐两种解决方案:

  1. 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.具有then和else路径的一般解析器:(这样做的好处是,您可以简单地将.nameoperator*用作解析器)

#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;
}
isr3a4wc

isr3a4wc2#

将recursive_dereference和convert_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;
}

输出量:

1
0
name
empty

Online GDB中使用它。
有了这个,我们就可以得到从容器到字段的一行:

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

我们得到一个可选的字符串“name”,如果没有设置,则为空可选。
仍然打开:

  • std::invoke是C17,而我需要C14(允许std::optional除外)
  • 如果我们能自动推导出deref中的最终类型,那么我们就不必在上面的行中指定deref<Entry>中的Entry了。

相关问题