V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Symbo1ic
V2EX  ›  C++

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

  •  
  •   Symbo1ic · 39 天前 · 877 次点击
    这是一个创建于 39 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如标题。最近在看现代 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 内存模型的角度来讲是怎么做到的?这种做法会不会对在该值所在的函数之后运行的函数栈空间分配产生影响?
    9 条回复    2024-11-14 12:18:49 +08:00
    lzoje
        1
    lzoje  
       39 天前   ❤️ 1
    先回答下第三点,这实际上应该是编译器 nrvo 优化的效果( n 是 named value ,rvo 是 return value optimization )。这个优化的效果相当于给被优化的函数添加一个引用参数,这个引用就是返回的引用。

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

    第二点很简单,你返回栈地址就是有问题的,虽然编译器可以给你做这个优化,但不是所有地方都能做这个优化,这得编译器决定。我个人理解是这样的。
    sanbuks
        2
    sanbuks  
       39 天前   ❤️ 1
    1. 这边结果是 move ,而不是 copy ,建议再试一下
    2. 会产生悬垂引用问题, 一般直接返回 T 即可,返回右值引用情况很少见,比如这样 T{}.func();
    3. 本质就是直接构造到最终目标的存储中,具体参考不同编译器优化
    zizon
        3
    zizon  
       39 天前   ❤️ 1
    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
        4
    Symbo1ic  
    OP
       39 天前
    @lzoje @sanbuks @zizon 感谢各位大佬!我使用的是 msys2 上的 mingw64 ,可能这些结果出现就是编译器对于这个情况的优化吧。我想知道如果在编译器不进行 nrvo 优化的情况下,对于 func 传值 f2 或者 f3 的时候是不是会调用 move 构造呢?在使用右值引用这个特性的时候,是不是像 f3 那样返回一个经过 move 包裹后的值的处理方法会更好?
    lzoje
        5
    lzoje  
       38 天前
    @Symbo1ic 现在的编译器应该都默认启动返回值优化的,如果没有优化会有很多次复制的。需不需要返回一个 move 包裹的值,是看具体情况的,毕竟移动有什么效果取决于你的移动构造或者移动赋值函数做了什么。
    lzoje
        6
    lzoje  
       38 天前
    对了,第一个问题我前面看错了。为什么 shift 会 copy ,shift2 会 move 。因为你返回的都不是引用类型呀。你要知道,main 和 f2 和 f3 的栈空间都是不一样的。f2 f3 结束后,在其栈上的东西对于 main 来说都是不可用的,所以最后返回的东西要复制到 main 的栈上,所以会有 copy 或者 move 。如果你返回引用就不会了。
    sanbuks
        7
    sanbuks  
       38 天前
    @Symbo1ic
    -fno-elide-constructors 关闭 rvo 优化,测试一下
    zizon
        8
    zizon  
       38 天前
    @Symbo1ic f3 返回 std::move 更像是一种手工 hint.
    毕竟实际情况下,一个 function 的逻辑可能比较复杂,return value 在哪里产生的,能不能做 rvo 优化对编译器来说直接推导可能不一定能符合直觉.
    显式的在 return 的时候构造一个 rvalue 就简单的告诉编译器这是个 rvalue,可以 rvo.

    但是这么做安全不安全就是另外一回事了.
    zizon
        9
    zizon  
       38 天前
    @zizon "显式的在 return 的时候构造一个 rvalue 就简单的告诉编译器这是个 rvalue,可以 rvo."
    应该说是可以利用可能更优的 move ctor.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2706 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 08:34 · PVG 16:34 · LAX 00:34 · JFK 03:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.