C++返回值的“拷贝”问题

这个问题本不是什么新鲜玩意了,但最近在和同事调试程序性能的时候,经常会提起这个问题。看来编译器的RVO和NRVO这个问题还是没有达到普及性。

先看一段代码:

#include <iostream>

using namespace std;


class MyClass
{
public:
    explicit MyClass() : m_value(0)
    {
        cout<<"MyClass Ctor!"<<endl;
    }

    MyClass(const MyClass &other) : m_value(other.m_value)
    {
        cout<<"MyClass Copy Ctor!"<<endl;
    }

    MyClass &operator =(const MyClass &other)
    {
        cout<<"MyClass Copy Operator!"<<endl;

        this->m_value = other.m_value;
        return *this;
    }

    ~MyClass()
    {
        cout<<"MyClass Dtor!"<<endl;
    }

    int m_value;
};

MyClass getMyObject()
{
    MyClass myObj;
    myObj.m_value = 2;
    return myObj;
}

void getMyObject(MyClass &myObj)
{
    myObj.m_value = 2;
}

int main()
{    
    //MyClass retObj = getMyObject(); // 调用1

    MyClass retObj;
    getMyObject(retObj); // 调用2

    return 0;
}

调用1和调用2的输出结果相同,如下截图:
这里写图片描述

我们可以清楚的看到,调用1的过程并不像我们想象的那样,会调用类的拷贝构造函数。这其实是编译器自动使用了NRVO(具名返回值优化技术),即在某些情况下编译器会避免无谓的拷贝操作。因此,不管是直接返回值还是通过引用回传值从理论上讲不会有任何空间效率和时间效率上的差异。
我们再简单的修改一下代码,在函数实现体里面增加一个分支:


MyClass getMyObject(bool even)
{
    MyClass myObj2, myObj5;
    if (even)
    {
        myObj2.m_value = 2;
        return myObj2;
    }
    else
    {
        myObj5.m_value = 5;
        return myObj5;
    }
}

然后调用:

int main()
{    
    MyClass retObj = getMyObject(true); // 调用1
    return 0;
}

输出结果如下:
这里写图片描述
我去!什么玩意?这是?NRVO呢?
不要着急,且慢,让我们再把代码作一点微小的调整:

MyClass getMyObject(bool even)
{
    MyClass myObj;
    if (even)
    {
        myObj.m_value = 2;
    }
    else
    {
        myObj.m_value = 5;
    }

    return myObj;
}

我们再次运行,得到输出结果如下:
这里写图片描述
看到上图,你是不是想骂: “NRVO你还有没有节操?一会儿这样一会儿那样?”。
关于这个问题的解释,推荐一篇文章 RVO V.S. std::move供阅读.
这篇文章用几幅图展示了函数执行时栈的增长方式。大概意思是,函数执行时,会有局部变量区,返回值区,函数调用区等。如果采用RVO的话,其实是在函数返回的时候直接覆写了返回值区,但如果函数中有多个return分支的话,编译器不知道该覆写哪个返回值区?
—— 这个有点绕,我也不太理解。不过不要紧,我们再把代码做一丁点修改。

MyClass getMyObject(bool even)
{
    if (even)
    {
        return MyClass();
    }

    MyClass myObj;
    myObj.m_value = 5;

    return myObj;
}

调用代码:


int main()
{    
    MyClass myobj = getMyObject(true);

    return 0;
}

运行输出结果如下:
这里写图片描述
这个时候输入的参数 even == true, 也就是说,执行的是if 分支,且return了一个匿名的临时对象, 编译器选择了 RVO

我们再来一次代码修改:“把if分支的返回对象改为具名的局部对象”。

MyClass getMyObject(bool even)
{
    if (even)
    {
        MyClass myTempObj;
        return myTempObj;
    }

    MyClass myObj;
    myObj.m_value = 5;

    return myObj;
}

调用代码的输入参数保持不变,依然为 even == true

int main()
{    
    MyClass myobj = getMyObject(true);

    return 0;
}

运行输出结果如下:
这里写图片描述

以上测试的是函数执行if分支。现在我们把输入的参数改为假,即 even == false. 让函数执行正常流程。

int main()
{    
    MyClass myobj = getMyObject(false);

    return 0;
}

运行输出结果如下:
这里写图片描述

把if 分支的具名局部对象去掉呢?会是什么结果?

MyClass getMyObject(bool even)
{
    if (even)
    {
        return MyClass();
    }

    MyClass myObj;
    myObj.m_value = 5;

    return myObj;
}

调用代码的输入参数保持不变,依然为 even == false
运行输出结果如下:
这里写图片描述
结果一样。

欲进一步探究细节,可以搜一下更多RVO或NRVO或者C++11右值引用、Move语义的相关文章读一读。

最后,我们再次通过下面这段代码来验证一下直接返回容器数据和通过引用返回容器数据的时间效率。

const int BIG_VALUE = 60000000; //  6千万
vector<float> getDatas()
{
    vector<float> datas;
    for (int i=0; i<BIG_VALUE; ++i)
    {
        datas.push_back(sqrt(i));
    }

    return datas;
}

void getDatas(vector<float> &datas)
{
    for (int i=0; i<BIG_VALUE; ++i)
    {
        datas.push_back(sqrt(i));
    }
}

int main()
{    
    double start = GetTickCount();
    vector<float> datas = getDatas();
    double  end = GetTickCount();
    cout << "getDatas()-time="<< (end - start) << endl;

    double refstart = GetTickCount();
    vector<float> refdatas;
    getDatas(refdatas);
    double  refend = GetTickCount();
    cout << "getDatas(ref)-time="<< (refend - refstart) << endl;

    return 0;
}

程序输出如下:
这里写图片描述
还能说什么呢?通过引用的时间效率居然还远远差于直接返回值的时间效率。
不知道是否与平台有关系,我测试的平台为windows,编译器是mingw4.4。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值