绑定到引用时临时对象生命周期扩展异常的基本原理是什么?

时间:2014-02-20 21:46:27

标签: c++ c++11 object-lifetime temporary-objects

在C ++ 11标准的12.2中:

  

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

     
      
  1. 临时约束   到构造函数的ctor-initializer中的引用成员(12.6.2)   持续到构造函数退出。

  2.   
  3. 临时绑定到   函数调用(5.2.2)中的引用参数一直持续到   完成包含调用的完整表达式。

  4.   
  5. 一生   临时绑定到函数返回中的返回值   声明(6.6.3)未予延长;临时被摧毁了   return语句中的完整表达式的结尾。

  6.   
  7. 暂时的   绑定到new-initializer(5.3.4)中的引用一直持续到   完成包含new-initializer的完整表达式。

  8.   

标准中有最后一个案例:

struct S {
  int mi; 
  const std::pair<int,int>& mp;
}; 
S a { 1,{2,3} };  // No problem.
S* p = new S{ 1, {2,3} };  // Creates dangling reference

对我来说,2. and 3.有道理且易于同意。但是,1. and 4.的原因是什么?这个例子对我来说看起来很邪恶。

3 个答案:

答案 0 :(得分:5)

与C和C ++中的许多内容一样,我认为这归结为可以合理(和有效)实现的内容。

Temporaries通常在堆栈上分配,并且调用其构造函数和析构函数的代码将被发送到函数本身。因此,如果我们将您的第一个示例扩展到编译器实际执行的操作,它将类似于:

  struct S {
    int mi;
    const std::pair<int,int>& mp;
  };

  // Case 1:
  std::pair<int,int> tmp{ 2, 3 };
  S a { 1, tmp };

编译器可以很容易地将tmp临时的生命周期延长到足以保持&#34; S&#34;是有效的,因为我们知道&#34; S&#34;将在函数结束前销毁。

但这并不适用于新的S&#34;情况下:

  struct S {
    int mi;
    const std::pair<int,int>& mp;
  };

  // Case 2:
  std::pair<int,int> tmp{ 2, 3 };
  // Whoops, this heap object will outlive the stack-allocated
  // temporary!
  S* p = new S{ 1, tmp };

为了避免悬空引用,我们需要在堆上而不是堆栈上分配临时值,如:

   // Case 2a -- compiler tries to be clever?
   // Note that the compiler won't actually do this.
   std::pair<int,int> tmp = new std::pair<int,int>{ 2, 3 };
   S* p = new S{ 1, tmp };

但是相应的delete p需要释放这个堆内存!这与引用的行为完全相反,并且会破坏使用普通引用语义的任何内容:

  // No way to implement this that satisfies case 2a but doesn't
  // break normal reference semantics.
  delete p;

所以问题的答案是:规则是这样定义的,因为它是C ++关于堆栈,堆和对象生命周期的语义的唯一实用解决方案。

警告:@Potatoswatter在下面指出,这似乎并非在C ++编译器中一致地实现,因此目前最不可移植。看看他的例子,说明Clang如何做标准似乎在这里要求的。他还说,情况可能比那更糟糕。 - 我不确切地知道这意味着什么,但实际上C ++中的这种情况似乎存在一些不确定性。

答案 1 :(得分:2)

主要推力是参考扩展仅在生命周期可以轻松且确定性地确定时发生,并且这个事实可以在创建临时的代码行上被推断为

调用函数时,它会扩展到当前行的末尾。这很长,很容易确定。

当您在“堆栈”上创建自动存储引用时,可以确定性地确定该自动存储引用的范围。临时可以在那时清理。 (基本上,创建一个匿名自动存储变量来存储临时值)

new表达式中,无法在创建时静态确定破坏点。每当delete发生时。如果我们希望delete(有时)破坏临时,那么我们的引用“二进制”实现必须比指针更复杂,而不是更少或相等。它有时会拥有所引用的数据,有时则不会。这是一个指针,加上bool。在C ++中,你不需要支付你不使用的东西。

在构造函数中也是如此,因为您无法知道构造函数是在new还是堆栈分配中。因此,在相关行中无法静态理解任何生命周期延长。

答案 2 :(得分:0)

您希望临时对象持续多长时间?它必须在某处分配。

它不能在堆上,因为它会泄漏;没有适用的自动内存管理。它不能是静态的,因为可以有多个。它必须在堆栈上。然后它会持续到表达式结束或函数结束。

表达式中的其他临时值,可能绑定到函数调用参数,在表达式结束时被销毁,并持续到函数结束或“{}”作用域将是一般规则的例外。因此,通过对其他案例的推论和推断,全表达是最合理的生命周期。

我不确定你为什么说这没问题:

S a { 1,{2,3} };  // No problem.

无论您是否使用new,悬挂参考都是相同的。

检测程序并在Clang中运行它会产生以下结果:

#include <iostream>

struct noisy {
    int n;
    ~noisy() { std::cout << "destroy " << n << "\n"; }
};

struct s {
    noisy const & r;
};

int main() {
    std::cout << "create 1 on stack\n";
    s a {noisy{ 1 }};  // Temporary created and destroyed.

    std::cout << "create 2 on heap\n";
    s* p = new s{noisy{ 2 }};  // Creates dangling reference
}

create 1 on stack
destroy 1
create 2 on heap
destroy 2

绑定到类成员引用的对象没有延长的生命周期。

其实我确定这是标准中已知缺陷的主题,但我现在没时间钻研......