按上文分析,std::move只是类型转换工具,不会对性能有好处;右值引用在作为函数形参时更具灵活性,看上去还是挺鸡肋的。他们有什么实际应用场景吗?
在实际场景中,右值引用和std::move被广泛用于在STL和自定义类中
实现移动语义,避免拷贝,从而提升程序性能。
在没有右值引用之前,一个简单的数组类通常实现如下,有构造函数、拷贝构造函数、赋值运算符重载、析构函数等。深拷贝/浅拷贝在此不做讲解。
class Array
{
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_;
}
public:
int *data_;
int size_;
};
该类的拷贝构造函数、赋值运算符重载函数已经通过使用左值引用传参来避免一次多余拷贝了,但是内部实现要深拷贝,无法避免。 这时,有人提出一个想法:是不是可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免深拷贝了,如:
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个问题:
1、不优雅
表示移动语义还需要一个额外的参数(或者其他方式)。
2、无法实现!
temp_array 是个const左值引用,无法被修改,所以 temp_array.data_ =nullptr; 这行会编译不过。当然函数参数可以改成非const: Array(Array&temp_array, bool move){…} ,这样也有问题,由于左值引用不能接右值, Array a= Array(Array(), true); 这种调用方式就没法用了。
可以发现左值引用真是用的很不爽,右值引用的出现解决了这个问题,在STL的很多容器中,都实现了以右值引用为参数的 移动构造函数 和 移动赋值重载函数 ,或者其他函数,最常见的如std::vector的 push_back 和emplace_back 。参数为左值引用意味着拷贝,为右值引用意味着移动。
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提高性能
// 例2:std::vector和std::string的实际例子
int main()
{
std::string str1 = "aacasxs";
std::vector<std::string> vec;
vec.push_back(str1); // 传统方法,copy
vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,st
vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值
vec.emplace_back("axcsddcas"); // 当然可以直接接右值
}
// std::vector方法定义
void push_back (const value_type& val);
void push_back (value_type&& val);
void emplace_back (Args&&... args);
在vector和string这个场景,加个 std::move 会调用到移动语义函数,避免了深拷贝。除非设计不允许移动,STL类大都支持移动语义函数,即 可移动的 。 另外,编译器会默认在用户自定义的 class 和 struct 中生成移动语义函数,但前提是用户没有主动定义该类的拷贝构造 等函数(具体规则自行百度哈)。 因此,可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用 std::move 触发移动语义,提升性能。
moveable_objecta = moveable_objectb;
改为:
moveable_objecta = std::move(moveable_objectb);
还有些STL类是 move-only 的,比如 unique_ptr ,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝):
std::unique_ptr<A> ptr_a = std::make_unique<A>();
std::unique_ptr<A> ptr_b = std::move(ptr_a); // unique_ptr只有‘移动赋值重载函数
std::unique_ptr<A> ptr_b = ptr_a; // 编译不通过
完美转发 std::forward
和 std::move 一样,它的兄弟 std::forward 也充满了迷惑性,虽然名字含义是转发,但他并不会做转发,同样也是做类型转换.
与move相比,forward更强大,move只能转出来右值,forward都可以。
std::forward(u)有两个参数:T与 u。 a. 当T为左值引用类型时,u将被转换为T类型的左值; b. 否则u将被转换为T类型右值。
举个例子,有main,A,B三个函数,调用关系为: main->A->B
void B(int&& ref_r)
{
ref_r = 1;
}
// A、B的入参是右值引用
// 有名字的右值引用是左值,因此ref_r是左值
void A(int&& ref_r)
{
B(ref_r); // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败
B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
B(std::forward<int>(ref_r)); // ok,std::forward的T是int类型,属于条件b,因
}
int main()
{
int a = 5;
A(std::move(a));
}
void change2(int&& ref_r)
{
ref_r = 1;
}
void change3(int& ref_l)
{
ref_l = 1;
}
// change的入参是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r)
{
change2(ref_r); // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编
change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
change2(std::forward<int &&>(ref_r)); // ok,std::forward的T是右值引用类型
change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译
change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用类型(i
// 可见,forward可以把值转换为左值或者右值
}
int main()
{
int a = 5;
change(std::move(a));
}
上边的示例在日常编程中基本不会用到, std::forward 最主要运于模版编程的参数转发中,想深入了解需要学习 万能引用(T &&) 和 引用折叠(eg:& && → ?) 等知识