10 | 到底应不应该返回对象?
10 | 到底应不应该返回对象?
讲述:吴咏炜
时长10:27大小7.16M
F.20
调用者负责管理内存,接口负责生成
接口负责对象的堆上生成和内存管理
接口直接返回对象
如何返回一个对象?
返回值优化(拷贝消除)
回到 F.20
内容小结
课后思考
参考资料
赞 4
提建议
精选留言(23)
- 小一日一2019-12-18我认为老师应该讲一下NRVO/RVO与std::move()的区别,这个问题曾经困扰过我,从stackoverflow的问题来看,学习c++11时大多数人都思考过这个问题:https://stackoverflow.com/questions/4986673/c11-rvalues-and-move-semantics-confusion-return-statement
作者回复: 简单来说,在对本地变量进行返回时,不用 std::move。实际上,我在第 3 讲就写了: “有一种常见的 C++ 编程错误,是在函数里返回一个本地对象的引用。由于在函数结束时本地对象即被销毁,返回一个指向本地对象的引用属于未定义行为。理论上来说,程序出任何奇怪的行为都是正常的。 “在 C++11 之前,返回一个本地对象意味着这个对象会被拷贝,除非编译器发现可以做返回值优化(named return value optimization,或 NRVO),能把对象直接构造到调用者的栈上。从 C++11 开始,返回值优化仍可以发生,但在没有返回值优化的情况下,编译器将试图把本地对象移动出去,而不是拷贝出去。这一行为不需要程序员手工用 `std::move` 进行干预——使用`std::move` 对于移动行为没有帮助,反而会影响返回值优化。”
共 4 条评论46 - 小白兔纸白又白2019-12-31请问有何方法可以测试编译器是否为一个类提供了右值引用的版本的成员函数
作者回复: 好问题。利用第 14 讲的 SFINAE 技巧,是可以写出来的。比如,你希望检测是不是有下面第一个重载: class Obj { public: void foo() &&; void foo() const &; }; 可以定义下面这样的 type trait: template <typename T, typename = void_t<>> struct has_rvalue_ref_foo : false_type {}; template <typename T> struct has_rvalue_ref_foo< T, void_t<decltype(static_cast<void (T::*)() &&>(&T::foo))>> : true_type {}; 上面第一个 foo 的重载在的话,使用 has_rvalue_ref_foo<Obj>::value 就能得到编译期常量 true。
10 - Milittle2020-02-25Armadillo这个库我用过,挺好用。语法可以和Matlab互转,如果有用Matlab的小伙伴 可以推荐使用 性能杠杠的
作者回复: 对,好用非常重要!
7 - 空气2020-01-04我在工作中使用引用出参的场景之一是同时返回多个对象,如果使用返回值就要封装很多不同结构体。请问老师这种场景建议怎么实现?
作者回复: 如果都是返回而非修改的话,可以使用 pair、tuple、tie 和第 8 讲讨论的结构化绑定。
5 - 木瓜7772019-12-18项目中一直使用您说的老方法,目前看编译器有优化的话,后面会逐步考虑采用返回对象的方法! 有个问题问下,如果要返回空对象,该如何做? 是直接采用空的构造函数?
作者回复: 用默认构造函数代表空,或者用 optional<对象> (不构造)代表空,或者抛异常代表不正常(视是否不正常而定)。 optional 会在第 22 讲里讨论。
6 - 泰伦卢2019-12-18请问老师这个C++20什么时候发布编译器之类的啊?还是说已经有了?
作者回复: 看这个页面吧: https://en.cppreference.com/w/cpp/compiler_support 目前 GCC 领先一些(可以用 -std=c++2a 启用 20 的功能),但还没有哪家完整支持 C++20。
共 3 条评论4 - 阿白2021-11-03老师看到你和别的同学讨论的问题,return :?中:?表达式返回的是左值引用,所以调用的是拷贝构造函数。但是return a1,a1是个标识表达式是个左值为什么就是调用移动构造。我不明白在函数声明返回值为值类型,但是:?返回一个左值引用的时候为什么调用的是拷贝构造? A getA_duang() { A a1; A a2; return (rand() > 42 ? a1 : a2); } Create A Create A Copy A Destroy A Destroy A Destroy A auto a = getA_duang();展开
作者回复: 函数返回一个本地变量是有特殊规则的。请在 cppreference 上查阅 return 和 copy elision 的文档。 可以类比一下 decltype(规则还是不一样的)来获得一些直观的感受。如果你有 int a; 和 int b; 两个变量,decltype(a) 会得到 int,decltype(a > b ? a : b) 则得到 int&。
3 - 怪兽2021-08-24吴老师,求助,我把getA_duang函数修改为: A getA_duang() { A a1; A a2; return (rand() > 42 ? a1 : a2); } 得到的结果是: Create A Create A Copy A Destroy A Destroy A Destroy A 请问为什么用条件表达式,得到的结果是 Copy A,而用你范例中的 if 语句判断就能得到 Move A呢?展开
作者回复: 因为 (rand() > 42 ? a1 : a2) 的结果是一个 A 的左值引用,return 语句返回的是左值引用,不是变量,所以就不行了。要移动需要自己加上 std::move。
4 - 怪兽2021-05-18老师,请教2个疑惑: 1. 在返回值优化(拷贝消除)小节里,A(const A&) = delete;了,但A(A&&)只是注释掉,不是说编译器会提供默认的移动构造函数吗?为什么getA_named也不行了?不是优先匹配移动构造吗? A getA_named() { A a; return a; } 2. 哪种情况下移动的代价高?我理解移动的代价都很低,至少比拷贝低吧?展开
作者回复: 1. 可以参考cppreference.com 网站,或者第 9 讲,里面提到,“用户如果没有自己声明拷贝构造函数、拷贝赋值函数、移动赋值函数和析构函数,编译器会隐式声明一个移动构造函数”。“= default”和“= delete”这种也算声明。 2. 移动的代价是看实现的,也就是我们例子里的的 A(A&&)。像我们的玩具例子,移动和拷贝性能就没啥区别。说移动代价高,也不是指比拷贝高,而是指拷贝代价高且没有对移动做优化(如没有移动构造函数),或对象太大,没法做移动优化(如 sizeof 达到数百个字节以上)。
2 - zhengfan2020-07-20吴老师您好。 抱歉我之前的的提问描述的不太清楚。您文中清楚地描述了semi-regular是“如何”定义的。我好奇的是,这样定义的意义是啥,对于copiable增加一个alias的意义何在?对应的regular定义是什么? 在网上仔细搜索了一下,似乎找到了源头: https://www.slideshare.net/ilio-catallo/regular-types-in-c 这一个ppt描述了regular的定义,以及由此引申出来的semi-regular。这里的各种定义是非常严谨的,类似于数域及其满足操作的扩展。 https://www.modernescpp.com/index.php/c-20-define-the-concept-regular-and-semiregular 这篇文章结合了C++ std20中concept介绍了一下regular和semi-regular定义的实操。其中深度使用了您介绍的SFINAE。 从中似乎隐然可以看出C++成为一个公式语言的可能性。展开
作者回复: 是,这些概念都是有深刻数学背景的。要进一步了解的话,我推荐 From Mathematics to Generic Programming,STL 之父的大作。
2 - 花晨少年2019-12-21我们继续变形一下: #include <stdlib.h> A getA_duang() { A a1; A a2; if (rand() > 42) { return a1; } else { return a2; } } int main() { auto a = getA_duang(); } 这回所有的编译器都被难倒了,输出是: Create A Create A Move A Destroy A Destroy A Destroy A ——————— 老师这个结果应该还是会有优化在的吧?如果完全没有优化应该是两个移动才对,a1或者a2移动给返回值是一次,返回值移动给a又是一次,如果真是这样,哪次被优化掉了?第二次吗展开
作者回复: C++编译器哪会做这么不必要的事……就是一次移动。如果有返回值优化的话,一次移动都不会有。
共 4 条评论2 - 光城~兴2019-12-22加入了move assignment后,默认是调用move assignment而不是copy assignment。2
- nelson2019-12-19文稿中的代码片段 ec = multiply(&temp, a, b); if (result != SUCCESS) { goto end; } result 应该是 ec吧展开
作者回复: 多谢。已修正。
1 - Cafba2022-06-05老师,想请教一下这里出现两次构造,但析构了三次的原因,我的思考是对于auto a = getA_duang(); 函数体内两个临时对象生成与析构对应着两次,当返回时因为移动构造,不需要拷贝,直接将一个临时对象的所有权转移给目标a(且此时保证着临时对象仍然处于可析构状态),此时这个目标a不需要构造函数初始化,但仍然析构,因此少了构造的一次析构了三次,这样理解哪里有问题 Create A Create A Move A Destroy A Destroy A Destroy A展开
作者回复: Move 和 Copy 也意味着构造,分别调用了移动构造函数和拷贝构造函数。不能说“不需要构造函数初始化”。
1 - 王旧业2021-02-16在 C++14 及之前确实是这样的。但从 C++17 开始,对于类似于 getA_unnamed 这样的情况,即使对象不可拷贝、不可移动,这个对象仍然是可以被返回的 ----这样的情况就是指能被返回值优化的吗?
作者回复: 不只是能,而是必须。
- 王旧业2021-02-16在 C++14 及之前确实是这样的。但从 C++17 开始,对于类似于 getA_unnamed 这样的情况,即使对象不可拷贝、不可移动,这个对象仍然是可以被返回的
- zhengfan2020-07-19吴老师您好。 1. 您能否介绍一下定义所谓“半正则对象”的意义? 2. 我测试了一下您在介绍移动一章提到的使用 auto&& 延长prvalue生命周期的方法: auto&& obj = get_obj(); 发现其和使用auto获得这个prvalue的代码是完全等价的: Obj obj = get_obj(); 这种等价不仅限于在获得Obj实例之后的使用范围和方式完全一样,其在get_obj()内部对Obj的构造和传递(无论是否激活NRVO)也完全一样。当然在汇编层面还是有一些不同,就是看不太懂T_T 3. 另外,我测试了一下NRVO的代码,发现了一个挺有意思的情况,在C++14下,形如 A get_A_directly() { return A{}; } 这样的代码,确实只使用了默认构造了一次,没有其它移动和复制。但是如果A不提供移动和delete掉复制构造,编译不能通过。GCC和clang都如此。“未使用但必须提供”,哈哈,好像违背了“未使用就不需要负担成本”的原则:)展开
作者回复: 1. 还是看正文。我不可能这儿说一两句就比我花力气写的正文更好。 2. 实用角度是差不多(但跟编译器优化程度有关)。有一个小区别是用 Obj&& 可以接收 Obj 的子类对象。用 Obj 则会发生对象切割,不行。 3. C++17 的一条新规则是,在你说的情况下编译器必须进行返回值优化,不要求对象可拷贝或移动。之前的规则是不管编译器能不能优化,语言认为这儿发生了对象拷贝或移动。
- 风清扬2020-02-09关于返回值优化的实验我们就做到这里。下一步,我们试验一下把移动构造函数删除: A(A&&)=delete 我们可以立即看到“Copy A”出现在了结果输出中,说明目前结果变成拷贝构造了。 这里我修改成A(A&&) = delete后,编译报错:提示:error: use of deleted function ‘A::A(A&&)展开
作者回复: 没有启用 C++17 标准吧? 应该是上面这种情况。不是的话,请描述具体的编译器版本和编译的命令行参数。
共 4 条评论 - 传说中的成大大2020-01-04#include <iostream> 2 using namespace std; 3 // Can copy and move 4 class A { 5 public: 6 A() { 7 cout << "Create A\n"; 8 } 9 ~A() { 10 cout << "Destroy A\n"; 11 } 12 A(const A&) { 13 cout << "Copy A\n"; 14 } 15 A(A&&) { cout << "Move A\n"; } 16 }; 17 A getA_unnamed(){ 18 // return A(); 19 A a; 20 return a; 21 } 22 int main(){ 23 auto a = getA_unnamed(); 24 } g++ main.cpp ./a.out 也不会执行copy操作呢?我的操作系统是ubuntu展开
作者回复: 你这不就是我下面给出的 getA_named 吗?确实不会拷贝啊,我这一讲就是这么写的。就是让你看到,返回值优化在这儿起的作用。
- petit_kayak2019-12-24一直使用共享指针,非常喜欢这些新的优化,能简化非常多代码,但现实是需要考虑很多无法升级的旧环境,不能随便使用c++11及以后的写法
作者回复: 先试试升级环境、充分测试看看有没有问题。也许没问题呢?