Skip to content

Latest commit

 

History

History
43 lines (33 loc) · 3.97 KB

101.md

File metadata and controls

43 lines (33 loc) · 3.97 KB

Tip of the Week #101: 返回值、引用与生命期 原文链接

最初在2015-07-29发布为totw/101
作者:Titus Winters (titus@google.com)

考虑以下代码片段:

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

特别地,我想请你注意&。它是否恰当?我们应该检查什么?会出现什么问题?我发现有相当数量的C++程序员并不完全清楚引用,但他们通常明白应该“避免拷贝”。就像C++的大多数问题一样,比那要复杂得多。

具体问题具体分析:返回了什么?怎么存储的?

这里有两个(或者三个)重要的问题:

  1. 返回的是什么类型(本例中指GetName())?
  2. 我们存储到/初始化了什么类型(本例中,name的类型是什么)?
  3. 如果返回的是引用,以引用返回的对象是否有生命期的限制?

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

  1. 返回string,初始化string:通常触发RVO,最坏情况下保证会触发现代类型的移动语义(参见TotW 77)。
  2. 返回string&const string&,初始化string:触发拷贝(我们返回的引用必然指向某个长时间存在的对象,所以一旦我们创建新字符串,数据就会有两个名字,因此是拷贝,参见TotW 77)。有时这是有价值的,比如你需要字符串的生命期长过函数返回保证的生命期。
  3. 返回string,初始化string&:无法编译,因为不能绑定引用到临时对象。
  4. 返回const string&,初始化string&:无法编译,因为不恰当的丢掉了常量性。
  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的话。)

然而,如果你一开始声明namestring,通常并不会低效(因为RVO和move语义),而且至少在对象生命期方面可能更安全。

附加的,如果有对象生命期问题,存储为string通常更容易发现问题:与其查看GetName()返回的引用的生命期保证与SetName()的生命期要求的相互作用,有自己的string对象意味着只需要查看本地代码和SetName()

所有这些是为了说:避免拷贝是好事,只要你没有把事情变得更复杂。本来就不会有拷贝时把代码弄得更复杂并不是好的权衡。