19

注意:这个问题被简单地标记为this的副本,但它不是完全相同的副本,因为我专门询问 std::optionals 。如果您关心一般情况,这仍然是一个值得阅读的好问题。

假设我有嵌套的选项,像这样(愚蠢的玩具示例):

struct Person{
    const std::string first_name;
    const std::optional<std::string> middle_name;
    const std::string last_name;
};
struct Form{
    std::optional<Person> person;
};

和这个垃圾邮件功能:

void PrintMiddleName(const std::optional<Form> form){
    if (form.has_value() && form->person.has_value() && form->person->middle_name.has_value()) {
        std::cout << *(*(*form).person).middle_name << std::endl; 
    } else {
        std::cout << "<none>"  << std::endl; 
    }
}

展平此可选检查的最佳方法是什么?我做了这样的东西,它不是可变参数,但我不太在意(membr3如果真的有必要,我可以再添加一个级别(重载),除此之外的所有内容都是糟糕的代码)。

template<typename T, typename M>
auto flatten_opt(const std::optional<T> opt, M membr){
    if (opt.has_value() && (opt.value().*membr).has_value()){
        return std::optional{*((*opt).*membr)};
    }
    return decltype(std::optional{*((*opt).*membr)}){};
}

template<typename T, typename M1, typename M2>
auto ret_val_helper(){
    // better code would use declval here since T might not be 
    // default constructible.
    T t;
    M1 m1;
    M2 m2;
    return ((t.*m1).value().*m2).value();
}

template<typename T, typename M1, typename M2>
std::optional<decltype(ret_val_helper<T, M1, M2>())> flatten_opt(const std::optional<T> opt, M1 membr1, M2 membr2){
    if (opt.has_value() && (opt.value().*membr1).has_value()){
        const auto& deref1 = *((*opt).*membr1);
        if ((deref1.*membr2).has_value()) {
            return std::optional{*(deref1.*membr2)};
        }
    }
    return {};
}

void PrintMiddleName2(const std::optional<Form> form){
    auto flat  = flatten_opt(form, &Form::person, &Person::middle_name);
    if (flat) {
        std::cout << *flat;
    }
    else {
        std::cout << "<none>"  << std::endl; 
    }
}

神螺栓

笔记:

  • 我不想切换std::optional一些更好的可选.
  • 我不太关心性能,除非我返回一个我必须复制的指针(除非 arg 是临时的),因为std::optional不支持引用。
  • 我不关心flatten_has_value函数(尽管它很有用),因为如果有一种方法可以很好地展平嵌套的可选项,那么还有一种方法可以编写该函数。
  • 我知道我的代码看起来可以工作,但它很丑陋,所以我想知道是否有更好的解决方案。
4

3 回答 3

13

您要查找的操作称为 monadic绑定操作,有时拼写为and_then(就像在P0798Rust中一样)。

您正在使用optional<T>and 函数T -> optional<U>并想要取回optional<U>. 在这种情况下,函数是指向数据成员的指针,但在这个意义上它确实表现得像一个函数。&Form::person取 aForm并返回 a optional<Person>

您应该以与函数类型无关的方式编写它。它是一个指向成员数据的指针这一事实在这里并不重要,也许明天你会想要一个指向成员函数甚至一个自由函数的指针。所以那是:

template <typename T,
          typename F,
          typename R = std::remove_cvref_t<std::invoke_result_t<F, T>>,
          typename U = mp_first<R>>
    requires SpecializationOf<R, std::optional>
constexpr auto and_then(optional<T> o, F f) -> optional<U>
{
    if (o) {
        return std::invoke(f, *o);
    } else {
        return std::nullopt;
    }
}

这是用 C++ 编写的多种函数声明之一,即使使用概念也是如此。我将把它作为一个练习来正确添加引用。我选择专门写它,-> optional<U>而不是-> R因为我认为它对可读性很重要,你可以看到它确实返回了某种optional.

现在,问题是我们如何将其链接到多个函数。Haskell>>=用于单子绑定,但在 C++ 中关联错误(o >>= f >>= g将首先评估f >>= g并需要括号)。所以下一个最接近的运算符选择是>>(这在 Haskell 中意味着不同的东西,但我们不是 Haskell,所以没关系)。或者您可以借用|Ranges 所做的模型来实现此功能。

所以我们要么在语法上结束:

auto flat  = form >> &Form::person >> &Person::middle_name;

或者

auto flat = form | and_then(&Form::person)
                 | and_then(&Person::middle_name);

