根据一道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"<<endl;
打印出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 相关。以下两个链接就很好地讨论了这个问题。
网友评论