每周技巧 #101:返回值、引用和生命周期
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #101: Return Values, References, and Lifetimes。
原文最初作为 totw/101 发布于 2015 年 7 月 29 日。
作者:Titus Winters (titus@google.com)
考虑下面这段代码:
|
|
我特别想让你注意这里的 &。它合适吗?我们应该检查什么?可能出什么问题?我发现相当多 C++ 程序员并不完全清楚引用,但通常知道它们“避免拷贝”。和 C++ 中大多数问题一样,事情比这更复杂。
逐案分析:返回了什么,又如何存储?
这里有两个(也许三个)重要问题:
- 返回的是什么类型(在这个例子里,是
GetName()返回什么)? - 我们正在存入/初始化什么类型(在这个例子里,
name的类型是什么)? - 如果返回的是引用,被引用返回的对象是否有任何生命周期限制?
我们会继续用 string 作为示例类型,但同样的论证可以推广到大多数非平凡值类型。
- 返回
string,初始化string:这通常是 RVO,而且对现代类型来说,最坏也保证是一次移动(见 TotW 77)。 - 返回
string&或const string&,初始化string:这是一次拷贝(一定存在某个长寿命对象,我们正在返回它的引用;一旦初始化一个新 string,就有两个名字指向这些数据,因此是拷贝。见 TotW 77)。有时这很有价值,比如你需要自己的string活得比函数提供的生命周期保证更久。 - 返回
string,初始化string&:这无法编译,因为不能把引用绑定到临时对象。 - 返回
const string&,初始化string&:这无法编译,因为你不恰当地丢掉了 const。 - 返回
const string&,初始化const string&:这没有成本(实际上你只是返回一个指针)。不过,你继承了任何已有生命周期限制:这个引用能有效多久?大多数返回引用的访问器方法返回的是成员,引用最多只能在包含对象的生命周期内有效。 - 返回
string&,初始化string&:这和 #5 相同,但有额外注意点:返回引用是非 const 的,因此你对该引用的任何修改都会反映到源对象上。 - 返回
string&,初始化const string&:同 #5。 - 返回
string,初始化const string&:考虑到 #3,你可能以为这行不通。不过,语言对此有特殊支持:如果你用一个临时T初始化const T&,这个T(此处为string)直到该引用离开作用域才会被销毁(在常见的自动变量或静态变量情况下)。
场景 #8 让大多数反射式使用引用的写法能够成立(也就是“噢,我不想拷贝,所以直接赋给引用吧”,却不一定思考返回的到底是什么)。不过,因为 #1,它实际上也没有真正帮你什么:很可能一开始就不会有拷贝。更进一步,现在你的代码读者必须处理局部变量类型是 const string& 而不是 string 这件事,并因此担心底层 string 是否已经离开作用域或发生变化。
换句话说,在评审最初那段代码时,我必须担心:
GetName()是按值返回还是按引用返回?Consumer的构造函数接收string、const string&还是string_view?- 构造函数是否对该参数有任何生命周期要求?(如果它不只是
string。)
然而,如果你一开始就把 name 声明为 string,通常并不会更低效(因为 RVO 和移动语义),而且至少同样可能在对象生命周期方面安全。
另外,如果确实存在对象生命周期问题,存储为 string 时往往更容易发现:不需要观察 GetName() 返回引用的生命周期承诺和 SetName() 生命周期要求之间的相互作用;拥有自己的 string 意味着只需要看本地代码和 SetName()。
所有这些意思是:避免拷贝没问题,只要你没有让事情变得更复杂。在本来就不会发生拷贝时让代码更复杂,并不是好的权衡。