关键字:c++,移动构造,move,forward,RVO
前言
最近在探索ROS2代码里面遇到不少c++11以来的特性,尤其是move,forward等语法,在这里记录一下
正文
假如我们有这样一个类,该类的对象会在堆上开辟了一块比较大的空间:
class MyObject
{
public:
MyObject(){
data = new int[1024*1024*1024];
}
int* data;
};
考虑用一个临时MyObject来创造一个新的MyObject对象的情景,c++11以前可以通过拷贝构造函数实现,此时会将数据在内存中拷贝一份:
class MyObject
{
public:
MyObject(){
data = new int[1024*1024*1024];
std::cout<<"construct "<<this<<", data addr: "<<data<<std::endl;
}
MyObject(const MyObject& a){
data = new int[1024*1024*1024];
memcpy(data, a.data, sizeof(data));
std::cout<<"copy "<<this<<", data addr: "<<data<<std::endl;
}
~MyObject(){
std::cout<<"destroy "<<this<<", data addr: "<<data<<std::endl;
std::cout<<"delete"<<std::endl;
delete data;
}
int* data;
};
在main函数中测试:
int main()
{
auto obj_a = MyObject();
std::cout<<"obj_a addr: "<<&obj_a<<", data addr: "<<obj_a.data<<std::endl;
std::cout<<"-----------------"<<std::endl;
auto obj_b = obj_a;
std::cout<<"obj_b addr: "<<&obj_b<<", data addr: "<<obj_b.data<<std::endl;
std::cout<<"-----------------"<<std::endl;
return 1;
}
得到输出结果中有两次内存释放(这里默认开启了RVO优化,构造函数中的拷贝被编译器优化掉了):
construct 0x7ffc18c51da8, data addr: 0x7f05d6011010
obj_a addr: 0x7ffc18c51da8, data addr: 0x7f05d6011010
-----------------
copy 0x7ffc18c51db0, data addr: 0x7f05d5c10010
obj_b addr: 0x7ffc18c51db0, data addr: 0x7f05d5c10010
-----------------
destroy 0x7ffc18c51db0, data addr: 0x7f05d5c10010
delete
destroy 0x7ffc18c51da8, data addr: 0x7f05d6011010
delete
在c++11中引入了新的移动构造函数,语法如下:
class_name ( class_name && ) (1) (since C++11)
https://en.cppreference.com/w/cpp/language/move_constructor
我们在MyObject类中添加一个移动构造函数:
class MyObject
{
public:
MyObject(){
data = new int[1024*1024];
std::cout<<"construct "<<this<<", data addr: "<<data<<std::endl;
}
MyObject(const MyObject& a){
data = new int[1024*1024];
memcpy(data, a.data, sizeof(data));
std::cout<<"copy "<<this<<", data addr: "<<data<<std::endl;
}
MyObject(MyObject&& a){ //移动构造函数
data = a.data;
a.data = nullptr;
std::cout<<"move "<<this<<", data addr: "<<data<<std::endl;
}
~MyObject(){
std::cout<<"destroy "<<this<<", data addr: "<<data<<std::endl;
if(data){
std::cout<<"delete"<<std::endl;
delete data;
}
}
int* data;
};
移动构造函数中获取数据的位置,然后置原来对象的指针为nullptr。在析构函数中,为了防止delete空指针所以加了一个指针判断。
移动构造函数需要一个rvalue reference,函数返回值一般是一个rvalue,我们用一个函数返回MyObject来测试移动构造函数:
decltype(auto) func(){
return MyObject();//此函数创建并返回一个MyObject作为rvalue对象
}
int main()
{
MyObject obj_a = func();
std::cout<<"obj_a addr: "<<&obj_a<<", data addr: "<<obj_a.data<<std::endl;
std::cout<<"-----------------"<<std::endl;
return 1;
}
为了更清楚地观察移动构造,先将g++编译器的RVO优化功能关闭
g++ test.cpp -std=c++14 -fno-elide-constructors
编译执行得到的结果为:
wenhui@ubuntu:~/Desktop$ ./a.out
construct 0x7ffda7455610, data addr: 0x7fa0ceb45010
move 0x7ffda7455650, data addr: 0x7fa0ceb45010
destroy 0x7ffda7455610, data addr: 0
move 0x7ffda7455648, data addr: 0x7fa0ceb45010
destroy 0x7ffda7455650, data addr: 0
obj_a addr: 0x7ffda7455648, data addr: 0x7fa0ceb45010
-----------------
destroy 0x7ffda7455648, data addr: 0x7fa0ceb45010
delete
可以看到调用了两次move构造函数,堆内存只分配并delete了一次。
如果我们把移动构造函数注释掉,同时关闭g++的RVO优化,可以得到如下的输出结果:
wenhui@ubuntu:~/Desktop$ ./a.out
construct 0x7ffc1d6f0ba0, data addr: 0x7fe711abb010
copy 0x7ffc1d6f0be0, data addr: 0x7fe611aba010
destroy 0x7ffc1d6f0ba0, data addr: 0x7fe711abb010
delete
copy 0x7ffc1d6f0bd8, data addr: 0x7fe711abb010
destroy 0x7ffc1d6f0be0, data addr: 0x7fe611aba010
delete
obj_a addr: 0x7ffc1d6f0bd8, data addr: 0x7fe711abb010
-----------------
destroy 0x7ffc1d6f0bd8, data addr: 0x7fe711abb010
delete
没有move构造函数时使用了拷贝构造函数,可以看到MyObject obj_a = func();
有三次delete的过程。
如果没有移动构造函数但是开启RVO优化,可以得到:
wenhui@ubuntu:~/Desktop$ ./a.out
construct 0x7ffcefb7aac0, data addr: 0x7f75fa96f010
obj_a addr: 0x7ffcefb7aac0, data addr: 0x7f75fa96f010
-----------------
destroy 0x7ffcefb7aac0, data addr: 0x7f75fa96f010
delete
开启了RVO优化后就算没有移动构造函数也只有一次delete。
到这里可以看出移动构造函数可以实现数据在对象之间的移动同时减少堆空间数据拷贝。然而编译器的RVO对于函数返回值避免拷贝的处理表现更好。
除了函数返回值产生rvalue外,c++11引入的std::move语义也可以将参数转为rvalue。std::move无条件地将变量转为rvalue,不会移动任何东西,也不产生任何执行代码。所以说std::move的名称非常有误导性,它本质上就是一个类型转换(rvalue cast),一个简单的实现如下:
template<typename T>
decltype(auto) move(T&& param)
{
using ReturnType = std::remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
我们用上面的move函数来获取rvalue(效果和std::move一样),同时MyObject里面有移动构造函数,测试代码如下:
int main()
{
MyObject obj_a;
std::cout<<"obj_a addr: "<<&obj_a<<", data addr: "<<obj_a.data<<std::endl;
std::cout<<"-----------------"<<std::endl;
MyObject obj_b = move(obj_a); //move(obj_a)将obj_a转为rvalue,不移动任何东西!
std::cout<<"obj_b addr: "<<&obj_b<<", data addr: "<<obj_b.data<<std::endl;
std::cout<<"obj_a addr: "<<&obj_a<<", data addr: "<<obj_a.data<<std::endl;
std::cout<<"-----------------"<<std::endl;
return 1;
}
关闭RVO后执行代码得到输出:
wenhui@ubuntu:~/Desktop$ ./a.out
construct 0x7ffc94d6c5a8, data addr: 0x7f3378324010
obj_a addr: 0x7ffc94d6c5a8, data addr: 0x7f3378324010
-----------------
move 0x7ffc94d6c5b0, data addr: 0x7f3378324010
obj_b addr: 0x7ffc94d6c5b0, data addr: 0x7f3378324010 //指向的就是obj_a的data
obj_a addr: 0x7ffc94d6c5a8, data addr: 0 //obj_a的data被置为nullptr
-----------------
destroy 0x7ffc94d6c5b0, data addr: 0x7f3378324010
delete //只释放一次data
destroy 0x7ffc94d6c5a8, data addr: 0
除了move以外,std::forward也可以实现std::move的功能,std::move无条件地将参数cast为rvalue,forward可以根据提供地模板参数进行转发,例子如下:
MyObject obj_b = std::move(obj_a);
改写为:
MyObject obj_b = std::forward<const MyObject&>(obj_a);
输出显示调用了拷贝构造函数
wenhui@ubuntu:~/Desktop$ ./a.out
construct 0x7ffc4663cfd8, data addr: 0x7f1f2f91e010
obj_a addr: 0x7ffc4663cfd8, data addr: 0x7f1f2f91e010
-----------------
copy 0x7ffc4663cfe0, data addr: 0x7f1e2f91d010
obj_b addr: 0x7ffc4663cfe0, data addr: 0x7f1e2f91d010
obj_a addr: 0x7ffc4663cfd8, data addr: 0x7f1f2f91e010
-----------------
destroy 0x7ffc4663cfe0, data addr: 0x7f1e2f91d010
delete
destroy 0x7ffc4663cfd8, data addr: 0x7f1f2f91e010
delete
改为:
MyObject obj_b = std::forward<MyObject&&>(obj_a);
输出结果显示调用了移动构造函数
wenhui@ubuntu:~/Desktop$ ./a.out
construct 0x7ffcf6b07eb8, data addr: 0x7f353dc13010
obj_a addr: 0x7ffcf6b07eb8, data addr: 0x7f353dc13010
-----------------
move 0x7ffcf6b07ec0, data addr: 0x7f353dc13010
obj_b addr: 0x7ffcf6b07ec0, data addr: 0x7f353dc13010
obj_a addr: 0x7ffcf6b07eb8, data addr: 0
-----------------
destroy 0x7ffcf6b07ec0, data addr: 0x7f353dc13010
delete
destroy 0x7ffcf6b07eb8, data addr: 0
总结
c++11中移动构造函数可以减少临时对象的堆内存拷贝,其输入参数是一个rvalue reference,std::move提供了将表达式转为rvalue的功能,此外std::forward提供了更丰富的value类型转发。对于函数临时对象返回值的拷贝,编译器的RVO优化效果看起来更好。