为什么临时的寿命不会延长到封闭物体的寿命?

时间:2011-08-04 05:45:50

标签: c++ temporary

我知道临时不能绑定到非const引用,但它可以绑定到const引用。也就是说,

 A & x = A(); //error
 const A & y = A(); //ok

我也知道在第二种情况(上图)中,A()创建的临时生命周期延长到const引用的生命周期(即y)。

但我的问题是:

绑定到临时的const引用是否可以进一步绑定到另一个const引用,将临时的生命周期延长到第二个对象的生命周期?

我尝试过这个并没有用。我不太明白这一点。我写了这段代码:

struct A
{
   A()  { std::cout << " A()" << std::endl; }
   ~A() { std::cout << "~A()" << std::endl; }
};

struct B
{
   const A & a;
   B(const A & a) : a(a) { std::cout << " B()" << std::endl; }
   ~B() { std::cout << "~B()" << std::endl; }
};

int main() 
{
        {
            A a;
            B b(a);
        }
        std::cout << "-----" << std::endl;
        {
            B b((A())); //extra braces are needed!
        }
}

输出(ideone):

 A()
 B()
~B()
~A()
-----
 A()
 B()
~A()
~B()

产量差异?为什么临时对象A()在第二种情况下在对象b之前被破坏?标准(C ++ 03)是否谈到了这种行为?

7 个答案:

答案 0 :(得分:17)

该标准考虑了延长临时寿命的两种情况:

  

§12.2/ 4有两种情况下,临时表在不同于完整表达结束时被摧毁。第一个上下文是表达式作为定义对象的声明符的初始值设定项。在该上下文中,保存表达式结果的临时值将持续存在,直到对象的初始化完成。 [...]

     

§12.2/ 5第二个上下文是指引用绑定到临时。 [...]

这两个中的任何一个都不允许您通过稍后将引用绑定到另一个const引用来延长临时的生命周期。但是要忽视这一点并思考发生了什么:

临时堆在堆栈中创建。从技术上讲,调用约定可能意味着适合寄存器的返回值(临时)甚至可能不会在堆栈中创建,但请耐心等待。当您将常量引用绑定到临时时,编译器语义会创建一个隐藏的命名变量(这就是复制构造函数需要可访问的原因,即使它未被调用)并绑定对该变量的引用。实际制作或删除副本是一个细节:我们拥有的是未命名的局部变量及其引用。

如果标准允许你的用例,则意味着临时的生命周期必须一直延长到最后一次引用该变量。现在考虑一下你的例子的简单扩展:

B* f() {
   B * bp = new B(A());
   return b;
}
void test() {
   B* p = f();
   delete p;
}

现在的问题是临时(让我们称之为_T)绑定在f()中,它的行为类似于局部变量。引用绑定在*bp内。现在该对象的生命周期超出了创建临时对象的功能,但因为_T未动态分配,这是不可能的。

在本例中,您可以尝试推理延长临时生命周期所需的工作量,答案是没有某种形式的GC就无法完成。

答案 1 :(得分:7)

不,通过传递引用不会进一步延长延长的生命周期。

在第二种情况下,临时绑定到参数 a,并在参数的生命周期结束时销毁 - 构造函数的结束。

该标准明确指出:

  

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

答案 2 :(得分:4)

§12.2/ 5说“第二个背景[当一个临时的生命周期 是扩展]是指引用绑定到临时。“采取 从字面上看,这清楚地表明应延长寿命 你的情况;你的B::a肯定是临时的。 (参考 绑定到一个对象,我没有看到它可能的任何其他对象 但是,这是非常糟糕的措辞。我确定那是什么 意思是“第二个背景是临时用于 初始化引用,“和扩展的生命周期对应于 使用rvalue表达式创建的引用的引用 临时的,而不是以后的任何其他参考文献 绑定到对象。就目前而言,措辞需要一些东西 这根本不可实现:考虑:

void f(A const& a)
{
    static A const& localA = a;
}

呼叫:

f(A());

编译器应该放在哪里A()(因为它通常看不到 f()的代码,并且不知道本地静态何时 发电话?)

我认为,实际上,这值得DR。

我可能会补充说,有文字强烈暗示我的 解释意图是正确的。想象一下,你有一秒钟 B的构造函数:

B::B() : a(A()) {}

在这种情况下,B::a将直接初始化为临时;该 即使我的解释,这个临时的寿命也应该延长。 但是,该标准对此案例作了特殊例外;这样的 临时只持续到构造函数退出(再次出现) 留下你的悬挂参考)。这种例外提供了非常好的 强烈表明该标准的作者并不打算这样做 类中的成员引用可以延长任何临时值的生命周期 他们必然会;再次,动机是可实施性。想像 而不是

