c++ - 当强制 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;
}
第 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]中描述的函数调用,我很难在标准中找到任何明确的声明,即构造函数调用应该以相同的方式工作。
推荐阅读
- python-3.x - 如何从 Pandas 中的所有 DataFrame 中获取所有项目
- javascript - 如果 p5 在不断重绘时出现滞后怎么办?
- windows - 当我的电脑启动时,我如何在手机上收到通知?
- azure - 无法使用应用程序网关通过 SSH 连接到规模集实例
- php - laravel 如果表单值等于数据库值
- scala - spark-shell 使用类型安全配置加载 scala 配置
- firebase - Firestore 数据建模
- excel - VBA 多个用户使用 DAO 更新共享 MS-Access 数据库中的同一个表
- scala - 在 Scala 中根据特定条件压缩两个列表
- kubernetes - 无法理解 istio 身份验证策略