浅谈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"<<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

copy elision & rvo

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

want speed, pass by value

want speed,don't always pass by value

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,780评论 0 38
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,514评论 1 51
  • 如果爱情遭遇了艾滋,会出现什么状况?应该如何去面对?结果会怎样? 《蓝色小药丸》的作者——瑞士著名漫画家弗雷德里克...
    山千黛阅读 883评论 2 6
  • 大学时大家叫我学霸,研究生大家叫我学神、勇哥。不是因为我很好学,成绩名列前茅(当然也不差),而是因为我很会学,也许...
    小成大数阅读 387评论 0 3
  • 什么是原则,什么是底线?很多人都说不是原则性也会就都能过去,不超底线就都可以接受,但什么是原则?什么是底线?难道原...
    巫女封心阅读 169评论 0 0