现代 C++ 编程实战
吴咏炜
前 Intel 资深软件架构师
34196 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 51 讲
加餐 (1讲)
现代 C++ 编程实战
15
15
1.0x
00:00/00:00
登录|注册

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

std::forward 的用法
转发引用的作用
临时对象的生命周期延长规则
临时对象的生命周期规则
glvalue、xvalue 和 prvalue 的概念
lvalue 和 rvalue 的定义
标记为 noexcept
实现通用的 operator=
全局 swap 函数
swap 成员函数
分开的拷贝构造和移动构造函数
在容器类的使用中更有意义
减少运行的开销
Herb Sutter, “GotW #88: A candidate for the ‘most important const’”
分析为什么 smart_ptr::operator= 对左值和右值都有效,不需要对等号两边是否引用同一对象进行判断
查看标准函数模板 make_shared 的声明,思考其实现方法
实现通用的 operator=
实现 swap 函数
实现移动构造函数和拷贝构造函数
使用 std::forward
返回值优化
使用 std::move
引用坍缩和完美转发
生命周期和表达式类型
左值和右值
实现移动的方法
移动语义的意义
介绍移动语义的概念
参考资料
课后思考
代码示例
值类别
移动语义
C++ 移动语义和值类别

该思维导图由 AI 生成,仅供参考

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

值分左右

我们常常会说,C++ 里有左值和右值。这话不完全对。标准里的定义实际更复杂,规定了下面这些值类别(value categories):
我们先理解一下这些名词的字面含义:
一个 lvalue 是通常可以放在等号左边的表达式,左值
一个 rvalue 是通常只能放在等号右边的表达式,右值
一个 glvalue 是 generalized lvalue,广义左值
一个 xvalue 是 expiring value,将亡值
一个 prvalue 是 pure rvalue,纯右值
还是有点晕,是吧?我们暂且抛开这些概念,只看其中两个:lvalue 和 prvalue。
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
变量、函数或数据成员的名字
返回左值引用的表达式,++xx = 1cout << ' '
字符串字面量如 "hello world"
在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

移动语义是C++11引入的重要概念,解决了处理左值和右值的复杂性。文章首先介绍了C++中的值类别,包括lvalue、rvalue、glvalue、xvalue和prvalue。右值引用的引入和`std::move`的作用也得到了讨论。此外,文章解释了临时对象的生命周期延长规则,以及可能导致的潜在问题。移动语义的意义在于减少运行开销,特别是在使用容器类的情况下。文章通过比较C++11之前和之后的写法,阐述了移动语义对性能的提升和代码简洁性的影响。实现移动的几个步骤,包括对象的拷贝构造和移动构造函数、`swap`成员函数的设计也得到了介绍。总的来说,本文通过具体的例子和技术细节,生动地阐述了移动语义的重要性和实现方法,对于想要提高C++代码性能和可读性的程序员具有很高的实用价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《现代 C++ 编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(85)

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

    作者回复: 理解满分。👍

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

    作者回复: 是。👍

    2019-12-02
    2
    25
  • 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
    6
    24
  • doge
    我感觉我有点理解完美转发的意思了,对于一个函数,如果形参是右值引用,但在函数体内,这个“右值引用”实际上是一个左值变量,然后函数内再有一个函数传入这个参数,那么就会调用对应的左值引用版本,而完美转发的意义就相当于做一次类型转换,让这个参数保持一开始传入时的左值右值类别。 不知道理解的对不对?

    作者回复: 对。

    2021-02-22
    22
  • 禾桃
    "请查看一下标准函数模板 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
    5
    21
  • 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
    6
    17
  • 可爱的小奶狗
    老师,为什么对临时对象不能使用取地址符&,比如&shape(),我知道这会编译报错:不能对右值取地址。我困惑的是:既然有对象,肯定有地址存放嘛,那一定能取地址才对。c++为什么要这样设计?或者说从堆栈的使用机制上看是为啥?

    作者回复: 因为危险。临时对象在当前语句执行完成之后就被析构了。你握着这个已经不存在的对象的指针,想干嘛?

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

    作者回复: 1. “通常”。字符串字面量是个继承自C的特殊情况。 2. 这个搜索一下就行。这是CPU的缓存结构决定的。 3. 其他类,尤其容器类,会期待移动构造函数无异常,甚至会在它有异常时选择拷贝构造函数,以保证强异常安全性。

    2019-12-02
    5
    11
  • 安静的雨
    Obj simple_with_move() { Obj obj; // move 会禁止 NRVO return std::move(obj); } move后不是类型转换到右值引用了吗? 为啥返回值类型还是obj?

    作者回复: 文中已经说了,禁止返回本地对象的引用。 需要生成一个 Obj,给了一个 Obj&&,不就是调用构造函数而已么。所以(看文中输出),就是多产生了一次Obj(Obj&&) 的调用。

    2019-12-03
    4
    10
收起评论
显示
设置
留言
85
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部