std::move
author: wearmheart date: 2023-01-24
C++11开始引入了std::move.
引入std::move主要是为了优化对象的生命周期, 以及优化函数参数传递方式。
然后又引入了一个右值得概念, 之前又有一个左值得概念。 左值和右值网上特别多,这里把这些理解汇总下。
左值 & 右值
- 左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址。
- 变量可以取地址,所以是左值,但是常量和临时对象等不可以取地址,所以是右值。
- 左值是表达式结束后依然存在的持久对象(代表一个在内存中占有确定位置的对象)。
- 右值是表达式结束时不再存在的临时对象(不在内存中占有确定位置的表达式)。
总之:对表达式取地址,如果能,则为左值,否则为右值。
左值引用 & 右值引用
左值引用的声明符号为 & ,右值引用的声明符号为 && 。
右值引用是用来支持转移语义的。
右值引用,用以引用一个右值,可以延长右值的生命期。
std::move
C++ move是为了转移所有权,将快要销毁的对象转移给其他变量,这样可以继续使用这个对象,而不必再创建一个一样的对象。
省去了创建新的一样内容的对象,也就提高了性能。
对于某些资源来说,可以改变所有者,但是只能有一份,move也解决这样的对象的管理问题。
下面将通过一个简单的demo介绍
C++ 中的 move 函数。
demo
移动函数,负责具体move发生了什么
当你想要转移资源所有权的时候,你会选择move。实际move发生的细节是由移动函数实现的。
下面的代码实现了当book被move的时候,做了特别的事情,推荐运行代码,
研究研究输出为什么count从1变成了3。(这个例子是为了说明move的具体实施者是谁,实际的代码要根据需求编写移动函数)
#include <string>
#include <memory>
#include <iostream>
class Book
{
int mCount{1};
std::string mName;
public:
std::string &get_name()
{
return mName;
}
Book(std::string iName) : mName(iName) {}
Book(Book &&iBook)
{
std::cout<< (this->mName) <<iBook.mName<<std::endl;
swap(this->mName, iBook.mName);
mCount = 3;
}
int get_count()
{
return mCount;
}
};
int main()
{
Book b("Im");
Book tb = std::move(b);
std::cout << "old b name is " << b.get_name() << " count is " << b.get_count();
std::cout << "\ntb name is " << tb.get_name() << " count is " << tb.get_count();
return 0;
}
小结
所以编程语言里的move跟我们日常生活的直观理解是有点区别的。我们生活中移动,是把物体从一个地方变动到另外一个地方。
而编程语言的move是改变物体(值)的所有权,而物体(值)在内存中没有变动过(存储在堆里那部分数据没有变动,在栈上的数据被拷贝了)。
右值引用和std::move的应用场景
在实际场景中,右值引用 和 std::move 被广泛用于在 STL 和 自定义类中实现移动语义,避免拷贝,从而提升程序性能。
在没有右值引用之前,一个简单的数组类通常实现如下,有构造函数、拷贝构造函数、赋值运算符重载、析构函数等。深拷贝/浅拷贝在此不做讲解。
class Array {
public:
int *data_;
int size_;
public:
Array(int size) : size_(size) {
data_ = new int[size_];
}
// 深拷贝构造
Array(const Array& temp_array) {
size_ = temp_array.size_;
data_ = new int[size_];
for (int i = 0; i < size_; i ++) {
data_[i] = temp_array.data_[i];
}
}
// 深拷贝赋值
Array& operator=(const Array& temp_array) {
delete[] data_;
size_ = temp_array.size_;
data_ = new int[size_];
for (int i = 0; i < size_; i ++) {
data_[i] = temp_array.data_[i];
}
}
~Array() {
delete[] data_;
}
};
该类的拷贝构造函数、赋值运算符重载函数已经通过使用左值引用传参来避免一次多余拷贝了,但是内部实现要深拷贝,无法避免。 这时,有人提出一个想法:是不是可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免深拷贝了,如:
class Array {
public:
Array(int size) : size_(size) {
data_ = new int[size_];
}
// 深拷贝构造
Array(const Array& temp_array) {
...
}
// 深拷贝赋值
Array& operator=(const Array& temp_array) {
...
}
// 移动构造函数,可以浅拷贝
Array(const Array& temp_array, bool move) {
data_ = temp_array.data_;
size_ = temp_array.size_;
// 为防止temp_array析构时delete data,提前置空其data_
temp_array.data_ = nullptr;
}
~Array() {
delete [] data_;
}
public:
int *data_;
int size_;
};
这么做有2个问题:
不优雅,表示移动语义还需要一个额外的参数(或者其他方式)。
无法实现!temp_array是个const左值引用,无法被修改,所以temp_array.data_ = nullptr;这行会编译不过。
当然函数参数可以改成非const:Array(Array& temp_array, bool move){...},这样也有问题,由于左值引用不能接右值,
Array a = Array(Array(), true);这种调用方式就没法用了。
class Array {
public:
......
// 优雅
Array(Array&& temp_array) {
data_ = temp_array.data_;
size_ = temp_array.size_;
// 为防止temp_array析构时delete data,提前置空其data_
temp_array.data_ = nullptr;
}
public:
int *data_;
int size_;
};
如何使用:
// 例1:Array用法
int main(){
Array a;
// 做一些操作
.....
// 左值a,用std::move转化为右值
Array b(std::move(a));
}
实例:vector::push_back使用std::move提高性能
#include <string>
#include <memory>
#include <vector>
#include <iostream>
int main()
{
std::string str1 = "wearmheart";
std::vector<std::string> vec;
vec.push_back(str1); // 传统方法,copy
vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值
vec.emplace_back("wearmheart"); // 当然可以直接接右值
// std::vector方法定义
// void push_back (const value_type& val);
// void push_back (value_type&& val);
// void emplace_back (Args&&... args);
return 0;
}
在vector和string这个场景,加个std::move会调用到移动语义函数,避免了深拷贝。
除非设计不允许移动,STL类大都支持移动语义函数,即可移动的。 另外,编译器会默认在用户自定义的class和struct中生成移动语义函数,但前提是用户没有主动定义该类的拷贝构造等函数(具体规则自行百度哈)。 因此,可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move触发移动语义,提升性能。
拓展
std::forward www.baidu.com/s?ie=UTF-8&…