如何展开嵌套的std::optional?

19

注意:这个问题曾经被暂时标记为此问题的重复,但它并不是完全相同的,因为我特别询问std::optional。如果你关心一般情况,那么阅读这个问题仍然是一个好主意。

假设我有嵌套的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; 
    }
}

Godbolt

注意:

  • 我不想切换到更好的可选项 (TartanLlama/optional)
  • 我不太在意性能,除非返回指针,因为除非参数是临时的,否则必须进行拷贝(因为 std::optional 不支持引用)。
  • 虽然 flatten_has_value 函数很有用,但我不太关心它,因为如果有一种方法可以很好地展开嵌套的可选项,那么也有一种方法编写该函数。
  • 我知道我的代码看起来像是正常工作了,但实际上它相当丑陋,所以我想知道是否有更好的解决方案。

13
一个更少冗长的 if (form.has_value() && form->person.has_value() && form->person->middle_name.has_value()) 可以改为 if (form && form->person && form->person->middle_name)。一个更少冗长的 *(*(*form).person).middle_name 可以改为 form->person->middle_name - Drew Dormann
2
在使用 std::string 时很少需要使用 std::optional。特别是在这种情况下,没有必要区分缺失的中间名和空白的中间名。因此,我会将 const std::optional<std::string> middle_name; 更改为 const std::string middle_name;,然后使用 if (form && form->person && !form->person->middle_name.empty()) { */ use form->person->middle_name as needed */ } - Remy Lebeau
2
请考虑middle_name。如果middle_name的值为空字符串和middle_name没有值有什么区别?如果答案是“永远没有区别”,如果你的代码从未将这两种情况视为不同,那么optional就不应该是这个属性的类型。 - Nicol Bolas
1
@RemyLebeau 这只是一个玩具示例,真正的代码可能是一个复杂的结构体/类,并且调用代码可能会注意到默认构造和nullopt之间的差异。 - NoSenseEtAl
这篇文章似乎很合适...如果你想要严谨,那么所有数据成员都应该是std::optional - Casey
显示剩余12条评论
3个回答

14
你要查找的操作称为单目绑定操作,有时拼写为and_then(如在P0798Rust中)。
你会取一个optional<T>和一个函数T -> optional<U>,希望得到optional<U>。在这种情况下,该函数是指向数据成员的指针,但从这个意义上说,它确实表现为一个函数。&Form::person获取一个Form并返回一个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++中许多函数声明之一,即使使用概念(concepts)也很难写。我将把如何正确添加引用作为练习留给你。我选择特别将其编写为-> 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);

因为>=>的结果是一个函数,它接受一个Form并返回一个optional<string>

1
很棒的回答!未来回答的小建议:如果你有一些概念集合,可以将它们添加到公共位置,然后在类似这样的帖子中链接它们(只是不要试图编写可排序的概念,因为众所周知一个人无法编写它;))。例如,我知道SpecializationOf是什么,因为我记得关于这个问题的提问,但我怀疑大多数阅读此答案的人都不知道如何在Google上找到它/在SO上找到它。 - NoSenseEtAl
有一个可重载的二进制指向成员指针绑定运算符(operator ->*):auto flat= form ->* &Form::person ->* &Person::middle_name;。请参考https://en.cppreference.com/w/cpp/language/operator_precedence中表格中的第4项。结合性也是从左到右,这很好。 - Red.Wave

5
让我们看一下展平函数的最佳形式会是什么样子。在这种情况下,“最佳”指的是最小表示形式。
即使在最优情况下,在执行展平操作时,您仍需要提供:
1. 要展开的optional<T>对象。 2. 展开操作函数名称。 3. 每个展平步骤中要间接引用的名称列表(按顺序排列)。
您的代码非常接近最优。唯一的问题是在“名称列表”中的每个名称必须包含您正在访问的成员的类型名称,这是可以使用有关T的知识计算的假设。
C++没有比这更好的机制。如果您想访问对象的成员,您必须提供该对象的类型。如果您想间接执行此操作,C++允许使用成员指针,但获取这样的指针需要在提取成员时知道对象的类型。即使是 offsetof(偏移量),也需要在获取偏移量时使用类型名称的技巧。
反射将允许更好的处理方式,因为您可以传递编译时字符串,静态反射可以使用这些字符串从当前使用的类型中提取成员指针。但是,C++20没有这样的功能。

5
你有很多辅助函数用于一些本质上是可链接操作的事情。而 C++ 有用于链式调用的东西:运算符。所以我可能会(滥用)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方法和任意转换,我在此列出了它们,但我并不百分之百确定它们是否都可以正确编译。

//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


我通常不喜欢定义运算符,但这个还不错... :) - NoSenseEtAl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接