每周技巧 #101:返回值、引用和生命周期

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #101: Return Values, References, and Lifetimes

原文最初作为 totw/101 发布于 2015 年 7 月 29 日。

作者:Titus Winters (titus@google.com)

考虑下面这段代码:

1
2
const string& name = obj.GetName();
std::unique_ptr<Consumer> consumer(new Consumer(name));

我特别想让你注意这里的 &。它合适吗?我们应该检查什么?可能出什么问题?我发现相当多 C++ 程序员并不完全清楚引用,但通常知道它们“避免拷贝”。和 C++ 中大多数问题一样,事情比这更复杂。

逐案分析:返回了什么,又如何存储?

这里有两个(也许三个)重要问题:

  1. 返回的是什么类型(在这个例子里,是 GetName() 返回什么)?
  2. 我们正在存入/初始化什么类型(在这个例子里,name 的类型是什么)?
  3. 如果返回的是引用,被引用返回的对象是否有任何生命周期限制?

我们会继续用 string 作为示例类型,但同样的论证可以推广到大多数非平凡值类型。

  1. 返回 string,初始化 string:这通常是 RVO,而且对现代类型来说,最坏也保证是一次移动(见 TotW 77)。
  2. 返回 string&const string&,初始化 string:这是一次拷贝(一定存在某个长寿命对象,我们正在返回它的引用;一旦初始化一个新 string,就有两个名字指向这些数据,因此是拷贝。见 TotW 77)。有时这很有价值,比如你需要自己的 string 活得比函数提供的生命周期保证更久。
  3. 返回 string,初始化 string&:这无法编译,因为不能把引用绑定到临时对象。
  4. 返回 const string&,初始化 string&:这无法编译,因为你不恰当地丢掉了 const。
  5. 返回 const string&,初始化 const string&:这没有成本(实际上你只是返回一个指针)。不过,你继承了任何已有生命周期限制:这个引用能有效多久?大多数返回引用的访问器方法返回的是成员,引用最多只能在包含对象的生命周期内有效。
  6. 返回 string&,初始化 string&:这和 #5 相同,但有额外注意点:返回引用是非 const 的,因此你对该引用的任何修改都会反映到源对象上。
  7. 返回 string&,初始化 const string&:同 #5。
  8. 返回 string,初始化 const string&:考虑到 #3,你可能以为这行不通。不过,语言对此有特殊支持:如果你用一个临时 T 初始化 const T&,这个 T(此处为 string)直到该引用离开作用域才会被销毁(在常见的自动变量或静态变量情况下)。

场景 #8 让大多数反射式使用引用的写法能够成立(也就是“噢,我不想拷贝,所以直接赋给引用吧”,却不一定思考返回的到底是什么)。不过,因为 #1,它实际上也没有真正帮你什么:很可能一开始就不会有拷贝。更进一步,现在你的代码读者必须处理局部变量类型是 const string& 而不是 string 这件事,并因此担心底层 string 是否已经离开作用域或发生变化。

换句话说,在评审最初那段代码时,我必须担心:

  • GetName() 是按值返回还是按引用返回?
  • Consumer 的构造函数接收 stringconst string& 还是 string_view
  • 构造函数是否对该参数有任何生命周期要求?(如果它不只是 string。)

然而,如果你一开始就把 name 声明为 string,通常并不会更低效(因为 RVO 和移动语义),而且至少同样可能在对象生命周期方面安全。

另外,如果确实存在对象生命周期问题,存储为 string 时往往更容易发现:不需要观察 GetName() 返回引用的生命周期承诺和 SetName() 生命周期要求之间的相互作用;拥有自己的 string 意味着只需要看本地代码和 SetName()

所有这些意思是:避免拷贝没问题,只要你没有让事情变得更复杂。在本来就不会发生拷贝时让代码更复杂,并不是好的权衡。

每周技巧 #99:非成员接口礼仪

上一节

每周技巧 #103:标志就是全局变量

下一节