当强制 RVO 应用于延长临时生命周期的引用时会发生什么?

问题描述

一个引用被另一个延长临时生命周期的引用初始化时,这个新引用不会扩展任何东西。

但是当强制 RVO 阻止引用被复制时会发生什么?

考虑这个例子:run on gcc.godbolt.org

#include <iostream>

struct A
{
    A() {std::cout << "A()\n";}
    A(const A &) = delete;
    A &operator=(const A &) = delete;
    ~A() {std::cout << "~A()\n";}
};

struct B
{
    const A &a;
};

struct C
{
    B b;
};

int main()
{
    [[maybe_unused]] C c{ B{ A{} } };
    std::cout << "---\n";
}

在 GCC 下打印

A()
---
~A()

但在 Clang 下结果是

A()
~A()
---

哪个编译器是正确的?

乍一看,GCC 做对了。但在这个例子中:

C foo()
{
    return { B{ A{} } };
}

int main()
{
    [[maybe_unused]] C c = foo();
    std::cout << "---\n";
}

A 的生命周期肯定不能扩展到函数之外(并且两个编译器都同意这一点)。

因为这个片段应该与第一个片段具有相同的 RVO,所以行为不应该相同吗?因此 Clang 的行为似乎更加一致。

解决方法

海湾合作委员会是对的。

在第二个示例中,由于 [class.temporary] ¶6.11,我们没有生命周期延长:

函数return语句([stmt.return])中返回值的临时绑定的生命周期没有延长;临时在 return 语句中的完整表达式结束时被销毁。

如果我们这样重写这个例子:

C foo(const A &a)
{
    return { B{ a } };
}

int main()
{
    C c = foo(A {});
    std::cout << "---" << std::endl;
}

clause 6.9 反而会开始:

在函数调用 ([expr.call]) 中绑定到引用参数的临时对象会一直存在,直到包含调用的完整表达式完成。

为什么生命周期延长适用于第一个示例?嗯,这很简单:聚合初始化器不是函数调用。它们在标准的不同部分进行了描述:函数调用在 [expr.call] 中描述,而初始化表达式在 [expr.type.conv] 中描述(以及在 [dcl.init.aggr] 中聚合初始化)。

但是请注意,如果 B 有一个实际的构造函数:

struct B
{
    const A &a;
    B(const A &a_): a(a_) {}
};

然后调用该构造函数算作一个函数调用,此时 [class.temporary] ¶6.9 再次变得相关。0 没有它,聚合的引用成员被视为直接声明为变量,就生命周期而言。

如果你想像 Clang 那样在没有临时生命周期延长的情况下执行聚合初始化(错误地),你可以使用括号代替大括号进行初始化,这将触发 [class.temporary] ¶6.10:

绑定到从带括号的表达式列表 ([dcl.init]) 初始化的类类型聚合的引用元素的临时对象一直存在,直到包含表达式列表。

不幸的是,Clang 目前显然没有实现这一点,因为这是 C++20 的新增功能(提案 P0960)。请注意,该提案的文本甚至明确说明 GCC 在第一个示例中的行为正是标准的意图。


0 大概。该子句只提到了 [expr.call] 中描述的函数调用,我很难在标准中找到任何明确的声明,即构造函数调用应该以相同的方式工作。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...