C++ 右值引用 std::move和std::forward的使用

前言

右值引用,std::move(移动语义)和std::forward(完美转发)都是C++11里面的特性。
使用右值引用和移动语义,可以避免无谓的复制,提供了程序性能。

右值引用

在说明右值引用之前,先说下什么是左值,什么是右值。 左值是表达式结束后仍然存在的持久对象,右值是指表达式结束时就不存在的临时对象。
区分左值和右值的便捷方法是看能不能对表达式取地址,如果能则为左值,否则为右值;
将亡值是C++11新增的、与右值引用相关的表达式,比如:将要被移动的对象、T&&函数返回的值、std::move返回值和转换成T&&的类型的转换函数返回值。
C++11中的所有的值必将属于左值、将亡值、纯右值三者之一,将亡值和纯右值都属于右值。

&&的作用

右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内
存,只是该对象的一个别名。
通过右值引用的声明,该右值又“重获新生”,其生命周期其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
在这里先提一下,具有T&&val,不一定是右值引用,也可能是左值引用,具体要看val是什么值,如果val是一个左值,那么就是一个左值引用,如果是一个右值,那就是一个右值引用。这点非常让人迷糊,后面使用代码在进行说明,这里先记一下

move语义

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

移动构造和拷贝构造

下面以类的拷贝构造和移动构造函数来说明右值引用和std::move的作用

#include 
#include 
using namespace std;
class MyString {
public:
	MyString() {
		std::cout << "无参构造函数" << std::endl;
	}
	MyString(const string& ptr) :m_ptr(new string(ptr)) {
		std::cout << "有参构造函数" << std::endl;
	}
	//由于是指针,拷贝的时候需要做深拷贝,如果做浅拷贝的话,会释放2次,会导致异常发生
	MyString(const MyString& m_str) {
		if (m_str.m_ptr) {//做一个非空判断,保证安全
			m_ptr = new string(*m_str.m_ptr);
		}
		std::cout << "拷贝构造函数" << std::endl;
	}

	MyString& operator=(const MyString& other) {
		if (&other != this)//如果不是自生,做赋值操作
		{
			if (m_ptr) { //防止赋值之前已经存在资源,如果不释放就会造成内存泄漏
				delete m_ptr;
				std::cout << "copy operator= delete" << std::endl;
			}
			m_ptr = nullptr;
			if (other.m_ptr) //如果传递的为nullptr,
			{
				m_ptr = new string(*other.m_ptr);//也可能出现问题,这里不考虑,假如new失败了,m_ptr就不能返回之前的状态了。
			}
			std::cout << "拷贝赋值函数" << std::endl;
		}
		return *this;
	}
	//与拷贝构造的区别,参数不能使用const,因为参数会进行移动
	//使用的是浅拷贝,直接把之前的资源拿过来,而不是去做真正的内存开辟操作
	//使用 noexcept 修饰 是为了提供强异常保证,
	//即在移动过程中即使发生异常(资源也能够恢复为之前的状态),也能保证程序的正确性。
	MyString(MyString&& m_str) noexcept :m_ptr(m_str.m_ptr) {
		m_str.m_ptr = nullptr;//移动资源过后,设置为nullptr,不然会导致多次调用析构的delete
		std::cout << "移动构造函数" << std::endl;
	}

	MyString& operator=(MyString&& other) noexcept {
		if (&other != this)//如果不是自己,做赋值操作
		{
			if (m_ptr) { //防止赋值之前已经存在资源,如果不释放就会造成内存泄漏
				delete m_ptr;
				std::cout << "move operator= delete" << std::endl;
			}
			m_ptr = other.m_ptr;
			other.m_ptr = nullptr;//移动资源过后,设置为nullptr,不然会导致多次调用析构的delete
			std::cout << "移动赋值函数" << std::endl;
		}
		return *this;
	}

