浅谈C++ NRVO——从一道360笔试题说起

根据一道360 2015年秋招笔试题,题目是这样的

Widget f(Widget u)
{
      Widget v(u);
    Widget w = u;
    return w;
}
int main()
{
    Widget x;
    Widget y = f(f(x));
}

题目问一共会调用多少次复制构造函数。

在这里,为了方便观看, 我们自己构造一个Widget类

class Widget
{
 public:
      Widget(){cout << "construct" << endl;}
    Widget(const Widget&){cout << "copy" << endl;}
    Widget& operator=(const Widget&){cout << "assign" << endl;}
}

分别用cout << "construct" << endl; cout << "copy" << endl; cout << "assign"<打印出construct、copy和assign的次数。

我们分别用两种编译器来执行上面的代码,vs2013 debug、gcc 4.8.3,运行结果显示,在vs2013 debug执行了1次构造,7次复制;而gcc执行了1次构造,5次复制。

为什么造成这样的区别呢?

显然,构造发生在这里
Widget x;

而复制则发生在数个地方。根据标准,当以一个 object 的内容作为另一个 class object 的初值时,调用 copy constructor。有三种情况会“以一个 object 的内容作为另一个 class object 的初值”。

1. 对一个 object 做显示初始化。对xx1,xx2,xx3的初始化都是显示初始化。

```c++
X x;
X xx1 = x;
X xx2(x);
X xx3{x};
```

2. 当 object 被当做参数交给一个函数时。

3. 当函数传回一个 class object 时。

现在分别看看上述 7 个 copy 各自对应哪种情况。

Widget f(Widget u) //情形2
{
      Widget v(u);  //情形1
    Widget w = u; //情形1
    return w;
}

至此,内层的 f 函数至此就调用了 3 次 copy 了。

现在注意,到 return w 这里了,w 是一个 f 作用域内的局部对象。那么编译器会怎么将 f 的返回值从局部对象中拷贝出来呢?编译器通常的做法是添加一个额外的 class object reference 类型的参数,然后在 return 指令之前安插一个 copy constructor 操作。将欲传回的 object 的内容(此处为 w)作为新添加的参数的值。

在只执行1层f函数的情况下

Widget f(Widget u) //情形2
{
      Widget v(u);  //情形1
    Widget w = u; //情形1
    return w;
}
int main()
{
    Widget x;
    Widget y = f(x);
}

vs2013 debug和gcc 分别会执行4次拷贝和3次拷贝。

在vs2013 debug中,这四次可以看做是 x--->u,u--->v,u--->w,w--->一个临时变量(或者就是这里的 y)。因此无
论执行 Widget y = f(x)或者是 f(x),都是 4 次 copy。

而在gcc中,这三次则是这样构成的 x--->u,u--->v,u--->w,w--->一个临时变量(优化掉)。这是因为gcc中默认开启了NRVO,会将 return 表达式构造于接受返回值的 y 的stack中。因此,省去了一次拷贝构造函数的使用。

继续看两层 f 的情况,现在分析外层的 f,外层的 f 相当于执行 f(Widget u= f(x))。在 vs 中,内层中最后 copy 的那个临时变量直接传递到外层 f( )中,这里就不会发生拷贝构造了。

在vs中,余下的 3 次,则就是 u--->v,u--->w,w--->一个临时变量。在 g++中,余下的 2 次,则就是 u--->v,u--->w,w--->一个临时变量(优化掉)。以此内推,vs 中每多一层 f,则调用 copy 的次数+3,g++中次数+2。

再来看看别的情况
首先说说 NRVO 优化满足的条件,根据标准规定:

NRVO (Named Return Value Optimization): If a function returns a class type by valueand the return statement's expression is the name of a non-volatile object with automaticstorage duration (which isn't a function parameter), then the copy/move that would be performed by a non-optimising compiler can be omitted. If so, the returned value is constructed directly in the storage to which the function's return value would otherwise be moved or copied.

一个示例如下:

Widget f(Widget u)
{
      Widget v(u); 
    Widget w = u; 
    return u; //注意这里返回u了,u不是f函数作用域内的局部变量
}
int main()
{
    Widget x;
    Widget y = f(x);
}

在gcc中,则会执行1次构造和4次拷贝。说明这里没有触发NRVO。

用一个简单的重载运算符来说明一下编译器在开启NRVO和未开启NRVO情况下可能生成的代码。

class Complex
{
    friend Complex operator+(const Complex&, const Complex&);
public:
    Complex(double r = 0.0,double i = 0.0) : real(r), imag(i){}
    Complex(const Complex& c) : real(c.real), imag(c.imag){}
    Complex& operator= (const Complex& ){}
private:
    double real;
    double imag;
};
Complex operator+(const Complex& lhs,const Complex& rhs)
{
    Complex resultValue;
    resultValue.real = lhs.real + rhs.real;
    resultValue.imag = lhs.imag + rhs.imag;
    return resultValue;
}

编译器可能会将operator+的代码改写成如下

void ADD(const Complex& _result,const Complex& lhs,const Complex& rhs)
{
    /*
    *
    */
}

未使用NRVO时,编译器可能生成如下代码

void ADD(const Complex& _result,const Complex& lhs,const Complex& rhs)
{
      Complex resultValue;
      resultValue.Complex::Complex()          //默认构造resultValue
      resultValue.real = lhs.real + rhs.real; //注意以下两行和开启NRVO时的区别
      resultValue.imag = lhs.imag + rhs.imag;
      _result.Complex::Complex(resultValue);  //拷贝构造_result
      resultValue.Complex::~Complex();        //销毁resultValue
      return;
}

使用NRVO时,编译器可能生成如下代码

void ADD(const Complex& _result,const Complex& lhs,const Complex& rhs)
{
      _result.Complex::Complex();            //默认构造_result
      _result.real = lhs.real + rhs.real;    //注意以下两行和未开启RVO时的区别
      _result.imag = lhs.imag + rhs.imag;
}

最后补充一下,这道题中所涉及的 NRVO 是 copy elision 中的一种。

关于copy elision 的一些介绍链接如下

copy elision

copy elision & rvo

而另一个备受争议的问题:函数传参是传值好还是传引用好,在某些细节问题上也和 copy elision 相关。以下两个链接就很好地讨论了这个问题。

want speed, pass by value

want speed,don't always pass by value

你可能感兴趣的:(浅谈C++ NRVO——从一道360笔试题说起)