现代C++实战30讲
吴咏炜
前 Intel 资深软件架构师
立即订阅
3691 人已学习
课程目录
已更新 14 讲 / 共 30 讲
0/4登录后,你可以任选4讲全文学习。
课前必读 (2讲)
开篇词 | C++这么难,为什么我们还要用C++?
免费
课前必读 | 有关术语发音及环境要求
基础篇 (9讲)
01 | 堆、栈、RAII:C++里该如何管理资源?
02 | 自己动手,实现C++的智能指针
03 | 右值和移动究竟解决了什么问题?
04 | 容器汇编 I:比较简单的若干容器
05 | 容器汇编 II:需要函数对象的容器
06 | 异常:用还是不用,这是个问题
07 | 迭代器和好用的新for循环
08 | 易用性改进 I:自动类型推断和初始化
09 | 易用性改进 II:字面量、静态断言和成员函数说明符
提高篇 (3讲)
10 | 到底应不应该返回对象?
11 | Unicode:进入多文字支持的世界
12 | 编译期多态:泛型编程和模板入门
现代C++实战30讲
登录|注册

03 | 右值和移动究竟解决了什么问题?

吴咏炜 2019-12-02
你好,我是吴咏炜。
从上一讲智能指针开始,我们已经或多或少接触了移动语义。本讲我们就完整地讨论一下移动语义和相关的概念。移动语义是 C++11 里引入的一个重要概念;理解这个概念,是理解很多现代 C++ 里的优化的基础。

值分左右

