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

本文介绍了C++11中的右值引用、移动语义以及std::forward的作用,展示了如何通过这些特性避免不必要的复制,优化程序性能,尤其是在处理大规模数据结构时。
摘要由CSDN通过智能技术生成

前言

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

右值引用

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

&&的作用

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

move语义

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

移动构造和拷贝构造

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

#include <iostream>
#include <string>
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 <iostream>

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;
}

/// <summary>
/// 注意,&&引用类型即可以时左值引用,也可以是右值引用
/// 引用之后,val值就变成了一个左值(需要注意)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="val"></param>
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::forward<int>y)" << std::endl;
	func(std::forward<int>(y));//std::forward<int>(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是一样的。

  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: c++中的std::move和std::forward都是用于实现完美转发的工具。 std::move是将一个左值强制转换为右值引用,从而实现将资源所有权从一个对象转移到另一个对象的目的。使用std::move可以避免不必要的复制和赋值操作,提高程序的效率。 std::forward则是用于在函数模板中实现完美转发,将参数按照原来的类型转发给下一个函数。它可以保证参数的类型和值被完美地转发,避免了不必要的拷贝和移动操作,提高了程序的效率。 总的来说,std::move和std::forward都是用于提高程序效率和避免不必要的拷贝和移动操作的工具。 ### 回答2: C++标准库中提供了两个模板函数std::move和std::forward,它们在C++11中引入,用于实现移动语义和完美转发。 std::move的作用是将一个左值强制转换为右值引用,使得该对象的所有权能够被转移,而不是进行复制或者赋值。通过调用移动构造函数或者移动赋值运算符来减少开销。移动语义C++11中的一个重要特性,它可以提高程序的效率并且使得程序更加高效。 std::forward的作用是实现完美转发,将函数参数原封不动地转发到另一个函数中,使得函数模板可以保持参数类型和实参类型一致。std::forward用于实现通用类型的泛型编程,解决了模板函数中参数类型无法确定的问题。 实际上,std::move和std::forward的实现方式都非常简单,都是使用了static_cast进行类型转换。但是它们在C++11中的引入,以及其实现的本质却给C++程序的效率提高和泛型编程提供了重要的支持。 总之,std::move和std::forwardC++11中非常重要的语言特性,它们可以帮助程序员实现移动语义和完美转发,提高程序的性能和可读性。要注意正确使用它们,以避免出现不必要的开销和错误。 ### 回答3: C++ 11中引入了两个新的特殊函数模板std::move()和std::forward(),用来实现完美转发和移动语义,提高了代码的效率和简洁性。 std::move的作用就是将一个左值转换成右值引用,将左值的所有权抢过来,但不进行任何内存拷贝。通常用于移动语义,可以提高程序的效率。用法很简单,就是std::move(左值变量)。比如,若有个vector<int> a和一个vector<int> b,我想把b中的元素全部移动到a中,可以这样写:a.insert(a.end(), std::make_move_iterator(b.begin()), std::make_move_iterator(b.end()));这里,std::make_move_iterator()是一个语法糖,将它们的元素包装成可以引用的右值。 std::forward的作用是保持参数本来的类型(左值或右值),既可以接收左值也可以接收右值,并将参数传递给其他函数,这就是所谓的完美转发。完美转发可以达到只有一个函数就可以处理所有情况的目的。用法就是std::forward<参数类型>(参数变量)。比如,若有个函数template<class T> void f(T&& t),其中参数t是万能引用,需要把t传递给其他函数g(),我们可以这样写:g(std::forward<T>(t));这样就可以达到完美转发的目的。 需要注意的是,std::move和std::forward虽然看起来相似,但作用是不同的,std::move是将左值转换成右值引用,而std::forward是维持参数的原类型,用于完美转发。同时,它们都需要加上相应的模板类型,以便让编译器进行类型推导。在使用时,需要根据情况选择合适的函数,以达到更好的效果。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值