【问题标题】:C++ Safe Recursive DereferenceC++ 安全递归取消引用
【发布时间】:2022-11-18 21:02:26
【问题描述】:

我想要一种优雅的方式来安全地读取包含在“可空类型”(例如 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;
}

要从给定容器的条目中读取“名称”字段,我可以这样写:

    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&lt;std::is_constructible&lt;bool, T&gt;::value 来检查该字段是否可以用作 if 中的布尔值(可选和共享指针就是这种情况),这将允许我们检查它们是否已设置。如果它们已设置,我们可以继续取消引用递归。如果没有设置,我们可以中断递归并返回最终类型的空可选。

不幸的是,我无法将其表达为工作代码。该解决方案最多应限于“C++14 with optionals”。

更新:

先说一句。我意识到没有必要使用std::is_constructible&lt;bool, T&gt;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

【问题讨论】:

  • 你将如何处理错误?
  • 此外,std::optional&lt;std::shared_ptr&lt;T&gt;&gt; 没有什么意义,除非你真的想区分这两种错误情况(你 - 根据帖子 - 不要):std::shared_ptr&lt;T&gt; 也可以为空,因此表示可选性。
  • 关于错误处理:在这种情况下,我要么有值,要么没有。 shared_ptr 或 optional 是否为空都不会改变这一事实。在重要的情况下,每个字段都必须单独测试。
  • std::optional&lt;std::shared_ptr&lt;T&gt;&gt; 毫无意义”。在这种情况下,这是我们必须处理的。
  • if constexpr and concepts 你的方法可以小得多

标签: c++ templates shared-ptr template-meta-programming stdoptional


【解决方案1】:

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

  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 路径的通用解析器:(这样做的好处是您可以将 .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;
    }
    

【讨论】:

    【解决方案2】:

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

    输出:

    1
    0
    name
    empty
    

    Online GDB 中使用它。

    有了这个,我们可以在一行中从容器到字段:

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

    如果未设置某些内容,我们将获得一个带有“名称”字符串的可选项或空可选项。

    还开着:

    • std::invoke 是 C++17,我需要 C++14(允许的 std::optional 除外)
    • 如果我们可以在deref 中自动推导出最终类型,这样我们就不必在上面的行中在deref&lt;Entry&gt; 中指定 Entry 了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-05-21
      • 1970-01-01
      • 2010-11-20
      • 2012-03-19
      • 1970-01-01
      • 2019-07-18
      • 2011-11-23
      • 1970-01-01
      相关资源
      最近更新 更多