25

在 [C++11: 12.8/31] 中有说明:

这种复制/移动操作的省略,称为复制省略,是允许的[...]:

— 在具有类返回类型的函数的 return 语句中,当表达式是具有与函数返回类型相同的 cv 非限定类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作

这意味着

#include <iostream>

using namespace std;

struct X
{
    X() { }
    X(const X& other) { cout << "X(const X& other)" << endl; }
};

X no_rvo(X x) {
    cout << "no_rvo" << endl;
    return x;
}

int main() {
    X x_orig;
    X x_copy = no_rvo(x_orig);

    return 0;
}

将打印

X(const X& other)
no_rvo
X(const X& other)

为什么需要第二个复制构造函数?编译器不能简单地延长x的生命周期吗?

4

2 回答 2

12

Imagineno_rvo是在不同的文件中定义的,main因此在编译时编译main器只会看到声明

X no_rvo(X x);

并且不知道X返回的类型对象是否与参数有任何关系。从它当时所知道的情况来看,实施no_rvo也可以是

X no_rvo(X x) { X other; return other; }

因此,当它例如编译该行时

X const& x = no_rvo(X());

当最大限度地优化时,它将执行以下操作。

  • no_rvo生成临时 X作为参数传递给
  • call no_rvo,并将其返回值绑定到x
  • 破坏它传递给的临时对象no_rvo

现在,如果 from 的返回值与no_rvo传递给它的对象是同一个对象,那么临时对象的销毁将意味着返回的对象的销毁。但这将是错误的,因为返回的对象绑定到一个引用,因此将其生命周期延长到该语句之外。然而,简单地不破坏参数也不是解决方案,因为如果定义no_rvo是我上面显示的替代实现,那将是错误的。因此,如果允许函数重用参数作为返回值,则可能会出现编译器无法确定正确行为的情况。

请注意,对于常见的实现,编译器无论如何都无法对其进行优化,因此正式允许它并不是一个很大的损失。另请注意,如果编译器可以证明这不会导致可观察行为的变化(所谓的 as-if 规则),则允许编译器优化副本。

于 2012-02-25T15:00:46.420 回答
5

RVO 的通常实现是调用代码传递一个内存块的地址,函数应该在其中构造其结果对象。

当函数结果直接是一个非形式参数的自动变量时,该局部变量可以简单地放置在调用者提供的内存块中,然后返回语句根本不进行复制。

对于按值传递的参数,调用机器代码必须在跳转到函数之前将其实际参数复制初始化到形式参数的位置。为了让函数将其结果放在那里,它必须首先销毁形式参数对象,这有一些棘手的特殊情况(例如,当该构造直接或间接引用形式参数对象时)。因此,不是用形式参数位置来识别结果位置,这里的优化逻辑上必须为函数结果使用单独的调用提供的内存块。

但是,未传入寄存器的函数结果通常由调用者提供。return即,对于表示形式论证的表达式的情况,人们可以合理地谈论 RVO,一种减少的 RVO,无论如何都会发生。而且它不符合“通过将自动对象直接构造到函数的返回值中”的文本。

总而言之,数据流需要调用者传入一个值,这意味着它必须是调用者初始化形式参数的存储,而不是函数。因此,通常无法避免从形式参数中复制回来(这个 Weasel 术语涵盖了编译器可以做非常特殊的事情的特殊情况,特别是对于内联的机器代码)。但是,它是初始化任何其他本地自动对象的存储的函数,然后再做RVO是没有问题的。

于 2012-02-25T14:53:22.063 回答