C++——右值引用


一、基本知识

1.作用

作用:C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。

2.右值的基本概念以及与左值的辨别

①C++11中的所有的值必将属于左值、将亡值、纯右值三者之一,将亡值和纯右值都属于右值。
左值是表达式结束后仍然存在的持久对象右值是指表达式结束时就不存在的临时对象
③区分左值和右值的便捷方法是看能不能对表达式取地址,如果能则为左值,否则为右值。
④将亡值是C++11新增的、与右值引用相关的表达式,比如:将要被移动的对象、T&&函数返回的值、std::move返回值和转换成T&&的类型的转换函数返回值。
左值是有标识符、可以取地址的表达式,最常见的情况有:变量、函数或数据成员的名字返回左值引用的表达式,如 ++x、x = 1、cout << ’ ',字符串字面量如 “hello world”。
纯右值是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:返回非引用类型的表达式,如 x++、x + 1、make_shared(42)、除字符串字面量之外的字面量,如 42、true。

3.右值引用的特性

①右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。
②无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内存,只是该对象的一个别名。
③通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
&& 的总结如下:
(1)左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值。
(2)auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。
(3)所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当 T&& 为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用。
(4)编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。

二、右值引用优化性能

对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:

#include <iostream>
using namespace std;
class A
{
public:
	A() :m_ptr(new int(0)) {
		cout << "constructor A" << endl;
	}
	~A() {
		cout << "destructor A, m_ptr:" << m_ptr << endl;
		delete m_ptr;
		m_ptr = nullptr;
	}
private:
	int* m_ptr;
};

// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;
	else
		return b;
}

int main()
{
	{
		A a = Get(false); // 运行报错
	}
	cout << "main finish" << endl;
	return 0;
}

在上面的代码中,默认构造函数是浅拷贝,main函数的 a 和Get函数的 b 会指向同一个指针 m_ptr,在析构的时候会导致重复删除该指针。解决办法就是用深拷贝。如下深拷贝代码:

//2-1-memory2
#include <iostream>
using namespace std;
class A
{
public:
	A() :m_ptr(new int(0)) {
		cout << "constructor A" << endl;
	}
	A(const A& a) :m_ptr(new int(*a.m_ptr)) {
		cout << "copy constructor A" << endl;
	}
	~A() {
		cout << "destructor A, m_ptr:" << m_ptr << endl;
		delete m_ptr;
		m_ptr = nullptr;
	}
private:
	int* m_ptr;
};

// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;
	else
		return b;
}

int main()
{
	{
		A a = Get(false); // 正确运行
	}
	cout << "main finish" << endl;
	return 0;
}

运行结果:
在这里插入图片描述
这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,
带来了额外的性能损耗。可以用右值引用来优化:

#include <iostream>
using namespace std;
class A
{
public:
	A() :m_ptr(new int(0)) {
		cout << "constructor A" << endl;
	}
	A(const A& a) :m_ptr(new int(*a.m_ptr)) {
		cout << "copy constructor A" << endl;
	}
	A(A&& a) :m_ptr(a.m_ptr) {
		a.m_ptr = nullptr;
		cout << "move constructor A" << endl;
	}
	~A() {
		cout << "destructor A, m_ptr:" << m_ptr << endl;
		if (m_ptr)
			delete m_ptr;
	}
private:
	int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;
	else
		return b;
}
int main()
{
	{
		A a = Get(false); // 正确运行
	}
	cout << "main finish" << endl;
	return 0;
}

运行结果:
在这里插入图片描述

三、移动语义(move)和完美转发(forward)

1.移动语义move

移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝

#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string>
using namespace std;
class MyString {
private:
	char* m_data;
	size_t m_len;
	void copy_data(const char* s) {
		m_data = new char[m_len + 1];
		memcpy(m_data, s, m_len);
		m_data[m_len] = '\0';
	}
public:
	MyString() {
		m_data = NULL;
		m_len = 0;
	}
	MyString(const char* p) {
		m_len = strlen(p);
		copy_data(p);
	}
	MyString(const MyString& str) {
		m_len = str.m_len;
		copy_data(str.m_data);
		cout << "Copy Constructor is called! source: " << str.m_data << endl;
	}
	MyString& operator=(const MyString& str) {
		if (this != &str) {
			m_len = str.m_len;
			copy_data(str.m_data);
		}
		cout << "Copy Assignment is called! source: " << str.m_data << endl;
		return *this;
	}
	// 用c++11的右值引用来定义这两个函数
	MyString(MyString&& str) {
		cout << "Move Constructor is called! source: " << str.m_data << endl;
		m_len = str.m_len;
		m_data = str.m_data; //避免了不必要的拷贝
		str.m_len = 0;
		str.m_data = NULL;
	}
	MyString& operator=(MyString&& str) {
		cout << "Move Assignment is called! source: " << str.m_data << endl;
		if (this != &str) {
			m_len = str.m_len;
			m_data = str.m_data; //避免了不必要的拷贝
			str.m_len = 0;
			str.m_data = NULL;
		}
		return *this;
	}
	virtual ~MyString() {
		if (m_data)
			free(m_data);
	}
};