	~MyString() {
		if (m_ptr)
		{
			std::cout << "delete ptr:" << m_ptr << std::endl;
			delete m_ptr;
		}
		m_ptr = nullptr;
		std::cout << "~Mystring" << std::endl;
	}
private:
	std::string* m_ptr = nullptr;//提前设置为nullptr,防止指针生成一个随机值,释放一个无效指针报错
};
int main{
	{
	MyString a,b;//调用2次无参构造函数
	
	//先调用有参构造构造出MyString("Hello")临时对象--->会分配一次内存
	//由于MyString("Hello") 返回的对象是一个将亡值(右值),随后通过这个右值赋值给a后调用移动赋值
	//赋值完成之后,MyString("Hello")临时对象生命周期也结束了,自然会调用析构函数,
	//但是里面的new出来的指针对象会移动到a对象中,因此指针的内容是不会被释放的。
	a = MyString("Hello");//有参构造->移动赋值->析构函数(不会释放资源)
	
	//由于a是一个左值,不会有任何释放操作,仅仅调用拷贝构造函数(会分配内存)a和d都需要释放内存
	MyString d = a;//拷贝构造
	b = std::move(a);//移动赋值
	b = a; //拷贝赋值函数
}

在上面的代码里,构造了一个MyString类,里面有一个string类型的指针,同时这个类具有构造,析构,拷贝构造,拷贝赋值,移动构造,移动赋值函数。
在使用的时候,如果使用了移动赋值和移动构造,不会new对象,生成一个新的对象,而是把别人的资源给抢占过来,供自己使用,但是如果使用拷贝构造和拷贝赋值的时候,会为string类型的指针开辟一个空间,然后将内容复制过来。也就是深拷贝在使用std::move的时候可以将左值a变为右值,从而节约了拷贝的开销,这在类中具有大数据结构的时候是非常有必要的。因此右值和move语义在开发中是不可缺少的

forward 完美转发

在说明完美转发之前,我们先用一段测试代码来说明其作用。

#include 

template<class T>
void print(T& t)
{
	std::cout << "L:" << t << std::endl;
}
template<class T>
void print(T&& t)
{
	std::cout << "R:" << t << std::endl;
}

/// 
/// 注意,&&引用类型即可以时左值引用,也可以是右值引用
/// 引用之后,val值就变成了一个左值(需要注意)
/// 
/// 
/// 
template<class T>
void func(T&& val)
{
	print(val);//左
	print(std::move(val));//右
	print(std::forward<T>(val));//保持val的左值或者右值属性
}
int main {
	//a是一个右值引用,但其本身a也有内存名字,所以a变成了一个左值
	int&& a = 10;//虽然使用了&&,不要误认为a就是右值了,a这时候是一个左值了
	//int&& b = a;//由于a是左值,不能使用右值引用去引用左值了。
	int& c = a;//a是左值,可以使用左值引用去引用。
	std::cout << "func(1)" << std::endl;
	func(1);//1是右值,执行fun过后之后,变成:左,右,右
	int x = 10;
	int y = 20;
	std::cout << "func(x)" << std::endl;
	func(x);//x是左值,执行fun过后之后,变成:左,右,左
	std::cout << "func(std::forwardy)" << std::endl;
	func(std::forward<int>(y));//std::forward(y)会将左值转换为右值,执行fun过后之后,变成:左,右,右
}

当传递右值1的时候,func(1)的输出为左,右,右,这就很奇怪了,明明传递了一个右值 ,为啥经过模板函数template func(T && val),val就变成了左值?,是否有什么办法完美转发这个属性呢?
对于下面这个模板函数

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

前面有提到过,对于 &&类型,既可以是一个左值引用,也可以是一个右值引用,具体要看val是什么值,在这里val其实是一个左值,而不是一个右值,即使我们传递了一个1。 对于下面代码:

int &&a = 10;
int &&b = a; //错误,a是一个左值,不能用一个右值引用去存储

a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这
是不对的。

那么有什么办法可以让这个1识别为一个右值呢,那就是使用std::forward完美转发,调用func(1),然后在调用print(std::forward(val)),就知道std::forward完美转发了其属性(如果传递的是右值,那么就会保持右值得属性,如果传递得是左值,就会保持左值的属性)。
在main函数里func(std::forward(y));这里y是一个左值,但是使用std::forward(y)变成了右值,这里需要注意以下,这时候std::forward和std::move一样了。

总结

右值引用和move语义结合起来,能够很好的实现移动语义,具体的move语义将一个左值变成一个右值,右值引用实现了移动语义,完美转发std::forward在函数调用时,能够保持变量的本来属性。但是std::forward在同一个函数调用时,会将左值变成一个右值,这跟std::move是一样的。

你可能感兴趣的:(零声-linux课程总结,C++11,move,forward,右值引用,深拷贝,浅拷贝,移动语义)