窥视C++细节-使用移动构造和右值引用优化函数返回值


本文不是介绍右值引用和移动语义的,在阅读本文前,假设读者已经知道了什么是右值引用和移动语义。

右值分为:纯右值和将亡值,本文主要介绍纯右值,并不涉及将亡值。

环境

在运行测试代码时,使用了如下环境:

linux使用的是ubuntu 18,在ubuntu上使用的是g++,版本如下:

root@learner:~# g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

在windows下,使用的是visual studio Community 2019,版本是16.8.4。

定义一个用于测试的类

class A
{
public:
    A(){
        cout << "constuctor :" << this << endl;

        const char *s = "hello world";
        size_ = strlen(s);
        str_ = new char[size_ + 1];
        strcpy(str_, s);
    }

    A(const A &another){
        cout << this << " cpy constructor from " << &another << endl;

        size_ = another.size_;
        str_ = new char[size_ + 1];
        strcpy(str_, another.str_);
    }

    A(A &&another){
        cout << this << " move constructor from " << &another << endl;
        
        this->size_ = another.size_;
        this->str_ = another.str_;

        another.str_ = nullptr;
    }

    ~A(){
        cout << "deconstrutor :" << this << endl;
        if (str_){
            delete[] str_;
        }
    }

    void print(){
        cout << "size = " << size_ << " : " << str_ << endl;
    }

    void print() const{
        cout << "[const]  size = " << size_ << " : " << str_ << endl;
    }

private:
    int size_;
    char *str_;
};

没有右值引用和移动构造

在各个编译器中,都会使用RVO(return value optimization,返回值优化)对函数的返回值进行优化,尽量减少拷贝的次数。在C++中类的拷贝有时会是很大的开销,如果能减少类拷贝的次数,则可以提高程序性能。

RVO:return value optimization,返回值优化。

#include <iostream>
#include <string.h>

using namespace std;

class A
{
... 
    //注释掉移动构造
    // A(A &&another){
    //     cout << this << " move constructor from " << &another << endl;
    //     this->size_ = another.size_;
    //     this->str_ = another.str_;

    //     another.str_ = nullptr;
    // }
...
};

A func()
{
    A a;
    cout<<"func: &a = "<<&a<<endl;
    return a;
}

int main()
{
    A a = func();
    cout<<"main: &a = "<<&a<<endl;
    
    return 0;
}

ubuntu运行结果如下:

root@learner:~/vscode/tmp# g++ aa.cpp 
root@learner:~/vscode/tmp# ./a.out 
constuctor :0x7ffc53b68230
func: &a = 0x7ffc53b68230
main: &a = 0x7ffc53b68230
deconstrutor :0x7ffc53b68230

visual studio运行结果如下:

constuctor :00AFFCE8
func: &a = 00AFFCE8
00AFFDF0 cpy constructor from 00AFFCE8
deconstrutor :00AFFCE8
main: &a = 00AFFDF0
deconstrutor :00AFFDF0

可以看出相同的程序,在不同的编译器上运行的结果是不一样的,各个编译器对函数返回值的优化是不一样的。

现在分析一下,它们都是怎么操作函数的返回值的。

ubuntu运行结果分析

在这里插入图片描述

通过运行的结果可以知道,func()中的a对象和main()函数中的a对象地址相同,说明它们是同一个对象。

调用func()函数,在函数内部会生成a对象,在func()调用结束后,把生成的对象a直接当成了main()函数中的a对象。这是g++做了优化的,避免了反复的拷贝。

visual studio运行结果分析

在这里插入图片描述

func()中生成a对象,在func()返回返回值时,通过返回值拷贝构造了main()函数中的a对象。

ubuntu中关闭返回值优化

以上使用的是编译器默认的返回值优化得出的结果,在编译时使用-fno-elide-constructors选项可以禁用返回值优化。

禁用返回值优化得到的结果如下:

root@learner:~/vscode/tmp# g++ -fno-elide-constructors aa.cpp 
root@learner:~/vscode/tmp# ./a.out 
constuctor :0x7ffc42befed0
func: &a = 0x7ffc42befed0
0x7ffc42beff20 cpy constructor from 0x7ffc42befed0
deconstrutor :0x7ffc42befed0
0x7ffc42beff10 cpy constructor from 0x7ffc42beff20
deconstrutor :0x7ffc42beff20
main: &a = 0x7ffc42beff10
deconstrutor :0x7ffc42beff10