我们常常会说,C++ 里有左值和右值。这话不完全对。标准里的定义实际更复杂,规定了下面这些值类别(value categories):
我们先理解一下这些名词的字面含义:
一个 lvalue 是通常可以放在等号左边的表达式,左值
一个 rvalue 是通常只能放在等号右边的表达式,右值
一个 glvalue 是 generalized lvalue,广义左值
一个 xvalue 是 expiring lvalue,将亡值
一个 prvalue 是 pure rvalue,纯右值
还是有点晕,是吧?我们暂且抛开这些概念,只看其中两个:lvalue 和 prvalue。
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
变量、函数或数据成员的名字
返回左值引用的表达式,如 ++xx = 1cout << ' '
字符串字面量如 "hello world"
在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《现代C++实战30讲》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(28)

  • hello world
    一直有个问题想问老师,老师第一节也说了一些,但我还是有一些疑惑和焦虑希望老师能够解惑。
    现在C++的应用范围貌似越来越窄,以前可能很多后台会用到,但是现在貌似后台都是go和java多一些,真正追求性能极致的貌似不多,现在也就一些人工智能方面和嵌入式方面会用到,但也是少数,真正要做一个深度学习框架的也不太多,即便是大企业也是个别几个部门,作为一个C++程序员比较迷茫,感觉这条路比较窄,未来的路怎么走呢?

    作者回复: 把C++当作一种工具,也当作一种锻炼思维的方式,不要把它当成非用不可的圣器。把自己定位成“程序员”,而不是“C++程序员”。职业的将来方向定位成“软件架构师”、“开发主管”、“CTO”之类的角色,而不是“高级C++程序员”。

    该用其他语言的时候用其他语言。要看到,即使不用C++,C++程序员在对优化的理解、对内存管理的理解等方面都是具有很大优势的。

    2019-12-02
    2
    13
  • NEVER SETTLE
    请教下老师,字符串字面量是左值,是不是在C++中 字符串其实是const char[N],其实是个常量表达式,在内存中有明确的地址。

    作者回复: 是。👍

    2019-12-02
    10
  • 中年男子
    第二题:
    左值和右值都有效是因为构造参数时,如果是左值,就用拷贝构造构造函数,右值就用移动构造函数
    无论是左值还是右值,构造参数时直接生成新的智能指针,因此不需要判断

    作者回复: 理解满分。👍

    2019-12-03
    7
  • 又是看不懂的一节。。。老师讲的课程太深刻了。。。
    1. 本来感觉自己还比较了解左右值的区别,但是,文中提到:一个 lvalue 是通常可以放在等号左边的表达式,左值,然后下面说:字符串字面量如 "hello world",但字符串字面量貌似不可以放到等号左边,搞晕了。
    2. 内存访问的局域性是指什么呢?又有何优势呢?老师能提供介绍的链接吗
    3. 为何对于移动构造函数来讲不抛出异常尤其重要呢?
    希望老师能指点一下

    作者回复: 1. “通常”。字符串字面量是个继承自C的特殊情况。

    2. 这个搜索一下就行。这是CPU的缓存结构决定的。

    3. 其他类,尤其容器类,会期待移动构造函数无异常,甚至会在它有异常时选择拷贝构造函数,以保证强异常安全性。

    2019-12-02
    3
    7
  • NEVER SETTLE
    老师,留言有字数限制,我是接着上个留言来的,上面那个总结了下左值与右值,这个是右值引用的学习心得:
    ## 2、右值引用
    **针对以上的所说的“临时变量”,如何来“接收”它呢?**
    * 最直白的办法,就是直接用一个变量来“接收”
    以x++为例:
    ```
    void playVal(int y) {
        cout << "y = " << y << ", (y).adrr = " << &y << endl;
    }

    int x = 0;
    playVal(x++);
    cout << "x = " << x << ", (x).adrr = " << &x << endl;
    ```

    >运行结果:
    y = 0, (y).adrr = 0x22fe20
    x = 1, (x).adrr = 0x22fe4c

    这是一个值传递过程,相当于 int y = x++,即x++生成的临时变量给变量y赋值,之后临时变量就“消失”,这里发生是一次拷贝。

    如何避免发生拷贝呢?
    通常做法是使用引用来“接收”,即引用传递。
    上面说过,使用一个(常规)引用来“接收”一个临时变量,会报错:
    ```
    void playVal(int& y)
    ```
    > error : 非常量引用的初始值必须为左值

    * 普遍的做法都是使用常量引用来“接收”临时变量(C++11之前)
    ```
    void playVal(const int& y)
    ```
    这里编译器做了处理: int tmp = x++; const int& y = tmp; 发生内存分配。
    其实还是发生了拷贝。

    * 使用右值引用来“接收”临时变量(C++11之后)
    上面说过,“临时变量”是一个右值,所以这里可以使用右值引用来“接收”它
    右值引用的形式是 T&& :
    ```
    void playVal(int&& y) {
        cout << "y = " << y << ", (y).adrr = " << &y << endl;
    }

    int x = 0;
    playVal(x++);
    cout << "x = " << x << ", (x).adrr = " << &x << endl;
    ```

    > 运行结果:
    y = 0, (y).adrr = 0x22fe4c
    x = 1, (x).adrr = 0x22fe48

    这是一个(右值)引用传递的过程,相当于 int&& y = x++,这里的右值引用 y 直接“绑定”了“临时变量”,因为它就会有了命名,变成“合法”的,就不会“消失”。
    **注意:这里的变量 y 虽然是右值引用类型,但它是一个左值,可以正常对它取地址**
    (如上例所示)

    作者回复: 再多说一句,如果每个人都这么让我来看笔记的话,我是不可能满足所有人的。只看你的,也对别人不公。在这儿回答(不重复的)问题则不同,问题一般是有共性的,回答之后,大家都能看到,都能从中受益。

    2019-12-03
    1
    4
  • 禾桃
    "请查看一下标准函数模板 make_shared 的声明,然后想一想,这个函数应该是怎样实现的。"

    template <class T, class... Args>
    std::shared_ptr<T> make_shared (Args&&... args)
    {
        T* ptr = new T(std::forward<Args...>(args...));
        return std::shared_ptr<T>(ptr);
    }

    我的考虑是:
    make_shared声明里的(Args&&...) 是universal reference, 所以在函数体里用完美转发(std::forward)把参数出入T的构造函数, 以调用每个参数各自对用的构造函数(copy or move)。

    肯定还有别的需要考量的地方,请指正。

    谢谢!

    作者回复: 对,最主要就是这点,用完美转发来正确调用构造函数。

    2019-12-03
    4
  • NEVER SETTLE
    老师,我这个初学者看的比较慢,目前只看了右值与右值引用,下面是我总结了的学习心得,请您指点下:
    **背景:
    C++11为了支持移动操作,引用了新的引用类型-右值引用。
    所谓右值引用就是绑定到右值的引用。
    为了区分右值引用,引入左值引用概念,即常规引用。
    那左值与右值是是什么?**

    ## 1、左值与右值

    **左值 lvalue 是有标识符、可以取地址的表达式**
    * 变量、函数或数据成员的名字
    * 返回左值引用的表达式,如 ++x、x = 1、cout << ' '
    * 字符串字面量如 "hello world"

    表达式是不是左值,就看是否可以取地址,或者返回类型是否可以用(常规)引用来接收:

    ```
    int x = 0;
    cout << "(x).addr = " << &x << endl;
    cout << "(x = 1).addr = " << &(x = 1) << endl; //x赋值1,返回x
    cout << "(++x).addr = " << &++x << endl; //x自增1,返回x
    ```

    > 运行结果:
    (x).addr = 0x22fe4c
    (x = 1).addr = 0x22fe4c
    (++x).addr = 0x22fe4c

    ```
    cout << "hello world = " << &("hello world") << endl;
    ```

    > 运行结果:
    hello world = 0x40403a

    C++中的字符串字面量,可以称为字符串常量,表示为const char[N],其实是地址常量表达式。
    在内存中有明确的地址,不是临时变量。

    ```
    cout << "cout << ' ' = " << &(cout << ' ') << endl;
    ```
    > 运行结果:
    cout << ' ' = 0x6fd0acc0

    **纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般称为“临时对象”**
    * 返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
    * 除字符串字面量之外的字面量,如 42、true

    ```
    //cout << "(x++).addr = " << &x++ << endl; //返回一个值为x的临时变量,再把x自增1
    //cout << "(x + 1).addr = " << &(x + 1) << endl; //返回一个值为x+1的临时变量
    //cout << "(42).addr = " << &(42) << endl; //返回一个值为42的临时变量
    //cout << "(true).addr = " << &(true) << endl; //返回一个值为true的临时变量
    ```
    > 编译出错:
    每行代码报错:表达式必须为左值或函数指示符
    因为以上表达式都返回的是“临时变量”,是不可以取地址的

    ---

    作者回复: 你在学校的时候,都是让老师来看你的笔记记得好不好的吗?😂

    对不起,如果有明确的问题,我可以回答。否则,我只能暂时忽略了。

    2019-12-03
    3
  • NEVER SETTLE
    “返回左值引用的表达式,,如 x++、x + 1 ”不太清楚原因,后来我就试了下:

    ```
    int x = 0;
    cout << "(x).addr = " << &x << endl;
    cout << "(x = 1).addr = " << &(x = 1) << endl;
    cout << "(++x).addr = " << &++x << endl;
    //cout << "(x++).addr = " << &x++ << endl;
    ```

    > 运行结果:
    (x).addr = 0x22fe4c
    (x = 1).addr = 0x22fe4c
    (++x).addr = 0x22fe4c
    最后一行注释掉的代码报错:表达式必须为左值或函数指示符

    作者回复: 对,x = 1 和 ++x 返回的都是对 x 的 int&。x++ 则返回的是 int。

    2019-12-02
    3
  • 千鲤湖
    老师,我把实例稍微改了下,
    class Obj
      {
        public:
          Obj()
          {
              std::cout << "Obj()" << std::endl;
          }
      
          Obj(const Obj&)
          {
              std::cout << "Obj(const Obj&)" << std::endl;
          }
      
          Obj(Obj&&)
          {
              std::cout << "Obj(Obj&&)" << std::endl;
          }
      };

    void foo(const Obj&)
    void foo(Obj&&)
    void bar(const Obj& s)
    void bar(Obj&& s)

    int main()
    {
       bar(Obj());
    }

    构造函数内加了打印。

    期望看到的结果是这样的
    Obj()
    Obj(&&)
    bar(Obj&&)
    Obj(const&)
    foo(const Obj&)
    可实际输出如下
    Obj()
    bar(&&)
    foo(const &)

    并没有期望中的移动构造和复制构造,这是为什么啊。
    关于没有移动构造,我的理解是Obj()本来已经是个右值了,不必再构造。
    可是想不通为什么没有了复制构造。

    作者回复: 中间传的都是引用,没有拷贝或移动发生的。只有用Obj(而不是Obj&或Obj&&)作为参数类型才会发生拷贝或移动构造。

    2019-12-04
    2
  • 安静的雨
    Obj simple_with_move()
    {
        Obj obj;
        // move 会禁止 NRVO
        return std::move(obj);
    }

    move后不是类型转换到右值引用了吗? 为啥返回值类型还是obj?

    作者回复: 文中已经说了,禁止返回本地对象的引用。

    需要生成一个 Obj,给了一个 Obj&&,不就是调用构造函数而已么。所以(看文中输出),就是多产生了一次Obj(Obj&&) 的调用。

    2019-12-03
    1
    2
  • 哇咔咔
    老师你好,这段代码压测下来,发现左值引用没有性能的提升。压测时间对比是:
    elapsed time: 1.2184s
    elapsed time: 1.1857s

    请问为什么呢?

    #include <string>
    #include <ctime>
    #include <chrono>
    #include <iostream>

    void func1(std::string s)
    {
    }

    void func2(const std::string &s)
    {
    }

    void test2()
    {
        auto start = std::chrono::system_clock::now();
        for (size_t i = 0; i < 20000000; i++)
        {
            func1(std::string("hello"));
        }
        auto end = std::chrono::system_clock::now();
        std::chrono::duration<double> elapsed_seconds = end - start;
        std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";

        start = std::chrono::system_clock::now();
        for (size_t i = 0; i < 20000000; i++)
        {
            func2(std::string("hello"));
        }
        end = std::chrono::system_clock::now();
        elapsed_seconds = end - start;
        std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
    }

    int main()
    {
        test2();
    }

    作者回复: 因为移动发挥威力了……试试把 std::string("hello") 放到 test2 开头作为变量,然后后面使用这个变量。

    2019-12-11
    1
  • 罗乾林
    平时Java是主要使用语言,也来回答一下
    1、make_shared 创建(new)新对象根据传入的值类别调用拷贝构造或移动构造,然后将新对象的指针给shared_ptr,其中我看见了_Types&&和forward
    2、smart_ptr::operator= 中参数为值传递,会先调用smart_ptr的拷贝构造函数,生成了临时对象,然后调用swap,
    因为生成了新对象所以对等号两边是否引用同一对象进行判断,也没意义了,但是a=a也会有临时对象的产生,有性能开销

    有错误的方,望老师指正

    作者回复: 2完全正确。1再想想。🤓

    2019-12-02
    1
  • 禾桃
    “result&& r = process_shape(
      circle(), triangle());

    如果一个 prvalue 被绑定到一个引用上,它的生命周期则会延长到跟这个引用变量一样长。”


    在上面这行代码执行完后,栈指针已经不再指向这个函数(process_shape)栈,换言之,这个栈所使用的内存可以被后续代码使用。

    您的例题中貌似也没有返回值优化,如果是的话,那个prvalue(result())貌似被构建在了上面那个函数的栈内存上,这样的话,在函数栈被回收后,在函数栈上被构建的prvalue这个对象的周期是如何被延长的?难道这种情况下,函数栈没有被回收?

    谢谢!

    作者回复: process_shape 返回的是对象,不是引用。结果不管有没有返回值优化(实际是有的),都是在调用者的栈上。没有生命期延长的话,执行完 process_shape 这一句,对象就销毁了。有生命期延长,则要到 r 的生命期结束时才销毁。

    2019-12-19
  • 张岩
    int main()
    {
        A&& a = static_cast<A&&>(GetA());
        cout<<"main end"<<endl;
        a.func();
        return 0;
    }

    g++ -g -std=c++11 construct.cpp ;./a.out
    construct: 1
    main end
    func: 1
    destruct: 1

    老师您好,请问下,为什么上面的这种情况xvalue的生命周期被延长了。

    作者回复: xvalue 是有标识符的。这儿临时对象一直没有标识符,也就一直是 prvalue。

    2019-12-18
    2
  • 微秒
    老师,我对左值、右值、移动的区别很懵。可以简单的说下吗?

    作者回复: 我觉得我做不到比文中更清楚了(否则我就写进去了)。😅

    多看例子来体会一下吧……

    2019-12-13
  • 王小白白白
    老师,string拼接那里,复制指的是?

    作者回复: 内存复制,memcpy、strcpy 这样的操作。

    2019-12-10
  • 三味
    看完了目前的06讲. 感觉最不好理解的还是这03讲. 关于xvalue还是感觉有些迷糊...
    关于circle triangle shape result那一段的代码.
    1. 我看上面描述的xvalue, 通常是使用std::move被强制转换为右值的值, 这么理解对不对?
    2. 还有一个就是
        result&& r = std::move(process_shape( circle(), triangle()));
    这个r当然是个左值, 它指代的右值, 就是xvalue, 这么理解对不对?
    3. 上面的这个xvalue, 究竟是什么时候完蛋的?
    最后是从上面引申出来的问题:
    4. std::move()函数的返回值是T&&, 所以用
        T&& t = std::move(value);
    多么自然的一件事情...为什么这么自然的表达式, 结果却是栈内存的错误解引用呢... C++为啥要这么去规定?
    期待您的回复.

    作者回复: 迷糊是正常的……我也啃了几遍才基本搞通。多看几遍吧。

    1. 是。
    2. 是。
    3. 超出作用域就没了。在你的这个例子里,它的生命期跟对象的生命期没有关系。
    4. 错误解引用是因为引用超出了变量的生命期。如果没有生命期延长,一个临时对象在当前语句执行结束即被销毁。你写的不会有问题,但把value替换成get_value()一般就会了。

    2019-12-09
  • じJRisenづジ
    老师您的github 是?

    作者回复: https://github.com/adah1972/

    2019-12-05
    1
  • 宝林
    这一节看了三遍,思路太跳跃了,不易读
    2019-12-05
  • 花晨少年
    Obj simple()
    {
      Obj obj;
      // 简单返回对象;一般有 NRVO
      return obj;
    }

    auto obj1 = simple();

    请问这个不应该是会调用一次构造和拷贝构造吗,因为函数返回对象Obj是用了函数内部的obj进行拷贝构造,而返回的这个对象被直接优化给了obj1而没有调用构造函数
    那么如果没有优化应该是两次构造,一次拷贝构造才对啊,感觉

    作者回复: 返回值优化在没有开启优化编译选项时也是可能发生的。编译器看到这种形式的代码直接统一处理了。

    2019-12-05
    1
收起评论
28
返回
顶部