将多个单子绑定组合在一起的另一种方法是 Haskell 拼写的操作,>=>称为 Kleisli 组合。在这种情况下,它接受一个函数T -> optional<U>和一个函数U -> optional<V>并产生一个函数T -> optional<V>。这对于编写约束来说是非常烦人的,所以我将跳过它,它看起来像这样(使用 Haskell 运算符拼写):

template <typename F, typename G>
constexpr auto operator>=>(F f, G g) {
    return [=]<typename T>(T t){
        using R1 = std::remove_cvref_t<std::invoke_result_t<F, T>>;
        static_assert(SpecializationOf<R1, std::optional>);
        using R2 = std:remove_cvref_t<std::invoke_result_t<G, mp_first<R1>>>;
        static_assert(SpecializationOf<R2, std::optional>);

        if (auto o = std::invoke(f, t)) {
            return std::invoke(g, *o);
        } else {
            // can't return nullopt here, have to specify the type
            return R2();
        }
    };
}

然后你可以写(或者如果是你可以使用的运算符,至少你可以>=>):

auto flat  = form | and_then(&Form::person >=> &Person::middle_name);

因为>=>现在的结果是一个接受 aForm并返回a 的函数optional<string>

于 2021-04-02T23:27:32.277 回答
5

让我们看看 flatten 函数的最佳形式是什么样的。在这种情况下,“最佳”是指最小的呈现。

即使在最佳情况下,在执行展平操作时,您也需要提供:

  1. optional<T>要展平的对象。
  2. 展平操作函数名称。
  3. 在每个展平步骤中按顺序间接列出的名称列表。

您的代码非常接近最优。唯一的问题是“名称列表”中的每个名称都必须包含您在该级别访问的成员的类型名称,这是假设可以使用T.

C++ 没有比这更好的机制。如果要访问对象的成员,则必须提供该对象的类型。如果你想间接做到这一点,C++ 允许使用成员指针,但获取这样的指针需要知道提取成员时对象的类型。offsetof当您获得偏移量时,即使是体操也需要使用类型名称。

反射将允许更好的东西,因为您可以传递静态反射可以用来从当前使用的类型中获取成员指针的编译时字符串。但是 C++20 没有这样的特性。

于 2021-04-02T17:31:46.950 回答
5

对于基本上是可链接操作的东西,您有很多辅助函数。C++ 有链的东西:操作符。所以我可能会(ab)使用operator*这个。

对于您的具体情况,您只需要

template<class class_t, class member_t>
std::optional<std::remove_cv_t<member_t>> operator*(
        const std::optional<class_t>& opt, 
        const std::optional<member_t> class_t::*member) 
{
    if (opt.has_value()) return opt.value().*member;
    else return {};
}

void PrintMiddleName2(const std::optional<Form> form){
    auto middle = form * &Form::person * &Person::middle_name;
    if (middle) {
        std::cout << *middle;
    }
    else {
        std::cout << "<none>"  << std::endl; 
    }
}

但实际上,您可能还需要非可选成员、getter 方法和任意转换的变体,我在此处列出了这些变体,尽管我不能 100% 确定它们都能正确编译。

//data member
template<class class_t, class member_t>
std::optional<std::remove_cv_t<member_t>> operator*(const std::optional<class_t>& opt, const std::optional<member_t> class_t::*member) {
    if (opt.has_value()) return opt.value().*member;
    else return {};
}
template<class class_t, class member_t>
std::optional<std::remove_cv_t<member_t>> operator*(const std::optional<class_t>& opt, const member_t class_t::*member) {
    if (opt.has_value()) return {opt.value().*member};
    else return {};
}

//member function
template<class class_t, class return_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, std::optional<return_t>(class_t::*member)()) {
    if (opt.has_value()) return opt.value().*member();
    else return {};
}
template<class class_t, class return_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, return_t(class_t::*member)()) {
    if (opt.has_value()) return {opt.value().*member()};
    else return {};
}

//arbitrary function
template<class class_t, class return_t, class arg_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, std::optional<return_t>(*transform)(arg_t&&)) {
    if (opt.has_value()) return transform(opt.value());
    else return {};
}
template<class class_t, class return_t, class arg_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, return_t(*transform)(arg_t&&)) {
    if (opt.has_value()) return {transform(opt.value())};
    else return {};
}

http://coliru.stacked-crooked.com/a/26aa7a62f38bbd89

于 2021-04-02T17:37:04.087 回答