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

10 | 到底应不应该返回对象?

拷贝消除
优点
示例代码
问题
示例代码
问题
示例代码
性能影响
可读性
返回值 vs 输出参数
例外情况
返回值优化
可移动构造/赋值
接口直接返回对象
接口负责对象的堆上生成和内存管理
调用者负责管理内存
F.20 C++ 核心指南
Copy elision
Armadillo
C++ core guidelines
课后思考
内容小结
如何返回一个对象
不同的做法
为什么要返回对象
参考资料
返回对象问题

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

你好,我是吴咏炜。
前几讲里我们已经约略地提到了返回对象的问题,本讲里我们进一步展开这个话题,把返回对象这个问题讲深讲透。

F.20

《C++ 核心指南》的 F.20 这一条款是这么说的 [1]
F.20: For “out” output values, prefer return values to output parameters
翻译一下:
在函数输出数值时,尽量使用返回值而非输出参数
这条可能会让一些 C++ 老手感到惊讶——在 C++11 之前的实践里,我们完全是采用相反的做法的啊!
在解释 F.20 之前,我们先来看看我们之前的做法。

调用者负责管理内存,接口负责生成

一种常见的做法是,接口的调用者负责分配一个对象所需的内存并负责其生命周期,接口负责生成或修改该对象。这种做法意味着对象可以默认构造(甚至只是一个结构),代码一般使用错误码而非异常。
示例代码如下:
MyObj obj;
ec = initialize(&obj);
这种做法和 C 是兼容的,很多程序员出于惯性也沿用了 C 的这种做法。一种略为 C++ 点的做法是使用引用代替指针,这样在上面的示例中就不需要使用 & 运算符了;但这样只是语法略有区别,本质完全相同。如果对象有合理的析构函数的话,那这种做法的主要问题是啰嗦、难于组合。你需要写更多的代码行,使用更多的中间变量,也就更容易犯错误。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

C++中返回对象的问题一直备受关注。本文首先介绍了传统的C风格做法,即调用者负责管理内存,接口负责生成对象,并分析了这种做法的不足之处。接着提出了另一种可能的做法,即接口负责对象的堆上生成和内存管理,但同样存在繁琐的问题。最后,作者提出了直接返回对象的做法,并通过实际代码展示了这种做法的简洁性和性能优势。文章还介绍了返回对象的构造、赋值和移动支持。通过对比不同的做法,强调了直接返回对象的简洁性和性能优势。文章还讨论了返回值优化的相关内容,以及C++17对返回对象的优化。总的来说,本文为读者提供了在C++中返回对象时的参考和建议,强调了直接返回对象的简洁性和性能优势。

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

全部留言(25)

  • 最新
  • 精选
  • 小一日一
    我认为老师应该讲一下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` 对于移动行为没有帮助,反而会影响返回值优化。”

    2019-12-18
    5
    50
  • 小白兔纸白又白
    请问有何方法可以测试编译器是否为一个类提供了右值引用的版本的成员函数

    作者回复: 好问题。利用第 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。

    2019-12-31
    2
    13
  • 怪兽
    吴老师,求助,我把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。

    2021-08-24
    7
  • Milittle
    Armadillo这个库我用过,挺好用。语法可以和Matlab互转,如果有用Matlab的小伙伴 可以推荐使用 性能杠杠的

    作者回复: 对,好用非常重要!

    2020-02-25
    7
  • 阿白
    老师看到你和别的同学讨论的问题,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&。

    2021-11-03
    6
  • 空气
    我在工作中使用引用出参的场景之一是同时返回多个对象,如果使用返回值就要封装很多不同结构体。请问老师这种场景建议怎么实现?

    作者回复: 如果都是返回而非修改的话,可以使用 pair、tuple、tie 和第 8 讲讨论的结构化绑定。

    2020-01-04
    6
  • 木瓜777
    项目中一直使用您说的老方法,目前看编译器有优化的话,后面会逐步考虑采用返回对象的方法! 有个问题问下,如果要返回空对象,该如何做? 是直接采用空的构造函数?

    作者回复: 用默认构造函数代表空,或者用 optional<对象> (不构造)代表空,或者抛异常代表不正常(视是否不正常而定)。 optional 会在第 22 讲里讨论。

    2019-12-18
    6
  • 泰伦卢
    请问老师这个C++20什么时候发布编译器之类的啊?还是说已经有了?

    作者回复: 看这个页面吧: https://en.cppreference.com/w/cpp/compiler_support 目前 GCC 领先一些(可以用 -std=c++2a 启用 20 的功能),但还没有哪家完整支持 C++20。

    2019-12-18
    3
    4
  • 怪兽
    老师,请教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 达到数百个字节以上)。

    2021-05-18
    3
  • 花晨少年
    我们继续变形一下: #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++编译器哪会做这么不必要的事……就是一次移动。如果有返回值优化的话,一次移动都不会有。

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