一、右值和右值引用
1. 右值
右值引用(R-value reference)是C++11的新类型,标记为&&
- 左值lvalue是
location value
的缩写,右值rvalue是real value
的缩写 - 左值:存储在内存中,有明确存储地址(可取地址)的数据
- 右值:可以提供数据值的数据(不可取地址),是一个可以生成临时对象的表达式或者是一个不可以被修改的值
即可以对表达式取地址(&)的就是左值,否则为右值。
2. 右值引用
引用
:别的变量的别名,不占用额外内存空间
右值引用本身是左值!
- 左值:
int num = 9;
- 左值引用:
int& a = num;
- 右值:
8; | x + y; | function(x, y);
- 右值引用:
int&& b = 8;
- 常量左值引用:
const int& c = num;
//使用常量左值引用引用一个常量(不能修改其值)并且该引用本身也是不可修改的 - 常量右值引用:
const int&& d = 8;
3. 初始化
- 左值引用只能给左值取别名
int& a = num;
//int &a = 10; //error
- 右值引用只能给右值取别名
int&& b = 8;
//int &&b = num; //error
int&& b = move(num);
- 常量右值引用只能通过右值初始化
const int&& d = 8;
//const int&& d = b; //error
- 常量左值引用可以通过左值、左值引用、右值、右值引用初始化
const int& c = num;
const int& e = a;
const int& f = b;
const int& g = d;
二、右值引用与左值引用的本质
1. 右值引用指向左值
使用std::move()
可以将右值引用指向左值:
int a = 5; //a是一个左值
int& ref_a_left = a; //左值引用指向右值
int&& ref_a_right = std::move(a) //通过std::move将左值转化为右值,可以被右值引用指向
cout << a; //打印结果:5
std::move()
是为了转移对象的所有权,并不是移动对象,跟生活中的移动不一样(日常生活中的移动是把物体从一个地方变动到另外一个地方),其功能是把左值强制转换为右值,让右值引用可以指向左值,其实现等同于一个类型转换:static_cast<T&&>(lvalue)
,单纯的std::move()
不会有性能提升。
右值引用能够指向右值,本质也是把右值提升为一个左值,并定义一个左值引用通过std::move
指向该左值:
int&& ref_a = 5;
ref_a = 6;
//等同于:
int temp = 5;
int&& ref_a = std::move(temp);
ref_a = 6;
2. 左值引用和右值引用的本身是左值
- 被声明出来的左值引用和右值引用都是左值,因为被声明出来出来的的左值引用和右值引用都是有地址的。
std::move
返回的右值引用inf&&
是右值- 综上,作为函数返回的右值引用&&是右值,直接声明出来的右值引用&&是左值
// 形参是右值引用
void change(int&& right_value)
{
right_value = 8;
}
int main() {
int a = 5; //左值
int& ref_a_left = a; //左值引用
int&& ref_a_right = std::move(a); //右值引用
//change(a); //编译不过,a是左值
//change(ref_a_left); //编译不过,声明出来的左值引用本身是左值
//change(ref_a_right); //编译不过,声明出来的右值引用本身是左值
change(std::move(a));
change(std::move(ref_a_left));
change(std::move(ref_a_right));
change(5);
std::cout << "a的地址:" << &a << std::endl;
std::cout << "ref_a_left的地址:" << &ref_a_left << std::endl;
std::cout << "ref_a_right的地址:" << &ref_a_right << std::endl;
}
本节结论:
- 从性能来说,左值引用和右值引用没有区别,传参使用左值引用和右值引用都可以避免拷贝
- 右值引用可以直接指向右值,也可以通过
std::move(l_value)
指向左值,左值引用只能指向左值(const左值引用
也可以指向右值) - 作为函数形参时,右值引用相比
const左值引用
更灵活,const左值引用
无法修改,有局限性。
void f1(const int& n)
{
n += 1;
}
void f2(int&& n)
{
n += 1;
}
int main()
{
//f1(5); //编译失败,const左值引用不能修改指向变量
f2(5); //编译成功
}
三、 移动语义
右值引用和std::move
被广泛用于STL和自定义类中实现移动语义,避免拷贝,从来提升程序性能。
之前C++中一个简单数组类的实现通常要经过如下步骤:构造函数、拷贝构造函数、赋值运算符重载、析构函数等。
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_;
}
private:
int *data_;
int size_;
};
Array
类的拷贝构造函数、赋值运算符重载函数已经通过使用左值引用来避免一次多余拷贝了,但是其内部实现要通过深拷贝,无法避免。
有人提出:提供一个移动构造函数,把被拷贝的数据移动过来,被拷贝数据之后就不要了,避免深拷贝:
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(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_;
}
private:
int *data_;
int size_;
};
这样写会有两个问题:
- 不优雅,表示移动语义还需要一个额外参数(或其他方式)
- 上面代码无法实现。
temp_array
是const左值引用,无法被修改,故temp_array.data_=nullptr
编译不通过。同时,若函数参数修改为非const:Array(Array& temp_array, bool move)...}
,由于左值引用不能接右值,Array = Array(Array(), true);
调用方式出错。
C++11新特性右值引用的出现就解决了这个问题,参数为左值引用意味着拷贝,为右值引用意味着移动:
class Array {
public:
......
// 优雅
Array(Array&& temp_array) {
data_ = temp_array.data_;
size_ = temp_array.size_;
// 为防止temp_array析构时delete data,提前置空其data_
temp_array.data_ = nullptr;
}
private:
int *data_;
int size_;
};
int main(){
Array a;
// 做一些操作
.....
// 左值a,用std::move转化为右值
Array b(std::move(a));
}
实例:vector::push_back使用std::move提高性能
在STL很多容器中,都实现了以右值引用为参数的移动构造函数和移动赋值重载函数,或其他函数,如std::vector
的push_back
和emplace_back
。
例子:vector和string场景,std::move
调用到移动语义函数,避免深拷贝。(注:std::move
后的被拷贝对象失去原值,例子中的str1)
int main() {
std::string str1 = "abcdef";
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"); // 直接接右值
std::cout << str1 << std::endl; //为空字符串
}
// std::vector方法定义
void push_back(const value_type& val);
void push_back(value_type&& val);
void emplace_back(Args&&... args);
同时,编译器默认在用户自定义的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::move进行类型转换
std::unique_ptr<A> ptr_c = ptr_a; //编译不通过
总结:引入右值引用,就是为了移动语义,移动语义就是为了减少拷贝。std::move
将左值转为右值引用,这样就可以重载到移动构造函数了,移动构造函数将指针赋值一下就好了,不用深拷贝了,提高性能。std::move
本身只做类型转换,对性能无影响, 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和std::move的语言特性。
四、完美转发
1. 万能引用
介绍完美转发前需要先搞清楚什么是万能引用:既可以接左值引用,也可以接右值引用
//万能引用本质是因为模板可以自动推导类型
template<typename T>
void PerfectForward(T&& t)
{
Func(t);
}
int main()
{
int a = 0;
PerfectForward(a); //传左值
PerfectForward(std::move(a)); //传右值
}
由于使用了模板参数,所以此时 PerfectForward() 接口不仅可以接收左值,而且也可以接收右值,此时该接口就被我们称之为万能引用接口。
注意区分右值引用和万能引用:
如下void fun(T && t); 中T&&并不是万能引用,因为T的类型在模板实例化时已经确定,当实例函数void fun(T && t);时 T的类型已经确定。
template<typename T>
class A
{
void fun(T&& t); //这里是右值引用
};
是万能引用:
template<typename T>
class A
{
template<typename U>
void fun(U&& u); //这里是万能引用
};
2. 为什么需要完美转发
当一个右值作为参数进行传参时,相应函数接口在接收该值时,会将该值识别成左值,因为其已被声明,有了地址。
//万能引用本质是因为模板可以自动推导类型
template<typename T>
void PerfectForward(T&& t)
{
Func(t);
}
void Func(int& x)
{
std::cout << "左值引用" << std::endl;
}
void Func(int&& x)
{
std::cout << "右值引用" << std::endl;
}
int main()
{
int a = 0;
PerfectForward(a); //传左值,打印:左值引用
PerfectForward(std::move(a)); //传右值,打印:左值引用,std::move(a)被声明为t,t有了地址被识别为了左值
}
可以发现:在实际使用万能引用时,当一个右值作为参数进行传参时,相应函数接口在接收该值时,会将该值识别成左值,那么在之后的函数调用中,无论是拷贝构造还是其它接口,都只能被识别成左值,导致最终只能调用相关左值引用实现的函数接口,无法达到预期目标(想要通过右值引用去调用移动构造),故需要万能引用将右值一直保持右值属性,关键字forward
3. 使用完美转发
使用场景:使用函数模板调用另一个函数
template<typename Func,typename T, typename U>
void PerfectForward(Func func, T&& t1, U&& t2)
{
func(t1, t2);
}
使用万能引用可以既能接收左值也能接收右值,但对于函数内部来说不管接收的是左值还是右值,模板函数内部对于形参都是左值(T&& t1=var
, t1本身是左值)。
此时如果func函数的第一个参数需要右值,我们必须这样调用func(std::move(t1), t2)
;
但模板是通用的,不能直接用std::move()
写死,这样就不能调用接收左值的函数了。
c++标准提供std::forward<>
模板类来保持参数的原有类型,代码如下:
template<typename Func,typename T, typename U>
void tempFun(Func func, T&& t1, U&& t2)
{
func(std::forward<T>(t1), std::forword<U>(t2));
}
这样传过来的参数t1、t2保持原有的类型被直接转发到函数f()中去,称为完美转发。
五、总结
- 可以对表达式取地址(&)的就是左值,否则为右值
- 被声明出来的左值引用和右值引用都是左值,因为被声明出来出来的的左值引用和右值引用都是有地址的。
std::move()
是为了转移对象的所有权,其功能是把左值强制转换为右值,让右值引用可以指向左值,其实现等同于一个类型转换:static_cast<T&&>(lvalue)
,单纯的std::move()
不会有性能提升。- 移动语义就是为了减少拷贝,
std::move
将左值转为右值引用,这样就可以重载到移动构造函数了,移动构造函数将指针赋值一下就好了,不用深拷贝了,提高性能。 - 万能引用:函数模板,既可以接左值引用,也可以接右值引用,但当一个右值作为参数进行传参时,相应函数接口在接收该值时,会将该值识别成左值
- 使用完美转发
std::forward<>
模板类来保持参数的原有类型(左值和右值)
参考:
一文读懂C++右值引用和std::move
Learning C++ No.29 【右值引用实战】
右值引用及其作用