文章目录
本文不是介绍右值引用和移动语义的,在阅读本文前,假设读者已经知道了什么是右值引用和移动语义。
右值分为:纯右值和将亡值,本文主要介绍纯右值,并不涉及将亡值。
环境
在运行测试代码时,使用了如下环境:
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上是一样的,并不会出现不同编译器运行结果不同。所以这样的代码在各个平台上的都是一致的。
总结
在编译器不对函数进行返回值优化时,函数的返回值会产生一个临时对象。
使用移动构造,可以提高创建临时对象的性能,两次拷贝构造变成了两次移动构造。
使用右值引用,可以减少一次移动构造。