【问题标题】:Initialization in return statements of functions that return by-value在按值返回的函数的 return 语句中初始化
【发布时间】:2021-10-23 06:30:30
【问题描述】:

我的问题源于对return 语句中的std::move 的深入研究,例如以下示例:

struct A
{
    A() { std::cout << "Constructed " << this << std::endl; }
    A(A&&) noexcept { std::cout << "Moved " << this << std::endl; }
 };

A nrvo()
{
    A local;
    return local;
}

A no_nrvo()
{
    A local;
    return std::move(local);
}

int main()
{
    A a1(nrvo());
    A a2(no_nrvo());
}

哪个打印(MSVC、/std:c++17、发布)

Constructed 0000000C0BD4F990
Constructed 0000000C0BD4F991
Moved 0000000C0BD4F992

我对按值返回的函数中的 return 语句的一般初始化规则以及在返回带有 std::move 的局部变量时适用哪些规则感兴趣,如上所示。

一般情况

关于return statements你可以阅读

  1. 计算表达式,终止当前函数并将表达式的结果隐式转换为函数返回类型后返回给调用者。 [...]

在 cppreference.com 上。

其中Copy initialization 发生

  1. 从按值返回的函数返回时 像这样
 return other;

回到我的示例,根据我目前的知识 - 与上述规则相反 - A a1(nrvo()); 是一个声明,它使用纯右值 nrvo() 直接初始化 a1。那么究竟哪个对象是复制初始化,如 cppreference.com 中描述的返回语句?

std::move

对于这种情况,我参考了 ipc 在Are returned locals automatically xvalues 上的回答。我想确保以下内容是正确的:std::move(local) 的类型为 A&amp;&amp;,但 no_nrvo() 被声明返回类型为 A,所以这里是

将表达式的结果隐式转换为函数返回类型后返回给调用者

部分应该发挥作用。我想这应该是Lvalue to rvalue conversion:

任何非函数、非数组类型 T 的泛左值都可以隐式转换为相同类型的纯右值。 [...] 对于类类型,此转换 [...] 将 glvalue 转换为 prvalue,其结果对象由 glvalue 复制初始化。

要从A&amp;&amp; 转换为A 使用A 的移动构造函数,这也是这里禁用NRVO 的原因。这些规则是否适用于这种情况,我是否理解正确?此外,他们再次说 copy-initialized 通过 glvalue 但A a2(no_nrvo()); 是直接初始化。所以这里也涉及到第一种情况。

【问题讨论】:

  • "这也是此处禁用 NRVO 的原因。"我认为你在这部分想太多了。 NRVO 并不适用,因为您返回一个临时的:std::move() 的结果,这是一个“正常”函数。它没有名字,所以 NRVO 已经出局了。
  • "...在return语句中,当操作数为非易失性对象的名称时,自动存储时长,不是函数参数或 catch 子句参数,并且与函数返回类型具有相同的类类型(忽略 cv 限定)。这种复制省略的变体被称为 NRVO,“命名返回值优化”......” "...返回值优化是强制性的,从 C++17 开始不再被视为复制省略..." en.cppreference.com/w/cpp/language/copy_elision
  • @DanielLangr 当然,std::move 的结果本身确实不是一个临时对象。考虑我使用“临时”作为简化,但一般原则仍然存在。这不是一个名字,所以 NRVO 是不行的。
  • @Frank 我一看到你的回答就明白你的意思了。删除了我的评论。只是想澄清一下,没有创建A 类型的临时。
  • @DanielLangr 这是一个公平的观点。这是一个具体的命名问题,因此 cmets/answers 应该是准确的。

标签: c++ stdmove copy-initialization direct-initialization


【解决方案1】:

在深入了解此类细节时,您必须小心 cppreference.com,因为它不是权威来源。

那么,如 cppreference.com 中描述的返回语句,究竟哪个对象被复制初始化了?

在这种情况下,没有。这就是复制省略:通常会发生的复制被跳过。 cppreference (4) 子句可以写成“当从一个按值返回的函数返回时,并且副本没有被省略”,但这有点多余。 standard: [stmt.return] 在这个主题上更加清晰。

要从 A&& 转换为 A 使用 A 的移动构造函数,这也是此处禁用 NRVO 的原因。这些规则是否适用于这种情况,我是否理解正确?

这不太对。 NRVO 仅适用于非易失性对象的名称。但是,在return std::move(local); 中,返回的不是local,而是A&amp;&amp; 调用std::move() 的结果。这没有名称,因此强制性 NRVO 不适用。

我认为这应该是左值到右值的转换:

std::move() 返回的A&amp;&amp; 绝对不是左值。它是一个 xvalue,因此已经是一个 rvalue。这里没有发生左值到右值的转换。

但是 A a2(no_nrvo());是直接初始化。所以这里也涉及到第一种情况。

不是真的。作为return 语句的一部分,函数是否必须对其结果执行复制初始化不受调用该函数的方式的影响。同样,在调用点如何使用函数的返回参数也不受函数定义的影响。

在这两种情况下,an 都由函数的结果直接初始化。实际上,这意味着编译器将为an 对象使用与函数返回值相同的内存位置。

A a1(nrvo()); 中,由于NRVO,分配给local 的内存位置与函数的结果值相同,恰好是a1。实际上,locala1 一直是同一个对象。

A a2(no_nrvo()) 中,local 有自己的存储,函数的结果,也就是a2 是从它移动构造的。实际上,local 被移动到 a2

【讨论】:

  • 感谢您的回答!我理解您在最后两段中描述的行为以及您对 NRVO 和左值到右值转换的说明。尚不清楚的是此处复制和直接初始化的确切含义。当我从函数的角度说返回对象是复制初始化的(这也可能意味着移动)时,我是否理解正确,从调用者的角度来看,a2 是由函数返回的纯右值直接初始化的?所以观点决定使用什么术语?
猜你喜欢
  • 2018-06-28
  • 2020-01-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多