B b((A()));
你写的是:

B* b = new B(A());

编译器应该将临时A()放在哪里,以便它的生命周期 将是动态分配的B

答案 3 :(得分:4)

您的示例不执行嵌套生命周期延期

在构造函数

B(const A & a_) : a(a_) { std::cout << " B()" << std::endl; }

此处的a_(重新命名为博览会)不是暂时的。表达式是否是临时表达式是表达式的语法属性, id-expression 永远不是临时的。所以这里没有终身延长。

以下是生命延长会发生的情况:

B() : a(A()) { std::cout << " B()" << std::endl; }

但是,因为引用是在ctor-initializer中初始化的,所以生命周期只会延长到函数结束。每 [class.temporary] p5

  

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

在对构造函数的调用

B b((A())); //extra braces are needed!

在这里,我们 绑定对临时的引用。 [class.temporary] p5 说:

  

在函数调用(5.2.2)中对引用参数的临时绑定一直持续到包含该调用的完整表达式完成为止。

因此A临时语句在语句结束时被销毁。这发生在块{}之前销毁B变量之前,解释了您的日志输出。

其他情况确实执行嵌套生命周期延期

聚合变量初始化

使用引用成员对结构进行聚合初始化可以是生命周期扩展:

struct X {
  const A &a;
};
X x = { A() };

在这种情况下,A临时绑定直接绑定到引用,因此临时生命周期延长到x.a的生存期,这与{{1的生命周期相同}}。 (警告:直到最近,很少有编译器能够做到这一点。)

聚合临时初始化

在C ++ 11中,您可以使用聚合初始化来初始化临时,从而获得递归的生命周期扩展:

x

使用trunk Clang或g ++,会产生以下输出:

struct A {
   A()  { std::cout << " A()" << std::endl; }
   ~A() { std::cout << "~A()" << std::endl; }
};

struct B {
   const A &a;
   ~B() { std::cout << "~B()" << std::endl; }
};

int main() {
  const B &b = B { A() };
  std::cout << "-----" << std::endl;
}

请注意, A() ----- ~B() ~A() 临时和A临时都是终身延长的。因为B临时的构造首先完成,所以它最后被破坏。

A初始化

C ++ 11的std::initializer_list<T>执行生命周期扩展,就好像通过绑定对底层数组的引用一样。因此,我们可以使用std::initializer_list<T>执行嵌套生命周期扩展。但是,编译器错误在这个领域很常见:

std::initializer_list

使用Clang trunk生成:

struct C {
  std::initializer_list<B> b;
  ~C() { std::cout << "~C()" << std::endl; }
};
int main() {
  const C &c = C{ { { A() }, { A() } } };
  std::cout << "-----" << std::endl;
}

和g ++ trunk:

 A()
 A()
-----
~C()
~B()
~B()
~A()
~A()

这些都是错的;正确的输出是:

 A()
 A()
~A()
~A()
-----
~C()
~B()
~B() 

答案 4 :(得分:2)

在第一次运行中,对象按照它们被推入堆栈的顺序被销毁 - &gt;那就是推A,推B,弹B,弹出A。

在第二轮中,A的生命周期以b的构造结束。因此,它创建A,它从A创建B,A的生命周期结束,因此它被破坏,然后B被破坏。有道理......

答案 5 :(得分:1)

我不了解标准,但可以讨论我在之前几个问题中看到的一些事实。

第一个输出是出于显而易见的原因ab在同一范围内。同样ab之后被销毁,因为它是在b之前构建的。

我认为你应该对第二输出更感兴趣。在开始之前,我们应该注意以下类型的对象创作(独立临时):

{
  A();
}

仅持续到下一个;,而不是围绕它。 Demo。在你的第二种情况下,当你这样做时,

B b((A()));

因此A()对象创建完成后会立即销毁B()。因为,const引用可以绑定到临时,这不会给出编译错误。但是,如果您尝试访问B::a,它肯定会导致逻辑错误,{{1}}现在已经超出范围变量。

答案 6 :(得分:0)

§12.2/ 5说

  

函数调用(5.2.2)中的引用参数的临时绑定将持续到包含该调用的完整表达式完成为止。

非常切割和干燥,真的。