在这里插入图片描述

禁用返回值优化后,多了两次拷贝构造。两次拷贝构造产生如上图。

添加移动构造

#include <iostream>
#include <string.h>

using namespace std;

class A
{
... 
    //添加移动构造
    A(A &&another){
        cout << this << " move constructor from " << &another << endl;
        this->size_ = another.size_;
        this->str_ = another.str_;

        another.str_ = nullptr;
    }
...
};

A func()
{
    A a;
    cout<<"func: &a = "<<&a<<endl;
    return a;
}

int main()
{
    A a = func();
    cout<<"main: &a = "<<&a<<endl;
    
    return 0;
}
root@learner:~/vscode/tmp# g++ -fno-elide-constructors aa.cpp 
root@learner:~/vscode/tmp# ./a.out 
constuctor :0x7ffe1e896200
func: &a = 0x7ffe1e896200
0x7ffe1e896250 move constructor from 0x7ffe1e896200
deconstrutor :0x7ffe1e896200
0x7ffe1e896240 move constructor from 0x7ffe1e896250
deconstrutor :0x7ffe1e896250
main: &a = 0x7ffe1e896240
deconstrutor :0x7ffe1e896240

添加移动构造后,在构建临时对象时,编译器将使用移动构造。使用移动构造可以避免类资源的拷贝,构建临时对象时,直接将类资源搬移到了临时对象,没有发生资源的拷贝提高了效率。
在这里插入图片描述

使用const引用接收返回值

A func()
{
    A a;
    cout<<"func: &a = "<<&a<<endl;
    return a;
}

int main()
{
    const A& ra = func();
    cout<<"main: &a = "<<&ra<<endl;
    
    ra.print();

    return 0;
}
root@learner:~/vscode/tmp# g++ -fno-elide-constructors aa.cpp 
root@learner:~/vscode/tmp# ./a.out 
constuctor :0x7fff1f8b7ab0
func: &a = 0x7fff1f8b7ab0
0x7fff1f8b7b00 move constructor from 0x7fff1f8b7ab0
deconstrutor :0x7fff1f8b7ab0
main: &a = 0x7fff1f8b7b00
[const]  size = 11 : hello world   #调用的是const类型的print()函数
deconstrutor :0x7fff1f8b7b00

使用const引用接收返回值,可以减少一次移动构造,但是由于是const类型的引用,所以无法对返回值进行修改,同时也只能调用类A的const类型的函数。

虽然使用const引用接收返回值,可以优化性能,但是返回值的使用会受到很大的限制。
在这里插入图片描述

使用右值引用接收返回值

A func()
{
    A a;
    cout<<"func: &a = "<<&a<<endl;
    return a;
}

int main()
{
    A&& rra = func();
    cout<<"main: &a = "<<&rra<<endl;
    
    rra.print();

    return 0;
}
root@learner:~/vscode/tmp# g++ -fno-elide-constructors aa.cpp 
root@learner:~/vscode/tmp# ./a.out 
constuctor :0x7ffc42ddb7a0
func: &a = 0x7ffc42ddb7a0
0x7ffc42ddb7f0 move constructor from 0x7ffc42ddb7a0
deconstrutor :0x7ffc42ddb7a0
main: &a = 0x7ffc42ddb7f0
size = 11 : hello world      #调用的是非const类型的print()函数
deconstrutor :0x7ffc42ddb7f0

使用右值引用也可以减少一次移动构造,且对于引用的对象,可以进行修改,也可以调用非const类型的成员函数。
在这里插入图片描述

以下是在vs中运行的结果:

constuctor :004FFCF4
func: &a = 004FFCF4
004FFDFC move constructor from 004FFCF4
deconstrutor :004FFCF4
main: &a = 004FFDFC
size = 11 : hello world
deconstrutor :004FFDFC

可以看出也只发生了一次移动构造,和ubuntu上是一样的,并不会出现不同编译器运行结果不同。所以这样的代码在各个平台上的都是一致的。

总结

在编译器不对函数进行返回值优化时,函数的返回值会产生一个临时对象。

使用移动构造,可以提高创建临时对象的性能,两次拷贝构造变成了两次移动构造。

使用右值引用,可以减少一次移动构造。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值