optional-和一些杂七杂八的容器的一个小问题

今天看 Jason Turner 的视频才发现,原来我之前用 std::optional 之类的容器的方法并不一定合适(怎么听起来这么像营销号)。Turner 自己也吐槽自己的视频起的很标题党,不过我看了以后觉得其实还好,而且这些容器使用最佳实践的问题我真没太想过。所以写下来记一下。

比如说这样的代码:

1
2
3
4
5
6
7
8
9
10
struct Container {
int x;
Container(int x) : x(x) {}
};

std::optional<Container> get_value() {
std::optional<Container> ret;
ret = Container(42);
return ret;
}

这中间会有一个移动构造,因为 = Container(42) 那里右边是个 prvalue。然后把这个 ret 返回出去。

之前我如果简写的话,会写成这样:

1
return Container{42};

但是 Turner 说这里还是会调用一下移动构造(隐式构造一个 std::optional )。他说这样其实不是非常的 RVO 友好。

我自己也拿 Compiler Explorer 试了一下:

1
call std::optional<LifeMonitor>::optional<LifeMonitor, true>(LifeMonitor&&)
1
2
3
4
value_init -> Param Constructor(42)
move_of_value_init -> Move Constructor from value_init
value_init -> Destructor
move_of_value_init -> Destructor

好吧还真是。等于说刚才两段代码是等价的。

其实还是有 copy elision 的,就是最后那个隐式生成的 optional 作为返回值是被 NRVO 优化了的。但是,可能有些时候我们以为编译器优化的会更彻底一些,比如说直接在调用方的栈空间里面就地构造这个对象,从而避免这个多余的移动赋值,但是编译器实际上并没有进行这个优化。

我看评论区有 Rustacean 说什么 Rust 就可以对这种东西进行比较好的优化因为默认移动云云(唉,r批)。他说的可能有一定道理,因为在语义约束更严格的时候,确实可能有利于编译器进行更激进的优化。不过比较可惜的是,他并没有给出佐证他的论断的证据。

鉴于我现在还是用的 C++,所以还是回到 C++ 上来。Turner 的建议是,如果要保证 RVO 或者 NRVO 能够应用,最好写成这样:

1
2
3
std::optional<Container> ret;
ret.emplace(42);
return ret;
1
return std::optional<Container>{42};
1
return std::optional<Container>(std::in_place, 42);
1
2
value_init -> Param Constructor(42)
value_init -> Destructor

看汇编也没有那个移动构造了。

这三种方法都可以。这样一方面能够应用 RVO/NRVO,也能够保证 optional 里面的对象是就地构造的,没有多余的移动构造。pair/tuple 什么的也是一个道理。

评论里面还有个哥们指出了一点我觉得很有道理,他说返回值是 std::optional 或者 std::expected 这一类的东西的代码一般都有分支,比如说:

1
2
3
4
5
6
std::optional<Container> get_value(int n) {
if (n == 0) {
return std::nullopt;
}
return std::optional<Container>{42};
}

这样编译器又不能进行激进优化了,移动赋值又要冒出来。所以他建议如果没必要返回 optional 或者 expected 就不要返回它,好让编译器正常进行优化。

其实这个问题并不是非常严重。我之前虽然知道这个隐式移动构造的事情,但是确实也没太往优化这方面想过,所以写下来记录一下。


optional-和一些杂七杂八的容器的一个小问题
https://lizi.moe/2025/08/04/optional-和一些杂七杂八的容器的一个小问题/
作者
李萌
发布于
2025年8月4日
许可协议