每周技巧 #107:引用生命周期延长

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #107: Reference Lifetime Extension

原文最初作为 totw/107 发布于 2015 年 12 月 10 日。

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

TotW 101 之后,有一些关于引用和生命周期的困惑反馈。因此在本技巧中,我们会更深入讨论这个问题:“什么时候会应用引用生命周期延长?”

1
2
3
string Foo::GetName();

const string& name = obj.GetName();  // 这安全吗/合法吗?

简而言之,临时对象(被引用物)的生命周期会被延长,当且仅当

  • 一个局部 const T&(或 T&&,尽管 Google 风格通常忽略这一点)被初始化为某个表达式的结果;该表达式通常是函数调用,并返回临时 T,或者返回临时对象的 T 子对象(例如包含 T 的 struct)。

标准语言可能有点难拆解,所以我们讨论一些边界情况来澄清:

  • 赋给 T& 时不适用,必须是 const T&。(否则是编译错误。)
  • 如果涉及(非多态)类型转换,则不适用。例如,从 string 赋给 const absl::string_view& 不会延长 string 的生命周期。(你首先也不该使用 const absl::string_view&,但那是另一个问题。)
  • 当你间接获取子对象时不适用:编译器不会看穿函数调用(getter 或类似函数)。子对象形式只在你直接从临时对象的 public 成员变量子对象赋值时有效。(所以这并不常见,因为我们很少有返回临时 struct 的表达式。)
  • 允许类型转换的一种情况是:把 U 的临时对象赋给 T&,其中 TU 的父类。请不要这样做:这比其他情况更让读者困惑。

如果临时对象的生命周期被延长,它会一直持续到引用离开作用域。如果临时对象的生命周期没有按上述方式延长,被引用的 T 会在语句结束时销毁(也就是遇到下一个 ; 时)。

按照 TotW 101 的建议,在显式引用初始化这种情况下,你大概不应该依赖生命周期延长:它并没有给你带来多少(或任何)性能收益,而且微妙、脆弱,容易给评审者和未来维护者增加额外工作。

有些微妙场景中,生命周期延长确实会发生,而且必要、有益(例如对临时容器做 ranged-for),但同样,延长的只是临时表达式结果的生命周期,不包括任何子表达式。例如,下面可以工作:

1
2
3
4
5
6
7
8
std::vector<int> GetInts();
for (int i : GetInts()) { }  // vector 的生命周期延长很重要

// 为这个字符串中的每个 char 返回大小为 1 的 string_view。
std::vector<absl::string_view> Explode(const string& s);

// 生命周期延长作用在 vector 上,但*不会*作用在临时 string 上!
for (absl::string_view s : Explode(StrCat("oo", "ps"))) { }  // 错误

下面不能工作:

1
2
3
4
5
6
7
8
MyProto GetProto();

// 这里*不会*发生生命周期延长:sub_protos(一个 repeated field)
// 会随着 MyProto 离开作用域而销毁,而生命周期延长规则
// 不会在这里魔法般地延长 GetProto() 返回的 MyProto 生命周期。
// 子对象生命周期延长只适用于简单的 is-a-member-of 关系:
// 编译器看不出 sub_protos() 本身返回的是外层临时对象的子对象引用。
for (const SubProto& p : GetProto().sub_protos()) { }  // 错误

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

上一节

每周技巧 #108:避免 `std::bind`

下一节


本节目录