延长临时生命周期,使用块作用域聚合,但不能通过`new`; 为什么?

Fil*_*efp 10 c++ object-lifetime language-lawyer c++11

注意:这个问题本来是问,评论 瑞安海宁这个答案.


struct A { std::string const& ref; };
Run Code Online (Sandbox Code Playgroud)

// (1)

A a { "hello world" };              // temporary's lifetime is extended to that of `a`
std::cout << a.ref << std::endl;    // safe
Run Code Online (Sandbox Code Playgroud)

// (2)

A * ptr = new A { "hello world" };  // lifetime of temporary not extended?
std::cout << ptr->ref << std::endl; // UB: dangling reference
Run Code Online (Sandbox Code Playgroud)


  • 为什么临时的生命期延长到(1),而不是(2)

Fil*_*efp 6

LONG STORY,SHORT

编译器无法延长所涉及的临时生命周期new A { "temporary " },因为A创建的和临时的,具有不同的存储持续时间.

可以在本文末尾找到标准所说的内容.标准明确规定生命周期不会延长,但它没有详细说明原因.

本文将尝试以更广泛的受众可以理解的方式解释原因,而不仅仅是普通的语言律师.


介绍

在C++中,对象可以有几种不同的存储持续时间,其中包括自动动态存储持续时间,简要说明如下:

自动存储持续时间

具有自动存储持续时间的对象的存储将持续存在,直到创建它们的块退出.

  • 在块范围声明的对象具有自动存储持续时间(除非它们被声明staticextern,但不是register).

  • 根据定义,Temporaries在块范围内声明,因此它们也具有自动存储持续时间.


动态存储持续时间

具有动态存储持续时间的对象的存储将持续存在,直到明确声明它应该被释放为止; 换句话说,这种存储不受任何特定范围的约束.

  • 动态创建的对象operator new具有动态存储持续时间.

    存储将一直存在,直到进行匹配的呼叫operator delete.




使用自动存储持续时间聚合初始化

如前一节所述,临时具有自动存储持续时间.

如果我们构造一个具有自动存储持续时间聚合,那么这也将存储绑定到当前范围; 意味着临时的生命周期可以很容易地扩展到与聚合的生命周期相匹配.

注意:我们可以想象它们生活在同一个"盒子"中,在范围的最后我们丢弃这个盒子,这很好; 暂时的,也不是总的,都不会比盒子的寿命更长.


我们的实施 (A)

struct A { std::string const& ref; };
Run Code Online (Sandbox Code Playgroud)

void func () {
  A x { {"hello world"} };
}
Run Code Online (Sandbox Code Playgroud)

幕后 (A)

由于两者x和临时都具有自动存储持续时间,因此编译器可以实现如下语义等效的片段:

void  __func () {
  std::string __unnamed_temporary { "hello world" };
  A x { __unnamed_temporary };
}
Run Code Online (Sandbox Code Playgroud)

注意:临时和聚合都有它们的生命周期绑定到当前范围,太棒了!



使用动态存储持续时间聚合初始化

我们的实施 (B)

A* gunc () {
  A *    ptr = new A { { "hello world" } };
  return ptr;
}

int main () {
  A * p = gunc ();

  std::cout << p->ref << std::endl; // DANGER, WILL ROBINSON!

  delete p;
}
Run Code Online (Sandbox Code Playgroud)

在前面的章节中,已经指出临时存储器具有自动存储持续时间,这意味着我们的临时绑定A::ref将在驻留在当前范围内的存储上构建.


幕后 (B)

语义等价gunc可以看作如下实现:

A* gunc () {
  A __unnamed_temporary { "hello world " };

  A * ptr = new A { __unnamed_temporary }; // (1)

  return ptr;
}
Run Code Online (Sandbox Code Playgroud)

你也在考虑,不是吗?

我们不再能够将临时的生命周期延长到A动态存储持续时间的生命周期相匹配,在(1).

问题是,__unnamed_temporary一旦我们返回,自动存储将会消失gunc,从而有效地杀死我们的临时存储.

A然而,动态创建的仍将是活着的,留下我们的悬挂参考main.



结论

编译器无法延长通过new-initializer创建对象时涉及的任何临时值的生命周期,因为新的 ed对象和临时对象将具有不同的存储持续时间.



标准(n3797)说什么?

12.2p5 临时对象 [class.temporary]

绑定引用的临时对象或绑定引用的子对象的完整对象的临时对象在引用的生命周期内持续存在,除了:

...

  • new-initializer(5.3.4)中对引用的临时绑定将持续到包含new-initializer的full-expression完成为止.

    [ 注意:这可能会引入悬空参考,并鼓励实施在这种情况下发出警告.- 结束说明 ]