7

在 C++11 标准的 12.2 中:

引用绑定到的临时对象或作为引用绑定到的子对象的完整对象的临时对象在引用的生命周期内持续存在,但以下情况除外:

  1. 临时绑定到构造函数的 ctor-initializer (12.6.2) 中的引用成员将持续存在,直到构造函数退出。

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

  3. 临时绑定到函数返回语句 (6.6.3) 中的返回值的生命周期不会延长;临时在 return 语句中的完整表达式的末尾被销毁。

  4. 临时绑定到 new-initializer (5.3.4) 中的引用将持续到包含 new-initializer 的完整表达式完成为止。

并且有一个标准中最后一种情况的例子:

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.有意义且容易同意。但是bebind的原因是什么1. and 4.?这个例子对我来说看起来很邪恶。

4

3 回答 3

5

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

临时对象通常分配在堆栈上,调用它们的构造函数和析构函数的代码被发送到函数本身。因此,如果我们将您的第一个示例扩展为编译器实际正在执行的操作,它看起来像:

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

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

编译器可以很容易地延长tmp临时的生命周期足够长以保持“S”有效,因为我们知道“S”将在函数结束之前被销毁。

但这在“新 S”情况下不起作用:

  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++ 中的这种情况似乎存在一些不确定性。

于 2014-02-20T22:22:16.753 回答
2

主要推力是,仅当可以轻松且确定地确定生命周期时才会发生引用扩展,并且可以在创建临时对象的代码行上尽可能地推断出这一事实。

当你调用一个函数时,它会被扩展到当前行的末尾。这足够长,而且很容易确定。

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

new表达式中,破坏点不能在创建点静态确定。每当delete发生。如果我们想要delete(有时)销毁临时对象,那么我们的引用“二进制”实现必须比指针更复杂,而不是小于或等于。它有时会拥有所引用的数据,有时则不拥有。所以这是一个指针,加上一个bool. 在 C++ 中,您无需为不使用的东西付费。

在构造函数中也是如此,因为您无法知道构造函数是在堆栈分配中new还是在堆栈分配中。因此,任何延长寿命都不能在有问题的线路上静态地理解。

于 2014-02-20T22:27:07.133 回答
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

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

实际上我确定这是标准中已知缺陷的主题,但我现在没有时间深入研究......</p>

于 2014-02-20T22:19:57.813 回答