optional-和一些杂七杂八的容器的一个小问题
今天看 Jason Turner 的视频才发现,原来我之前用 std::optional
之类的容器的方法并不一定合适(怎么听起来这么像营销号)。Turner 自己也吐槽自己的视频起的很标题党,不过我看了以后觉得其实还好,而且这些容器使用最佳实践的问题我真没太想过。所以写下来记一下。
比如说这样的代码:
1 |
|
这中间会有一个移动构造,因为 = Container(42)
那里右边是个 prvalue。然后把这个 ret
返回出去。
之前我如果简写的话,会写成这样:
1 |
|
但是 Turner 说这里还是会调用一下移动构造(隐式构造一个 std::optional
)。他说这样其实不是非常的 RVO 友好。
我自己也拿 Compiler Explorer 试了一下:
1 |
|
1 |
|
好吧还真是。等于说刚才两段代码是等价的。
其实还是有 copy elision 的,就是最后那个隐式生成的 optional 作为返回值是被 NRVO 优化了的。但是,可能有些时候我们以为编译器优化的会更彻底一些,比如说直接在调用方的栈空间里面就地构造这个对象,从而避免这个多余的移动赋值,但是编译器实际上并没有进行这个优化。
我看评论区有 Rustacean 说什么 Rust 就可以对这种东西进行比较好的优化因为默认移动云云(唉,r批)。他说的可能有一定道理,因为在语义约束更严格的时候,确实可能有利于编译器进行更激进的优化。不过比较可惜的是,他并没有给出佐证他的论断的证据。
鉴于我现在还是用的 C++,所以还是回到 C++ 上来。Turner 的建议是,如果要保证 RVO 或者 NRVO 能够应用,最好写成这样:
1 |
|
1 |
|
1 |
|
1 |
|
看汇编也没有那个移动构造了。
这三种方法都可以。这样一方面能够应用 RVO/NRVO,也能够保证 optional 里面的对象是就地构造的,没有多余的移动构造。pair/tuple 什么的也是一个道理。
评论里面还有个哥们指出了一点我觉得很有道理,他说返回值是 std::optional
或者 std::expected
这一类的东西的代码一般都有分支,比如说:
1 |
|
这样编译器又不能进行激进优化了,移动赋值又要冒出来。所以他建议如果没必要返回 optional 或者 expected 就不要返回它,好让编译器正常进行优化。
其实这个问题并不是非常严重。我之前虽然知道这个隐式移动构造的事情,但是确实也没太往优化这方面想过,所以写下来记录一下。