int main()
{
	MyString a;
	a = MyString("Hello"); //无名对象,右值,Move Assignment
	MyString b = a; // Copy Constructor
	MyString c = std::move(a); // Move Constructor,将左值转为右值

	return 0;
}

运行结果:
在这里插入图片描述

2.完美转发forward

forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。所谓完美转发是指在函数模板中,完全依照模板的参数的类型保持参数的左值或者右值特性,将参数传递给函数模板中调用的另一个函数,不管参数是T&&这种未定的引用还是明确的左值引用或者右值引用,它会按照参数本来的类型转发。

(1)根据前面所描述的,下面这种引用类型既可以对左值引用,亦可以对右值引用。但要注意,引用以后,这个val值它本质上是一个左值

Template<class T>
void func(T &&val);

(2)注意下面的语句,a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这是不对的。

int &&a = 10;
int &&b = a; //错误

(3)因此我们有了std::forward()完美转发,这种T &&val中的val是左值,但如果我们用std::forward (val),就会按照参数原来的类型转发。

int &&a = 10;
int &&b = std::forward<int>(a);

(4)通过示例进一步说明完美转发

#include <iostream>
using namespace std;

template <class T>
void Print(T& t)
{
	cout << "L" << t << endl;
}

template <class T>
void Print(T&& t)
{
	cout << "R" << t << endl;
}


//既可以对左值引用,也可以对右值引用。但要注意,引用以后,这个t值它本质上是一个左值
template <class T>
void func(T&& t)
{
	Print(t);//一定是左值,因为t此时已经是一个具名的变量
	Print(std::move(t));//move(t)是右值
	Print(std::forward<T>(t));//forward(t)按照参数原来的类型转发
}

int main()
{
	cout << "-- func(1)" << endl;
	func(1);//右值
	int x = 10;
	int y = 20;
	cout << "-- func(x)" << endl;
	func(x); // x本身是左值
	cout << "-- func(std::forward<int>(y))" << endl;
	func(std::forward<int>(y)); //
	return 0;
}

运行结果:
在这里插入图片描述
func(1) :由于1是右值,所以未定的引用类型T&&t被一个右值初始化后变成了一个右值引用,但是此时t已经是一个具名的左值了。所以Print(1)是L1;Print(move(t)),此时t是一个左值,但是move(t)会返回值是右值属性,所以Print(move(t))打印出来是R1;Print(forward(t)),forward会按参数原来的类型转发,这里已经发生了类型推导,因此,它还是一个右值,所以这里的T&&不是一个未定的引用类型,会调用void PrintT(T&&t)函数打印 “R1”。

同上:
func(x)未定的引用类型T&&t被一个左值初始化后变成了一个左值引用,因此,Print(t)是L10;Print(move(t))是R10;Print(forward(x))是L10,因为forward(t)会自动类型推到参数原本的属性,x是左值。

func(forward(y))未定的引用类型T&&t被一个左值初始化后变成了一个左值引用,因此,Print(t)是L20;由于move(t)是无条件的将左值转化为右值,所以同上,Print(move(t))是R20。

最让人费解的无疑就是func(forward< int >(y)),最后func里面的Print(forward< int >(t))调用的是void Print(T&& t),打印出来R20。
先说一下引用重叠
只有右值引用与右值引用重叠才会是左值引用,而左值引用与任何引用叠加都是左值引用。如下:
T& && v 左值引用
T& & v 左值引用
T&& & v 左值引用
T&& && v 右值引用
而forward< int >(y)返回值类型为右值引用类型T&&,也就是说当我们传递给func()函数的时候,是以右值引用类型传递进去的,模板参数为引用类型T、T&&时,返回右值引用,所以func函数里的Print(forward< int >(t))进行类型推导的时候,推导出参数原类型为右值引用类型。

优秀博客点击此处

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟小胖_H

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值