C++ 相当于 Javascript 的“?.”可选链运算符?

问题描述 投票:0回答:5

在 Javascript 中,如果我有一个潜在的 null 对象

obj
,如果不为 null,它将有一个字段
x
,我可以写
obj?.x
。这称为 可选链接或安全导航:如果 obj 不是具有可访问字段的对象,则不会抛出异常(如果
obj
是一个对象但没有
x
字段,则不会抛出异常,尽管这是也许是一个单独的功能)。

现在让我们转向 C++,从 C++17 开始它就有了标准库

std::optional<T>
。假设我们有:

struct Foo { Bar x; }

我有一个名为

optional<Foo>
obj
对象。天真地,我可能会写出这样的表达式:

obj ? obj->x : optional<Bar>{}

...但这行不通,因为两个结果表达式的类型不同,并且三元运算符不能协调它们。 Godbolt 确认,使用以下代码:

#include <optional>

using Bar = int;
struct Foo { Bar x; };

std::optional<Bar> f()
{
    std::optional<Foo> obj {};
    return obj ? obj->x : std::nullopt;
}

那么,在 C++ 中,有什么简短的惯用法来表达

obj?.x
呢?


注意:任何语言标准版本都可以,但显然越旧越好。

c++ option-type idioms stdoptional optional-chaining
5个回答
4
投票

C++23 引入了 可选的 Monadic 操作,因此您可以使用 std::Optional::transform

#include <optional>

using Bar = int;
struct Foo { Bar x; };

std::optional<Bar> f()
{
    std::optional<Foo> obj {};
    return obj.transform([](auto&& o) { return o.x;});
}

对于较低的 C++ 版本,您可以使用 C++11 可选库,它具有类似的操作(映射),或者编写您自己的

transform
版本(不推荐)。

godbolt 演示

我不建议您编写自己的

transform
的原因是因为UB很可能从标准库对象继承,而使其可链接的唯一方法是从
std::optional
继承。


3
投票

我很惊讶地发现C++

option
缺少一个
map
方法,但经过几分钟的思考,似乎很容易模仿。

template<class I, class O>
std::optional<O> map(const std::optional<I>& val, O const I::*member)
{ return val ? std::optional<O>{*val.*member} : std::nullopt; }

template<class I, class O, class...Args>
std::optional<O> map(const std::optional<I>& val, O (I::*method)() const, Args&&...args)
{ return val ? std::optional<O>{(*val.*method)(std::forward<Args>(args)...)} : std::nullopt; }

然后用法很简洁:

map(obj, &Foo::x);
map(obj, &Foo::getX); //if you need to pass parameters to this, you can

这仅适用于成员和方法,不适用于任意函数调用,但任意函数调用将需要 lambda,这在 C++ 中非常冗长。


2
投票

Mooing Duckanswer 的一个 hacky,但可以说更有趣的调整可以让我们使用:

map(obj, x)

而不是 Javascript 的

obj?.x

是的,就是这么简单,因为涉及到宏>:-)

#include <optional>
#include <type_traits>

using Bar = int;
struct Foo { Bar x; };

template<class I, class O>
std::optional<O> map_impl(const std::optional<I>& val, O const I::*member)
{ return val ? std::optional<O>{*val.*member} : std::nullopt; }

#define map(obj_, field_) map_impl(obj_, &std::remove_reference_t<decltype(*obj_)> :: field_ )

备注:

  • 请参阅 GodBolt 上的工作。
  • 需要进行一些调整才能适用于方法而不仅仅是数据字段,但我很确定某些 TMP 可以实现这一点。
  • 我们可能不会使用这个名称,以避免与 std::map 和“map”一词的其他用法发生冲突 - 宏更加“语法帝国主义”。所以也许像
    qdot
  • 如果我们应用@milleniumbug的中缀运算符生成器,我们甚至可以将其变成一个合适的中缀运算符,这将使我们接近涅槃与
    obj qdot x

1
投票

(C++23) 通过一点

operator|
重载来进行管道传输,并从 monad 剧本中取出一页,可以做类似的事情...

#include <iostream>
#include <optional>
#include <type_traits>
#include <utility>

using Bar = int;

struct Foo { Bar x; };

struct FFoo { std::optional<Bar> x{}; };

inline auto f(std::optional<Foo> obj) -> std::optional<Bar> {
    return obj ? obj->x : std::nullopt;
}

inline auto ff(std::optional<FFoo> obj) -> std::optional<Bar> {
    return obj ? obj->x : std::nullopt;
}

inline void println(std::optional<Bar> obj) {
    if (!obj) {
        std::cout << "(empty)\n";
    } else {
        std::cout << *obj << "\n";
    }
}

// C++23 magic to use operator| to pipe calls.
template <typename T, typename FN> requires (std::invocable<FN, T>)
constexpr auto operator|(T&& t, FN&& f) -> typename std::invoke_result_t<FN, T> {
    return std::invoke(std::forward<FN>(f), std::forward<T>(t));
}

int main() {
    std::optional<FFoo> no_foo;
    std::optional<FFoo> foo_no_bar{ Foo{} };
    std::optional<FFoo> foo_bar{ Foo{ 10 } };
    std::optional<Foo> original_foo{ Foo{ 20 } };
    std::optional<Foo> original_no_foo{};


    no_foo | ff | println;
    foo_no_bar | ff | println;
    foo_bar | ff | println;
    original_foo | f | println;
    original_no_foo | f | println;
}

更新:将修改后的

Foo
重命名为
FFoo
,将
f
重命名为
ff
,添加了原始的
Foo
和原始的
f
,并添加了
original_foo
original_no_foo
对象来演示使用。


0
投票

根据用例,您可以使用 4 个 monadic 函数中的任意一个:

  1. 获取完整值(如果存在)或提供默认值:
    optional::value_or
    。此方法需要一个默认值。
std::optional<Foo> optfoo;
Foo foo = foo.value_or(Foo{});
  1. 替换当前值(如果存在)或使用
    nullopt
    返回 
    optional::transform
    :
std::optional<Foo> optfoo;
std::optional<Bar> optbar = foo.transform(&Foo::x);

此方法可以将结果类型从

optional<foo>
更改为
optional<bar>

  1. 通过
    optional::and_then
    将当前值转换为可选值(可能为空)。
std::optional<Foo> optfoo;
std::optional<Bar> optbar = foo.and_then([](Foo& foo){
    return std::optional{foo.x};
});

transform
的区别在于 lambda 返回一个
optional

  1. 用相同类型的可选值替换该值,如果它为空,则使用
    optional::or_else
    :
std::optional<Foo> optfoo;
std::optional<Foo> optfoo2 = foo.or_else([]{
    return std::optional{Foo{}};
});

此方法保留

optional<foo>
的类型,但尝试替换值(如
value_or
)。

选项 1 是唯一的 C++17 解决方案,也是唯一一个不将 lambda/函数作为输入的解决方案,也是唯一一个不一定返回

optional
的解决方案。其他选项需要 C++23 ,返回可选值并需要函数输入。

© www.soinside.com 2019 - 2024. All rights reserved.