C++中右值与右值引用在使用中的疑问

8 天前
 Symbo1ic

如标题。最近在看现代 cpp ,感觉右值和右值引用这两个概念非常重要,因此自己尝试构建一些例子来加深理解。例子如下:

#include <iostream>
#include <ostream>
#include <utility>
using namespace std;
struct C {
    int a = 1;
    C() { cout << "Ha ha" << endl; }
    C(C& c) : a(c.a) { cout << "Copy" << endl; }
    C(C&& c) : a(std::move(c.a)) { cout << "Move" << endl; }
    ~C() { cout << "Fucked" << endl; }
};
C func() {
    C shit;
    cout << &shit << endl;
    return shit;
}

C f2() {
    C&& shit = func();
    cout << &shit << endl;
    return shit;
}

C f3() {
    C&& shit = func();
    cout << &shit << endl;
    return std::move(shit);
}

int main() {
    auto&& shit = f2();
    // Ha ha
    // 0x5ffe24
    // 0x5ffe24
    // Copy
    // Fucked
    cout << &shit << endl;
    // 0x5ffe7c
    cout << "*************" << endl;
    auto shit2 = f3();
    cout << &shit2 << endl;
    // Ha ha
    // 0x5ffe24
    // 0x5ffe24
    // Move
    // Fucked
    // 0x5ffe78
    cout << "*************" << endl;
    auto&& shit3 = f3();
    cout << &shit3 << endl;
    // Ha ha
    // 0x5ffe24
    // 0x5ffe24
    // Move
    // Fucked
    // 0x5ffe74
    cout << "*************" << endl;
    // cout << shit.a;
}

对于这个例子所产生的结果,我不是很懂,我主要是不太懂以下几个问题:

  1. 为啥对于右值引用 shit ,在初始化之前会产生 copy ?而对于 shit2 则会在之前有 move ?
  2. 我尝试将 f3 的声明改成了'C&& f3()',这个时候 clangd 警告引用了一个栈空间上的变量。为啥会这样?正常来说右值引用应该怎么使用?
  3. f2 和 f3 都引用了位于 func 构建出来的函数栈上的 shit ,从地址可以看出他们引用后地址没有产生变化。则使用右值引用引用了一个位于栈上的值,从 cpp 内存模型的角度来讲是怎么做到的?这种做法会不会对在该值所在的函数之后运行的函数栈空间分配产生影响?
481 次点击
所在节点    C++
9 条回复
lzoje
8 天前
先回答下第三点,这实际上应该是编译器 nrvo 优化的效果( n 是 named value ,rvo 是 return value optimization )。这个优化的效果相当于给被优化的函数添加一个引用参数,这个引用就是返回的引用。

所以第一点的问题,f2 和 f3 里都无法做 nrvo 优化,因为返回值是一个右值引用,而不是栈变量(虽然我们能看到那个是栈变量,但是编译器应该是判断不到)。

第二点很简单,你返回栈地址就是有问题的,虽然编译器可以给你做这个优化,但不是所有地方都能做这个优化,这得编译器决定。我个人理解是这样的。
sanbuks
8 天前
1. 这边结果是 move ,而不是 copy ,建议再试一下
2. 会产生悬垂引用问题, 一般直接返回 T 即可,返回右值引用情况很少见,比如这样 T{}.func();
3. 本质就是直接构造到最终目标的存储中,具体参考不同编译器优化
zizon
8 天前
func -> 没有 rvalue 之前 return 会 copy,rvalue 的引入就是为了解决这种不必要的 copy.

f2 -> 类似 func,看编译器有没激进到断定 shit 可以以 rvalue 做 rvo, 不能的话就会保守 copy.因为 shit 本身指向一个 rvalue,所以编译器有理由认为它可能在 return 后不是一个有效地址,就是你 2 里提到的 clang 警告的原因.如果编译器先把 func inline 了,那么就可能出现 2L 说的第一点的情况.

f3 -> 类似 f2,std::move 产生了一个 rvalue.调用 move ctor 是编译器认为这个 rvalue 可能不是有效的地址.同 f2,编译器也可以激进优化成 func.

通俗的理解 ravlue 就是之前没有 rvalue 的时候会产生隐式不可代码层面 reference 的 copy ctor 对象的一个称呼.
有了这个定义之后,新标准就可以有规则 eliminate 这种不必要的 copy ctor.
Symbo1ic
8 天前
@lzoje @sanbuks @zizon 感谢各位大佬!我使用的是 msys2 上的 mingw64 ,可能这些结果出现就是编译器对于这个情况的优化吧。我想知道如果在编译器不进行 nrvo 优化的情况下,对于 func 传值 f2 或者 f3 的时候是不是会调用 move 构造呢?在使用右值引用这个特性的时候,是不是像 f3 那样返回一个经过 move 包裹后的值的处理方法会更好?
lzoje
8 天前
@Symbo1ic 现在的编译器应该都默认启动返回值优化的,如果没有优化会有很多次复制的。需不需要返回一个 move 包裹的值,是看具体情况的,毕竟移动有什么效果取决于你的移动构造或者移动赋值函数做了什么。
lzoje
8 天前
对了,第一个问题我前面看错了。为什么 shift 会 copy ,shift2 会 move 。因为你返回的都不是引用类型呀。你要知道,main 和 f2 和 f3 的栈空间都是不一样的。f2 f3 结束后,在其栈上的东西对于 main 来说都是不可用的,所以最后返回的东西要复制到 main 的栈上,所以会有 copy 或者 move 。如果你返回引用就不会了。
sanbuks
7 天前
@Symbo1ic
-fno-elide-constructors 关闭 rvo 优化,测试一下
zizon
7 天前
@Symbo1ic f3 返回 std::move 更像是一种手工 hint.
毕竟实际情况下,一个 function 的逻辑可能比较复杂,return value 在哪里产生的,能不能做 rvo 优化对编译器来说直接推导可能不一定能符合直觉.
显式的在 return 的时候构造一个 rvalue 就简单的告诉编译器这是个 rvalue,可以 rvo.

但是这么做安全不安全就是另外一回事了.
zizon
7 天前
@zizon "显式的在 return 的时候构造一个 rvalue 就简单的告诉编译器这是个 rvalue,可以 rvo."
应该说是可以利用可能更优的 move ctor.

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1089024

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX