03 | 右值和移动究竟解决了什么问题?
03 | 右值和移动究竟解决了什么问题?
讲述:吴咏炜
时长21:46大小14.93M
值分左右
生命周期和表达式类型
移动的意义
如何实现移动?
不要返回本地变量的引用
引用坍缩和完美转发
内容小结
课后思考
参考资料
赞 24
提建议
精选留言(83)
- 泰伦卢2019-12-02一直有个问题想问老师,老师第一节也说了一些,但我还是有一些疑惑和焦虑希望老师能够解惑。 现在C++的应用范围貌似越来越窄,以前可能很多后台会用到,但是现在貌似后台都是go和java多一些,真正追求性能极致的貌似不多,现在也就一些人工智能方面和嵌入式方面会用到,但也是少数,真正要做一个深度学习框架的也不太多,即便是大企业也是个别几个部门,作为一个C++程序员比较迷茫,感觉这条路比较窄,未来的路怎么走呢?展开
作者回复: 把C++当作一种工具,也当作一种锻炼思维的方式,不要把它当成非用不可的圣器。把自己定位成“程序员”,而不是“C++程序员”。职业的将来方向定位成“软件架构师”、“开发主管”、“CTO”之类的角色,而不是“高级C++程序员”。 该用其他语言的时候用其他语言。要看到,即使不用C++,C++程序员在对优化的理解、对内存管理的理解等方面都是具有很大优势的。
共 5 条评论97 - 中年男子2019-12-03第二题: 左值和右值都有效是因为构造参数时,如果是左值,就用拷贝构造构造函数,右值就用移动构造函数 无论是左值还是右值,构造参数时直接生成新的智能指针,因此不需要判断
作者回复: 理解满分。👍
共 3 条评论45 - 宝林2019-12-05这一节看了三遍,思路太跳跃了,不易读共 2 条评论32
- NEVER SETTLE2019-12-03老师,我这个初学者看的比较慢,目前只看了右值与右值引用,下面是我总结了的学习心得,请您指点下: **背景: 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的临时变量 ``` > 编译出错: 每行代码报错:表达式必须为左值或函数指示符 因为以上表达式都返回的是“临时变量”,是不可以取地址的 ---展开
作者回复: 你在学校的时候,都是让老师来看你的笔记记得好不好的吗?😂 对不起,如果有明确的问题,我可以回答。否则,我只能暂时忽略了。
共 5 条评论23 - NEVER SETTLE2019-12-02请教下老师,字符串字面量是左值,是不是在C++中 字符串其实是const char[N],其实是个常量表达式,在内存中有明确的地址。
作者回复: 是。👍
共 2 条评论23 - 禾桃2019-12-03"请查看一下标准函数模板 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)。 肯定还有别的需要考量的地方,请指正。 谢谢!展开
作者回复: 对,最主要就是这点,用完美转发来正确调用构造函数。
共 5 条评论19 - NEVER SETTLE2019-12-03老师,留言有字数限制,我是接着上个留言来的,上面那个总结了下左值与右值,这个是右值引用的学习心得: ## 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 虽然是右值引用类型,但它是一个左值,可以正常对它取地址** (如上例所示)展开
作者回复: 再多说一句,如果每个人都这么让我来看笔记的话,我是不可能满足所有人的。只看你的,也对别人不公。在这儿回答(不重复的)问题则不同,问题一般是有共性的,回答之后,大家都能看到,都能从中受益。
共 6 条评论17 - doge2021-02-22我感觉我有点理解完美转发的意思了,对于一个函数,如果形参是右值引用,但在函数体内,这个“右值引用”实际上是一个左值变量,然后函数内再有一个函数传入这个参数,那么就会调用对应的左值引用版本,而完美转发的意义就相当于做一次类型转换,让这个参数保持一开始传入时的左值右值类别。 不知道理解的对不对?
作者回复: 对。
14 - 小奶狗2020-05-21老师,为什么对临时对象不能使用取地址符&,比如&shape(),我知道这会编译报错:不能对右值取地址。我困惑的是:既然有对象,肯定有地址存放嘛,那一定能取地址才对。c++为什么要这样设计?或者说从堆栈的使用机制上看是为啥?
作者回复: 因为危险。临时对象在当前语句执行完成之后就被析构了。你握着这个已经不存在的对象的指针,想干嘛?
11 - NEVER SETTLE2019-12-02“返回左值引用的表达式,,如 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。
11 - 糖2019-12-02又是看不懂的一节。。。老师讲的课程太深刻了。。。 1. 本来感觉自己还比较了解左右值的区别,但是,文中提到:一个 lvalue 是通常可以放在等号左边的表达式,左值,然后下面说:字符串字面量如 "hello world",但字符串字面量貌似不可以放到等号左边,搞晕了。 2. 内存访问的局域性是指什么呢?又有何优势呢?老师能提供介绍的链接吗 3. 为何对于移动构造函数来讲不抛出异常尤其重要呢? 希望老师能指点一下展开
作者回复: 1. “通常”。字符串字面量是个继承自C的特殊情况。 2. 这个搜索一下就行。这是CPU的缓存结构决定的。 3. 其他类,尤其容器类,会期待移动构造函数无异常,甚至会在它有异常时选择拷贝构造函数,以保证强异常安全性。
共 5 条评论11 - 安静的雨2019-12-03Obj simple_with_move() { Obj obj; // move 会禁止 NRVO return std::move(obj); } move后不是类型转换到右值引用了吗? 为啥返回值类型还是obj?展开
作者回复: 文中已经说了,禁止返回本地对象的引用。 需要生成一个 Obj,给了一个 Obj&&,不就是调用构造函数而已么。所以(看文中输出),就是多产生了一次Obj(Obj&&) 的调用。
共 4 条评论9 - zhengfan2020-07-16吴老师,不好意思我又来了…… 您给出了例子: class A { B b_; C c_;}; 其实是长久以来我对移动构造的困惑:如果一个A这样的类,他的成员B, C都是通过值类型定义的,并且嵌套地,B、C成员也都是值类型定义的,一直到最终的原生类型。毫无疑问这样的内存布局是最符合temporal and spatial locality的,但是我非常困惑于如何实现一个A的高效率移动构造。除非“所谓的移动构造是一个triky的语法糖, 他事实上等价于容器的emplace”。展开
作者回复: 右值引用只是一种重载规则。如果 B 和 C 是内部不使用指针的值类型的大对象,那 A、B、C 的移动构造是不会比拷贝构造更高效的。 你去看一下标准容器的拷贝构造和移动构造就知道,移动的高效在于右值标明了这个对象马上就没人用了,允许移动构造函数通过调整指针把内容全部偷走而已。
共 2 条评论8 - 千鲤湖2019-12-04老师,我把实例稍微改了下, 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&&)作为参数类型才会发生拷贝或移动构造。
8 - englefly2020-03-06一开始我以为左值右值在内存表示上有什么不同,后来才明白内存表示上没有不同。这么做是为了让程序员告诉编译器,我这里可以传地址,不要把内容copy了。
作者回复: 接近。但不是“可以传地址”,而是:这个值回头马上没人要了,你可以把内容全移走。另外,不管对这个对象做了什么操作,对象还是必须处于可析构的状态。
6 - 哇咔咔2019-12-11老师你好,这段代码压测下来,发现左值引用没有性能的提升。压测时间对比是: 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 开头作为变量,然后后面使用这个变量。
共 8 条评论6 - 布拉姆2021-03-13自我赋值在以下情况下可能会出问题:this内的指针成员变量pb和rhs的pb指向的是同一块堆内存。如果先delete pb;后new Obj(*rhs.pb),就会把this和rhs指向的同一内存预先释放,那么operator=执行后pb变成了野指针。 swap同时能处理异常安全和自我赋值问题。 swap达成的效果和如下语句一样: Resource* pbachup = pb; pb = new Obj(*rhs.pb); delete pbachup; 即使new所在语句抛出异常,pb也不会失效;即使this.pb和rhs.pb指向同一内存,由于pb率先指向了另一内存,把this.pb删了了也没事。 而这边之所以可以rhs.swap(*this), 是因为operator=(obj)内参数是以值传递的,以值传递或导致copy一份临时对象,而这个临时对象和上面代码pbachup是一个道理。展开
作者回复: 对,很好!
4 - wsh2021-01-14吴老师,我想请教一个问题,类中的成员用值还是指针是不是主要看两个类之间的关系,如果是一对一,那就直接用值,如果是一对多,那就只能用指针?
作者回复: 一对一也可以用指针,比如你可能中间需要替换成另外一个值,而另外一个值在另外一个地方已经构造出来了,等等。当然,现在推荐用智能指针而不是裸指针了。 如果多个对象使用另外一个对象,那取决于使用的方式,可以选择裸指针、智能指针和引用中的一种。裸指针和引用适用于当前对象不管理那个对象的生命周期的情况。如果可能改变指向的对象或可能为空,那需要使用指针;反之,如果这个对象的存在过程中,永远指向另外一个对象,不会变化也不可能为空,那用引用更好。如果多个对象中有一个对象管理另一个对象的生命周期,那建议使用unique_ptr;如果多个对象共同管理另一个对象的生命周期,在最后一个消失后再销毁那另一个对象,则应使用shared_ptr。
共 2 条评论3 - 尹登丽2020-03-20老师 两个问题请教下 1. 左值也包含xvalue,这个没理解到 2. std::move如何使用呀,因为感觉讲了好多是std::move不好的地方,所以有点懵
作者回复: 1. 左值(lvalue)不包含xvalue;glvalue才包含xvalue。 2. 当你打算用一个左值去调用某个需要右值引用的函数,并且函数返回后你不会再去使用左值原本包含的内容(因为可能已经被移走了),就使用std::move。使用move是一种优化。比如对于smart_ptr,用move传递给其他函数,可能可以省去一次不必要的add_count和一次不必要的reduce_count。 我没有只是说了一些std::move可能被误用的地方。它本身没什么不好。
3 - 小鱼仙倌2022-04-29好亏呀,今天面试就问了我这个移动,所以我应该怎么给面试官介绍这个移动:从那些方面去聊,聊什么,为什么要有这个移动
作者回复: 仔细理解这一讲,自然就能回答了。有特别问题的话,可以留言。
共 2 条评论2