什么是移动语义
解决什么问题
在C++中如果不使用指针, 那么return一个对象的行为会触发copy构造函数, 带来额外开销
如下:
Person MakePerson()
{
Person p;
p.name = "WangLiang";
p.tel = "13888888888";
p.address = "南京市雨花台区";
return p;
}
Person p = MakePerson();
所以在c++11以前, 一般做法是使用引用作为出参方式, 如下:
void MakePerson(Person& p)
{
p.name = "WangLiang";
p.tel = "13888888888";
p.address = "南京市雨花台区";
}
Person p;
MakePerson(p);
上面的出参方式写法比较冗繁
移动语义
C++11引入移动语义, 移动语义含义为对象内部的状态转移到接收对象,被移走状态的对象废弃,后续不再使用
移动语义示例分析
我这里使用的环境为windows+cygwin(gcc11.2.0)
在我试图编写一段演示不使用移动语义和使用移动语义的对比代码时,我发现一个有意思的现象,应该是gcc编译器的一个优化, 这里展开说明下
gcc默认开启移动语义优化
如下验证代码, 现在MakePerson函数内部打印出成员对象指针的地址, 在不使用移动语义的情况下
接收MakePerson返回, 预期是不使用移动语义, 接收后的对象内部成员地址不同
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string name;
string tel;
string address;
void PrintPtrAddr()
{
cout << (void*)name.c_str() << endl;
cout << (void*)tel.c_str() << endl;
cout << (void*)address.c_str() << endl;
}
};
Person MakePerson()
{
Person p;
p.name = "WangLiang";
p.tel = "13888888888";
p.address = "南京市雨花台区";
p.PrintPtrAddr();
return p;
}
int main(int argc, char* argv[])
{
Person p = MakePerson();
cout << endl;
cout << "after MakePerson return p:" << endl;
p.PrintPtrAddr();
return 0;
}
结果显示, 前后地址都是相同的, 这一步应该是高版本gcc编译器默认开启了移动语义优化
因为return的对象即将退出作用域,编译器判断return后原对象已经无用,默认开启了移动语义。
std::string copy-on-write优化介绍
当我尝试在相同作用域下使用默认的复制构造时, 结果仍然显示地址没有变化
上面代码中main函数写成:
int main(int argc, char* argv[])
{
Person p = MakePerson();
cout << endl;
cout << "after MakePerson return p:" << endl;
p.PrintPtrAddr();
cout << endl;
cout << "p2 ptr addr:" << endl;
Person p2 = p;
p2.PrintPtrAddr();
return 0;
}
结果显示,前后3个Person对象持有的成员对象内部地址相同
推测为string copy-on-write优化机制, 在此也简单介绍下
p2对象只是持有和p相同的内容,但没有改动,所以这时候std::string并没有启动重新构造,而是类似浅copy的实现方式, 直接指向指针
但如果有重新赋值就会触发重新new一块内存
如下代码增加一行p2.name赋值操作
int main(int argc, char* argv[])
{
Person p = MakePerson();
cout << endl;
cout << "after MakePerson return p:" << endl;
p.PrintPtrAddr();
cout << endl;
cout << "p2 ptr addr:" << endl;
Person p2 = p;
p2.name = "WangQiang"; //add writing
p2.PrintPtrAddr();
return 0;
}
结果p2的name对象产生了一个不同的地址:
显式开启移动语义
在p2复制p对象时,增加std::move(p)即可显式开启移动语义, 这种情况下p2将仍使用p原来的地址,
而p在移动复制后理论上不可再使用(会读取到错误值)
int main(int argc, char* argv[])
{
Person p = MakePerson();
cout << endl;
cout << "after MakePerson return p:" << endl;
p.PrintPtrAddr();
cout << endl;
cout << "p2 ptr addr:" << endl;
Person p2 = std::move(p); //显式开启移动语义
p2.name = "WangQiang";
p2.PrintPtrAddr();
return 0;
}
结果: