C++笔记四(右值引用)

一、右值和右值引用

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::vectorpush_backemplace_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);

同时,编译器默认在用户自定义的classstruct中生成移动语义函数,前提是用户没有自定义该类的拷贝构造函数等函数。

可移动对象在需要拷贝且被拷贝对象之后不再被需要的场景,使用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()中去,称为完美转发。

五、总结

  1. 可以对表达式取地址(&)的就是左值,否则为右值
  2. 被声明出来的左值引用和右值引用都是左值,因为被声明出来出来的的左值引用和右值引用都是有地址的。
  3. std::move() 是为了转移对象的所有权,其功能是把左值强制转换为右值,让右值引用可以指向左值,其实现等同于一个类型转换:static_cast<T&&>(lvalue)单纯的std::move()不会有性能提升
  4. 移动语义就是为了减少拷贝,std::move将左值转为右值引用,这样就可以重载到移动构造函数了,移动构造函数将指针赋值一下就好了,不用深拷贝了,提高性能。
  5. 万能引用:函数模板,既可以接左值引用,也可以接右值引用,但当一个右值作为参数进行传参时,相应函数接口在接收该值时,会将该值识别成左值
  6. 使用完美转发 std::forward<>模板类来保持参数的原有类型(左值和右值)

参考:
一文读懂C++右值引用和std::move
Learning C++ No.29 【右值引用实战】
右值引用及其作用

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值