右值引用、移动语义
一、 什么是左值、右值
首先不考虑引用以减少干扰,可以从2个角度判断:
- 左值可以取地址、位于等号左边;
- 而右值没法取地址,位于等号右边。
比如:
int num = 3; |
- num可以通过 & 取地址,位于等号左边,所以
num
是左值。 - 3位于等号右边,3没法通过 & 取地址,所以3是个右值。
再者:
class Test { public: Test(int num = 0) { _num = num; } private: int _num; }; void main(){ Test test=Test(); } |
- 同样的,test可以通过 & 取地址,位于等号左边,所以test是左值。
- Test()是个临时值,没法通过 & 取地址,位于等号右边,所以Test()是个右值。
因此,有地址的变量就是左值,没有地址的字面值、临时值就是右值。
二、什么是左值引用、右值引用
引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝,其实现原理和指针类似。
1、左值引用
顾名思义,左值引用=对左值的引用。能指向左值,不能指向右值的
int num = 3; int &ref_num = num; // 左值引用指向左值,编译通过 int &ref_num = 3; // 左值引用指向了右值,会编译失败 |
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。
但是,const左值引用是可以指向右值的:
const int &ref_num = 3; // 编译通过 |
const左值引用不会修改指向值,因此可以指向右值。
正是这个原因,函数参数的用法:void Test(const std::string& value);void Test(const std::vector<std::string>& vet)等等。
2、右值引用
同理,右值引用=对右值的引用。能指向右值,不能指向左值的。
int &&right_num = 3; // ok int num = 3; int &&left_num = num; // 编译不过,右值引用不可以指向左值 right_num = 4; // 右值引用的用途:可以修改右值 |
三、右值引用能否指向左值
在C++11中,引入了新特性:std::move
int num = 3; // num是个左值 int &left_num = num; // 左值引用指向左值 int &&right_num = std::move(num); // 通过std::move将左值转化为右值,可以被右值引用指向 cout << num; // 打印结果:3 |
本质:
- 其实std::move语义其本质并非将一个值移动到另一个值,释放原内存。
- 而是把左值强制转化为右值,让右值引用可以指向左值。
- 其实现等同于一个类型转换:
static_cast<T&&>(lvalue)
。
同理,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:
int &&ref_num = 3; ref_num = 4; 等同于以下代码: int temp = 30; int &&ref_num = std::move(temp); ref_num = 40; |
四、左值引用、右值引用本身是左值还是右值?
被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。
// 形参是个右值引用 void ChangeValue(int&& right_value) { right_value = 8; } int main() { int num = 3; // a是个左值 int &ref_a_left = num // ref_a_left是个左值引用 int &&ref_a_right = std::move(a); // ref_a_right是个右值引用 ChangeValue(num); // 编译不过,num是左值,change参数要求右值 ChangeValue(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值 ChangeValue(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值 ChangeValue(std::move(a)); // 编译通过 ChangeValue(std::move(ref_a_right)); // 编译通过 ChangeValue(std::move(ref_a_left)); // 编译通过 change(5); // 当然可以直接接右值,编译通过 // 打印这三个左值的地址,都是一样的 cout << &num << ' '; cout << &ref_a_left << ' '; cout << &ref_a_right; } |
最后,std::move会返回一个右值引用int &&
,它是左值还是右值呢?
从表达式int &&ref = std::move(num)
来看,右值引用ref
指向的必须是右值,所以move返回的int &&
是个右值。所以右值引用既可能是左值,又可能是右值吗? 确实如此:右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。
结论:
1、从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
2、右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
3、作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
代码上的实践
// 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方法,避免拷贝,str1会失去原有值,变成空字符串 vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值 vec.emplace_back("axcsddcas"); // 当然可以直接接右值 } |
在vector和string这个场景,加个std::move
会调用到移动语义函数,避免了深拷贝。
可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move
触发移动语义,提升性能。
比如:
moveable_objecta = moveable_objectb; 改为: moveable_objecta = std::move(moveable_objectb); |
还有些STL类是move-only
的,比如unique_ptr
,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝):
std::unique_ptr<Test> ptr_test1 = std::make_unique<Test>(); // unique_ptr只有‘移动赋值重载函数‘,参数是&& ,只能接右值,因此必须用std::move转换类型 std::unique_ptr<Test> ptr_test2 = std::move(ptr_test1); // 编译不通过 std::unique_ptr<Test> ptr_test2 = ptr_test1; |
std::move本身只做类型转换,对性能无影响。 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和std::move的语言特性。
此外,还有完美转发 std::forward,不过forward多用于模板,后续再补吧。。。