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

左值右值,有没有通俗易懂且具体的资料

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

    看了几篇文档了

    比如巨硬的这个: Lvalues and Rvalues

    比如 cpprefrence 的这个: value categories

    38 条回复    2022-02-07 13:09:45 +08:00
    mainjzb
        1
    mainjzb  
       129 天前
    右值就是临时变量为了减少拷贝生产的
    例如
    int Test(){
    return 100;
    }

    int main(){
    auto t = Test();
    }
    整个过程只有生成一个 100 ,没有发生任何拷贝。如何做到这一点。就是让编译器知道 100 是右值。他可以直接把所有权交给 m 不用再拷贝。

    int Test(){
    int x = 100;
    return x;
    }

    int main(){
    auto t = Test();
    }
    因为 x 是左值。所以产生了一次拷贝。
    mainjzb
        2
    mainjzb  
       129 天前
    以前的 C++ 2 种情况都会拷贝一次。为了解决这种没用的拷贝才发明的这堆名词。
    anonymousar
        3
    anonymousar  
       129 天前
    modern effective c++ 里面写的很清楚了
    Coelacanth
        4
    Coelacanth  
       129 天前 via Android
    看 stackoverflow 上面的最高赞回答,应该是最通俗的。
    yulon
        5
    yulon  
       129 天前
    有名字的就是左值,不要光看声明,有名字的右值引用也是左值。

    1L 是不对的,两种情况完全一样,左值也可以不拷贝,因为有 RVO ,而且基础类型只有拷贝这一种情况,除非被优化掉,优化的也不是变量,而是函数被优化成内联。
    leido
        6
    leido  
       129 天前
    能写在等号左边的就是左值 (逃
    majula
        7
    majula  
       129 天前 via iPhone
    @leido 谭浩强给你点了个赞(
    6david9
        8
    6david9  
       129 天前 via Android
    把变量想象成一个箱子,存在变量里的就是右值,可以存其他东西的“箱子”就是左值。
    变量和变量里的值。
    GeruzoniAnsasu
        9
    GeruzoniAnsasu  
       129 天前
    > 比如 cpprefrence 的这个: https://en.cppreference.com/w/cpp/language/value_category


    ----

    那么你看这个了吗: https://zh.cppreference.com/w/cpp/language/value_category

    没开玩笑,如果你觉得英文版还不够通俗易懂,有翻译好的中文版而且质量没什么问题,别因为莫名其妙的「鄙视感」忽略它。

    c++中值类别的最大作用或者说区分它的意义就是用来判定能否「减少复制」(无论 copy elision 还是 move semantics )。我相信凡是写过 c++代码的人都一定对「对象复制」有一些直觉,标准中去界定这些值类型只不过把你想到或没想到的地方都公理化了而已



    还有,别看 c++11 了,直接从 c++17 看起。c++11 作为 morden c++的第一个版本很多地方都是不成熟的
    statumer
        10
    statumer  
       129 天前 via iPhone
    歪个楼,C++的 value category ,prvalue ,glvalue ,rvalue ,guaranteed copy elision 的重大意义就是解决了其他语言根本不存在的问题,那就是 s=s1+s2 中这个=如何实现的问题。
    Jooooooooo
        12
    Jooooooooo  
       129 天前
    我一直很好奇, c 里面如此奇葩的东西怎么没人喷呢, 反而是 java 里那些无关紧要的东西被喷的要死.
    msg7086
        13
    msg7086  
       129 天前
    @Jooooooooo
    Java 用的人更多一些。
    C 和 C++更古老一些,历史包袱比较多,而且更贴近硬件,对性能要求更高。
    (当然 Java 也有很多历史包袱(著名的泛型……

    在其他语言里,赋值的时候复制一下内存可能睁一只眼闭一只眼也就算了,但是在 C/C++环境里,遇到计算密集的场景,对这些「浪费」就会非常敏感。少复制一块内存,少产生一个无用指令集,在生产环境里可能就会带来数万元甚至更多的收益。所以大家更喜欢在细节处抠性能。
    shineit
        14
    shineit  
       129 天前
    还要看应用场景:左值引用 /右值引用
    crackhopper
        15
    crackhopper  
       129 天前   ❤️ 1
    crackhopper
        16
    crackhopper  
       129 天前
    cppreference 里很多说明跟 c++标准差不多。想搞明白挺难的。不过反复琢磨会提高理解是深度。
    ligiggy
        17
    ligiggy  
    OP
       129 天前
    @Jooooooooo c++无时无刻都在重视性能
    ligiggy
        18
    ligiggy  
    OP
       129 天前
    @crackhopper
    @leimao
    感谢两位大佬的分享!
    powerman
        19
    powerman  
       129 天前
    @msg7086 这些场景现在越来越少了,基本上跟整个软件产业来讲,约等于无了
    powerman
        20
    powerman  
       129 天前
    @msg7086 现在新项目 能用 rust 的很多 都在 rust 了,有一定性能要求,能忍受 GC 停顿的,都选 golang ,除了一些历史遗留,新东西以后会越来越少使用 C++
    Jooooooooo
        21
    Jooooooooo  
       129 天前
    @msg7086 注重这个没问题, 但这种事是不是应该 c 自己干而不是让开发人员关心?

    举个例子, java 有对于类中字段的重排使得内存对齐浪费更少的内存, 但这是 jvm 自动的, 普通开发人员并不需要关心. (这个东西冷门到面试八股文都很少出现
    Aspector
        22
    Aspector  
       129 天前   ❤️ 1
    我这条命都是 CppCon 里 Back to Basics 系列给的,强烈推荐。
    ligiggy
        23
    ligiggy  
    OP
       129 天前
    @Jooooooooo 看得出来,你没有写过多少 C/C++,建议秉持开放的态度,尝试拥抱一下,哈哈哈哈
    ligiggy
        24
    ligiggy  
    OP
       129 天前
    @Aspector 马上去看,哈哈哈哈,谢谢
    Jooooooooo
        25
    Jooooooooo  
       129 天前
    @ligiggy 更合理的是, 性能高的路线 /方案是自动的, 参考 java 里的 jit. 变量用不上, 自动优化掉, 方法调用没意义, 直接不调了.
    Jooooooooo
        26
    Jooooooooo  
       129 天前
    @ligiggy 上学的时候学过, 后来接触了 Java, 再也没有烫烫烫了.
    justfly
        27
    justfly  
       128 天前
    一般用来控制拷贝和移动。
    ColorfulBoar
        28
    ColorfulBoar  
       128 天前   ❤️ 4
    Effective Modern C++是写于 C++14 版本的,现在已经发生了一些变化(在使用上变化不是很大,概念上变化比较大)。cppreference 定义照抄标准而且后面全是枚举例子,好像只看它很难看懂发生了什么。CppCon 里的好像很多是教你怎么用的,没咋看过,或许用多了也能发展出一些直觉吧。

    为了理解它,如果不想在那 1800 多页的标准构成的粪坑里游泳的话,现成的材料里 C++ Templates: The Complete Guide 第二版关于 value categories 的附录可能好一点,就几页纸,概念又比较正确(不过 reference collapsing 放在正文中间的好像作者还建议第一次读跳过去的部份)。


    简单来说大概是这么理解的(这只是我的理解方式,估计肯定有某些不符合标准的地方,另外可能先跳到 4 会好一点):

    1. value categories 是表达式的属性而不是变量的属性或者别的什么玩意的属性。比如 int a = -1;这里的 a 并不是一个表达式,它不是啥左值,写在等号左边就是左值这个想法是错的。而 C++11 里为了 move semantics 等需求引入的 lvalue/rvalue reference 是两种不同的类型,它们是和表达式正交的属性,比如 void push_back(string&& x);里面的 x 的类型是右值引用 string&&,但函数定义里面如果直接写一个表达式 x 的话则是一个 glvalue 的表达式。

    2. 自 C++17 起表达式分为两类:glvalue 和 prvalue ,前者提供某个位置信息,后者提供初始化或者修改的时候所需的值。从这个版本起只需要以上二者就可以理解我们需要的东西,不再需要另外定义 lvalue 和 rvalue 了。乍看之下存在一些显然有问题的地方,比如 int a = b;里面的 b 既提供了 b 这个变量的位置又提供了初始化 a 所需要的值。实际上 C++里存在以下两种 value categories 之间的直接转换(具体行为受到类型影响,但大致上符合直觉):
    ------2.1 lvalue-to-rvalue ,名字叫这个,但它实际上是把一个 glvalue 转换成一个 prvalue (正如前面所说,我们已经不需要 lvalue 和 rvalue 这两种 value categories 了),这个 prvalue 在用来提供值的时候会从转换成它的 glvalue 提供的位置信息里面来获取相应的值(不同的类型的具体行为不太一样,但大致上是符合直觉的)。比如之前提到的 int a = b;里面的 b 就是先转换成了 prvalue 才能提供用来初始化 a 的值的。
    ------2.2 temporary materialization conversion 机制允许通过一个 prvalue 生成一个临时对象然后把它的位置作为一个 glvalue 放在好像需要 glvalue 的地方,用这种机制可以在需要 const lvalue reference 的地方传进去一个 prvalue 。但这种机制发挥的场合是受限的,直觉上只有找不到 glvalue 让这个 prvalue 起到初始化的作用的时候才会发生这种转换。比如 string a = string(string(string()));里内层表达式是 prvalue ,它会一直往外抛,直到找到 glvalue a 后进行一次初始化,C++17 里利用这个特性实现了所谓的 guaranteed copy elision 。这种特性生成的 glvalue 表达式不能用在=的左边,也不能用来初始化 non-const lvalue reference ,由此我们在 glvalue 中分出一个新的子类 xvalue ,它只通过屈指可数的几种情况生成,感觉上更像是一种技术手段。

    3. 作为 type 的 lvalue 和 rvalue reference 是与表达式的 value categories 正交的性质。但二者存在下面的相互作用:
    ------3.1 lvalue reference 只能被 glvalue 里面不是 xvalue 的那一类所初始化(这一类叫作 lvalue ),而 rvalue reference 只能被剩下的表达式初始化(即 xvalue 与 prvalue ,这一类叫作 rvalue ),并且不接受从 glvalue 转化来的 prvalue 。如果试图扔掉 lvalue/rvalue 的话,大概可以说 l/rvalue reference 只能被本来是 glvalue/prvalue 的表达式初始化,这么说看起来更简单,但我不知道能不能严格地定义。
    ------3.2 函数返回值的分类,或者说由调用这个函数构成的表达式的 value category ,受到返回值类型的影响:返回值为左值引用给出 lvalue ,右值引用给出 xvalue ,非引用类型给出 prvalue 。如果试图扔掉 lvalue/rvalue 的话大概思路和 3.1 一样沿着返回路径回溯看看源头是什么(碰到引用的时候看是谁初始化的引用类型),同样我也不知道能不能严格定义这种偷鸡理解。
    ------3.3 存在向 rvalue reference 的强制转换 std::move ,以及利用 reference collapsing (引用的引用会变成一个单层引用,左引用会传染下去)机制做的所谓 universal reference 与 perfect forwarding
    ------3.4 借由这种相互作用,我们可以通过表达式本身匹配上不同的函数调用 copy/move 相关的重载实现 move semantics ,也可以通过包含函数的表达式本质上的 value category 可以一直追溯到产生那个值的地方这个特性来实现 guaranteed copy elision 。

    4. 总的来说细节很多,并且散布在标准的各处,它们之间还有相互作用,我已经看吐了(虽然吐过之后好像不怕读标准了),所以在不出意外的时候我打算用这个简化的理解:表达式分两类,一类提供广义的位置(最终用来取出来一个值或者修改一个值),一类提供广义的值(最终被某个位置吃掉),二者可以但只应该在必要的时候相互转化,毕竟从位置里取出来一个值或者用某些值创建一个临时对象是有代价的,我们尽量晚地做这件事,说不好可以少干一点或者干脆不用干了。左 /右引用是类型而不是表达式分类,但它们大致上可以通过追溯源头来反应表达式是哪类的:当源头是位置的时候引用的类型是左值引用,是值的时候引用的类型是右值引用,其中左值引用具有传染性,除非被某种转换(比如 std::move )截断。我们利用这个机制来实现 Modern C++里面的诸多特性。剩下的都是技术细节。


    题外话:我一直很好奇 2022 年咋还有 C 和 C++都分不清楚的……
    msg7086
        29
    msg7086  
       128 天前
    @Jooooooooo
    编译器已经干了很多了,但还是比不过人类的手调优化。
    当然,更细致的优化还是要用汇编来手调。

    你说的那些变量用不上自动优化掉这些,C 家族编译器不知道多少年前就已经实现了。
    现在的编译器已经在做远远强于你说的这些事情了。

    比如之前我有一个项目,用 intrinsics 写 SIMD 汇编,用 clang 编译以后速度快得惊人,比其他编译器都要快。
    后来我去仔细检查了 clang 生成的汇编代码,发现编译器直接把我写的汇编指令等价重写成了另一批指令集,而那些指令集运行速度要比我用的指令集快不少。现在的编译器开关开得多了以后,会更激进地帮你重写代码。比如说循环里顺序读写内存的指令,会被自动矢量化成 SIMD ;比如为了减少跳转,而把短小的循环 unroll 展开;等等各种。

    然后在这之上,专业的优化人员可以通过检查 CPU 核心的状态,再去微调和重排指令。
    比如说根据 CPU 执行流水线上某个指令周期是否在摸鱼,而把某几条顺序无关的 CPU 指令往前或者往后移动等等。当然这个在比较新的 CPU 上可能也没有什么用了,现代 CPU 都会重新译码成 uop 然后在内部重排。

    左值右值的概念也会随着编译器的进步而逐渐淡出人们的视线吧。提到这些概念的地方大多也是编译器或者公理规范这些东西,只是使用的话,一把梭随便搞,一般不太会翻车的。

    PS: 和 C/汇编比,Java 是真的慢……
    3dwelcome
        30
    3dwelcome  
       128 天前
    右值就是 std::move 和&&,可能写游戏的,才会比较重视性能优化。

    我这种写前端逻辑的,性能真的是无关紧要。

    把代码整体逻辑清晰化呈现,写便于维护的代码,才是首先要考虑的。
    ericgui
        31
    ericgui  
       128 天前
    能被赋值的就是左值

    比如数字 5 ,只能是右值,赋值给 x ,就是 x=5

    但 x * 4 = 20 这也不行,不能类似解方程,你只能 x=5 ,不能 x*4= 20

    个人理解,轻拍
    ipwx
        32
    ipwx  
       128 天前
    楼上( @statumer )已经开喷 C++ 要关心左值右值,是为了解决 s = s1 + s2 这个 = 如何实现的问题。在别的语言这
    种问题都不存在。
    ====

    我倒也不是黑这句话,我只是想补充一点:C++ 为啥要关心 = 如何实现?

    是因为 C++ 没有垃圾回收。

    没有垃圾回收就要求区分栈对象和堆对象。而栈对象的赋值传值就会带来一系列拷贝的问题。而拷贝就会关心 =,就会关心左值右值。

    你看别的不用关心这些的语言,哪个没有垃圾回收?

    但是没有垃圾回收是罪过吗?不是,因为有些地方就是忌讳垃圾回收。

    所以,这只是取舍罢了。
    ipwx
        33
    ipwx  
       128 天前
    比如系统响应要求延迟稳定在 10 微秒以下,你敢用垃圾回收吗?对你不敢。

    哪怕号称最先进的 zgc ,java 里面的那个,现在也不过号称稳定版 STW 回收 <10ms ,努力向着 1ms 进发。这和 10 微秒可还相差 2 个数量级呢 hhh
    ipwx
        34
    ipwx  
       128 天前
    哦对顺便 Go 的 STW 是 1ms 。但是这和 10 微秒依然差了 2 个数量级。

    C++ 在延迟关键的领域就是无可替代,唯一的候选人只有 Rust 。但是 Rust ,ummm ,我感觉这东西的心智负担 tm 比 C++ 还要高。
    amiwrong123
        35
    amiwrong123  
       128 天前 via Android
    所以楼主觉得哪个文档比较通俗易懂😂
    jones2000
        36
    jones2000  
       128 天前
    大学“编译原理”都有的。转换成 AST 以后,递归执行 AST 每个节点的计算,全局变量表, 临时变量表...... 计算机专业都应该学过的。
    GeruzoniAnsasu
        37
    GeruzoniAnsasu  
       128 天前
    @Jooooooooo 歪个楼,你以为的高性能是在减少冷分支……实际上写「高性能代码」的人都已经在 probe CPU 的 register file 有多大了
    chzmwfg
        38
    chzmwfg  
       102 天前
    @jones2000 只是很难结合起来去思考,如果能够都结合起来,就很好了
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2525 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 02:54 · PVG 10:54 · LAX 19:54 · JFK 22:54
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.