在 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++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
版本(不推荐)。
我不建议您编写自己的
transform
的原因是因为UB很可能从标准库对象继承,而使其可链接的唯一方法是从std::optional
继承。
我很惊讶地发现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++ 中非常冗长。
对 Mooing Duck 的 answer 的一个 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_ )
备注:
qdot
。obj qdot x
。(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
对象来演示使用。
根据用例,您可以使用 4 个 monadic 函数中的任意一个:
optional::value_or
。此方法需要一个默认值。std::optional<Foo> optfoo;
Foo foo = foo.value_or(Foo{});
nullopt
返回
optional::transform
:std::optional<Foo> optfoo;
std::optional<Bar> optbar = foo.transform(&Foo::x);
此方法可以将结果类型从
optional<foo>
更改为 optional<bar>
。
optional::and_then
将当前值转换为可选值(可能为空)。std::optional<Foo> optfoo;
std::optional<Bar> optbar = foo.and_then([](Foo& foo){
return std::optional{foo.x};
});
与
transform
的区别在于 lambda 返回一个 optional
。
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 ,返回可选值并需要函数输入。