美文网首页C++C++点点滴滴
浅谈C++ NRVO——从一道360笔试题说起

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

作者: WOWSCpp | 来源:发表于2017-02-01 17:16 被阅读0次

    根据一道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

    相关文章

      网友评论

        本文标题:浅谈C++ NRVO——从一道360笔试题说起

        本文链接:https://www.haomeiwen.com/subject/yullittx.html