35

我知道临时不能绑定到非常量引用,但可以绑定到常量引用。那是,

 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) 是否讨论过这种行为?

4

7 回答 7

24

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

§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,它就无法完成。

于 2011-08-04T07:41:22.863 回答
8

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

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

该标准明确规定:

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

于 2011-08-04T06:27:48.613 回答
5

§12.2/5 说“第二个上下文 [当临时对象的生命周期延长时] 是引用绑定到临时对象时。” 从字面上看,这清楚地表明在您的情况下应该延长寿命;你B::a的肯定是暂时的。(引用绑定到一个对象,我看不到它可能绑定到的任何其他对象。)然而,这是非常糟糕的措辞;我确定它的意思是“第二个上下文是使用临时变量来初始化引用时”,并且延长的生命周期对应于使用创建临时的右值表达式初始化的引用的生命周期,而不是以后可能绑定到对象的任何其他引用的生命周期。就目前而言,措辞需要一些根本无法实现的东西:考虑:

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

于 2011-08-04T08:25:54.517 回答
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,与 的生命周期相同x。(警告:直到最近,很少有编译器能做到这一点)。

聚合临时初始化

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

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;
}

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

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

请注意,A临时的和B临时的都是生命周期延长的。因为A临时建筑首先完成,所以最后被破坏。

std::initializer_list<T>初始化中

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

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;
}

使用 Clang 主干生成:

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

并使用 g++ 主干:

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

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

 A()
 A()
-----
~C()
~B()
~A()
~B()
~A()
于 2013-06-27T23:04:37.450 回答
2

在您的第一次运行中,对象按照它们被压入堆栈的顺序被销毁 -> 即 push A、push B、pop B、pop A。

在第二次运行中,A 的生命周期以 b 的构造结束。因此,它创建 A,从 A 创建 B,A 的生命周期结束,因此它被销毁,然后 B 被销毁。说得通...

于 2011-08-04T06:20:40.033 回答
1

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

由于显而易见的原因,第一个输出保持不变,a并且b在同一范围内。也 a被销毁 afterb因为它是在 before 构建的b

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

{
  A();
}

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

B b((A()));

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

于 2011-08-04T06:34:37.390 回答
-1

§12.2/5 说

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

切得很干,真的。

于 2011-08-07T23:39:25.103 回答