【问题标题】:What happens when mandatory RVO is applied to a reference that's extending the lifetime of a temporary?当强制 RVO 应用于延长临时生命周期的引用时会发生什么?
【发布时间】:2021-09-20 21:48:34
【问题描述】:

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

但是当强制 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 的生命周期肯定不能扩展到函数之外(并且两个编译器都同意这一点)。

既然这个 sn-p 应该与第一个具有相同的 RVO,那么行为不应该相同吗?因此 Clang 的行为似乎更加一致。

【问题讨论】:

  • fyi - 有趣的尝试是添加 -O3 并注意两个编译器都不会创建对象而只打印输出。至少两者的顺序没有改变。
  • fyi2 - 为BC 添加了析构函数工具,我现在更加困惑gcc.godbolt.org/z/YqdYn95xP
  • @RichardCritten B 是直接在c 中构造的,因为是RVO,所以说得通。
  • @j6t 为什么不呢?我们用一个临时值初始化一个引用。当不涉及 RVO 时,它确实会发生 here
  • 我的意思是忘记 [class.temporary]/3,由于 [class.temporary]/(6.2) 从函数返回时不会延长临时对象的生命周期。

标签: c++ language-lawyer return-value-optimization


【解决方案1】:

GCC 是对的。

在第二个示例中,由于[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] 中描述的函数调用,我很难在标准中找到任何明确的声明,即构造函数调用应该以相同的方式工作。

【讨论】:

  • 嗯。对我来说[class.temporary]/6.11 读起来好像它只适用于返回类型是引用时,但我不确定。由于“函数返回语句中的值”,它不应该应用when NRVO is used,但即使在这种情况下也没有生命周期延长。
  • @HolyBlackCat 好的,这似乎是标准措辞中的一个空白。但是无论如何都没有办法使生命周期延长工作,因为函数的调用者除了返回值之外没有办法获得另一个对象的所有权。 (有趣的是,看起来 GCC 无论如何都试图延长生命周期:A 的析构函数根本没有被调用,好像编译器期望 foocaller 销毁对象- 它当然不能。)
  • 是的,我刚刚reported it to GCC。我知道这是不可能的,我只是希望有一些措辞更明确地禁止它。
猜你喜欢
  • 2019-01-06
  • 1970-01-01
  • 2012-11-15
  • 2018-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-20
相关资源
最近更新 更多