【问题标题】:Why lifetime of temporary doesn't extend till lifetime of enclosing object?为什么临时的生命周期不会延长到封闭对象的生命周期?
【发布时间】:2025-11-25 12:45:01
【问题描述】:

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

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

【问题讨论】:

  • B b((A())); //需要额外的大括号! - 你能解释一下吗?
  • @Luchian:是的。你没听说过Most vexing parse吗?
  • 请注意,您的程序不包含任何延长生命周期的示例。通过 const 引用传递一个临时变量并不会延长它的生命周期,临时变量仍然在完整表达式结束时被销毁。

标签: c++ temporary


【解决方案1】:

该标准考虑了两种情况下临时的生命周期延长:

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

【讨论】:

  • @Nawaz:我通常用对象和发生的事情创建思维导图,类似于您可以为 NRVO 找到here 的小图像。能够画画有助于理解,也有助于我记忆。
【解决方案2】:

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

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

标准明确规定:

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

【讨论】:

  • 这个引文没有谈到 进一步 绑定到另一个类成员的 const 引用。所以我有点怀疑。
  • 该标准明确列出了一些延长寿命的地方。没有提到您的情况,表明它不会发生在那里。
  • 没有“延长寿命”。通过 const 引用传递临时变量不会延长其生命周期,临时变量仍会在完整表达式结束时被销毁。
  • 这不是适用的规则。在 C++0x 中,作为函数参数传递的临时规则支配。不知道C++03有没有这样的规则。
【解决方案3】:

§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吗?

【讨论】:

  • 我不同意 B::a 绑定到一个临时的。它所绑定的表达式是由参数的(隐式)取消引用形成的。在这种情况下,这是一个左值(尽管是const),而不是临时的。 C++0x 的文本对这些情况也很清楚:“在函数调用 (5.2.2) 中临时绑定到引用参数会持续存在,直到包含调用的完整表达式完成为止。”和“临时绑定到新初始化程序 (5.3.4) 中的引用,直到包含新初始化程序的完整表达式完成。”
  • @Ben Voigt 这是一个术语问题。引用不绑定到表达式。它绑定到一个对象。引用由表达式初始化;如果该表达式是一个左值,则它绑定到该左值指定的对象。如果左值是指定临时对象的引用,则该引用将绑定到该临时对象(对象)。
  • @James:都是真的。但无论标准使用何种措辞,临时性都是表达式的属性,而不是对象的属性。除非您想将“持续到”阅读为“至少持续到”。但是那样你就会失去对临时对象的确定性破坏,这在 IMO 中更糟。
  • @Ben Voigt 在标准的词汇中,对象是否是临时的;表达式是右值或左值。在需要对象的上下文中(例如初始化引用),右值表达式将导致创建临时对象。使用表达式(左值或右值)初始化引用,这导致它被绑定到对象(临时或非临时)。用右值表达式初始化的引用绑定到一个临时的;该引用在表达式中使用,是一个指向临时对象的左值。
【解决方案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&lt;T&gt;初始化

C++11 的std::initializer_list&lt;T&gt; 执行生命周期扩展,就好像通过绑定对底层数组的引用一样。因此我们可以使用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()

【讨论】:

    【解决方案5】:

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

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

    【讨论】:

    • 不是真的。 A的一生到底什么时候结束?在B的构造函数之后?如果是这样,另外一个人有相同的答案,但一段时间后删除了他的答案。
    • 这不能回答我的问题。我进一步将 const 引用(到临时)绑定到另一个 const 引用(成员),但临时之前被销毁了。我特别想知道这是不可能的吗? (至于记录,从输出中我可以解释对象破坏的order;事实上任何人都可以解释这一点。问题是,为什么对象会按该顺序被破坏?)跨度>
    【解决方案6】:

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

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

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

    {
      A();
    }
    

    只持续到下一个; 并且不适用于它周围的街区Demo。在你的第二种情况下,当你这样做时,

    B b((A()));
    

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

    【讨论】:

      【解决方案7】:

      §12.2/5 说

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

      切得干干净净,真的。

      【讨论】:

        最近更新 更多