右值引用和move语义

发布时间:2024年01月22日


一、左值和右值

在《C Primer Plus》书中这样提到左值:左值是用于标识或定位存储位置的标签。

对于早期的 C 语言,提到左值意味着:

  1. 它指定一个对象,可以引用内存中的地址
  2. 它可用在赋值运算符的左侧

但是后来,标准中新增了 const 限定符。用 const 创建的变量不能被修改。因此,const 标识符满足上面的第1项,但是不满足第2项。一方面 C 继续把标识对象的表达式定义为左值,一方面某些左值却不能放在赋值运算符的左侧。

为此,C 标准新增了一个术语:可修改的左值,用于标识可修改的对象。所以,赋值运算符的左侧应该是可修改的左值。

右值指的是能够赋值给可修改左值的量,且本身不是左值。

所以我们总结出左值和右值的区别:

  • 左值可以取地址,可以出现在赋值符号的左边或右边
  • 右值不能取地址,不能出现在赋值符号的左边,可以出现在赋值符号的右边

二、左值引用和右值引用

传统的c++引用(现在称为左值引用)使得标识符关联到左值:

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

C++11新增了右值引用,这是用 && 表示的。右值引用可以关联到右值。

int main()
{
	double x = 1.1, y = 2.2;
	
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。

int main()
{
	 double x = 1.1, y = 2.2;
	 int&& rr1 = 10;
	 const double&& rr2 = x + y;
	 rr1 = 20;
	 rr2 = 5.5;  // 报错
	 return 0;
}

对于二者的比较:

左值引用的总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。
int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值
    
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}

右值引用的总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
int main()
{
	 // 右值引用只能右值,不能引用左值。
	 int&& r1 = 10;
	 
	 // error C2440: “初始化”: 无法从“int”转换为“int &&”
	 // message : 无法将左值绑定到右值引用
	 int a = 10;
	 int&& r2 = a;
	 
	 // 右值引用可以引用move以后的左值
	 int&& r3 = std::move(a);
	 return 0;
}

三、move语义

C++11 之前,只有 copy 语义,这对于极度关注性能的语言而言是一个重大的缺失。那时候程序员为了避免性能损失, 只好采取规避的方式。比如:

std::string str = s1;
str += s2;

这种写法就可以规避不必要的拷贝。而更加直观的写法:

std::string str = s1 + s2;

对于 move 语义的急迫需求,到了 C++11 终于被引入。其直接的驱动力很简单:在构造或者赋值时, 如果等号右侧是一个中间临时对象,应直接将其占用的资源直接 move 过来(对方就没有了)。

但问题是,如何让一个构造函数,或者赋值操作重载函数能够识别出来这是一个临时变量?

在 C++11 之前,拷贝构造和赋值重载的原型如下:

struct Foo {
   Foo(const Foo&);
   Foo& operator=(const Foo&);
};

参数类型都是 const & ,它可以匹配到三种情况:

  1. non-const lvalue reference :非const左值引用
  2. const lvalue reference :const左值引用
  3. const rvalue reference :const右值引用

对于 non-const rvalue reference 是无能为力的。 另外,即便是能捕捉 const rvalue reference , 比如: foo = Foo(10); ,但其 const 修饰也保证了其资源不可能被 move 走。

因而,能够被 move 走资源的,恰恰是之前缺失的那种引用类型: non-const rvalue reference 。

这时候,就需要有一种表示法,明确识别出那是一种 non-const rvalue reference ,最后定下来的表示法是 T&& 。 这样,就可以这样来定义不同方式的构造和赋值操作:

struct Foo {
   Foo(const Foo&);   // copy ctor
   Foo(Foo&&);        // move ctor

   Foo& operator=(const Foo&); // copy assignment
   Foo& operator=(Foo&&);      // move assignment
};

通过这样的方式,让 foo = Foo(10) 这样的表达式,都可以匹配到 move 语义的版本。 与此同时,让 foo = foo1 这样的表达式,依然使用 copy 语义的版本。

四、右值引用的作用

我们使用下面的类作为例子,分析左值引用和右值引用:

namespace tes
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "默认构造 -- string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
		{

			string tmp(s._str);
			swap(tmp);
			cout << "拷贝构造 -- string(const string& s) " << endl;

		}

		// 赋值重载
		string& operator=(string s)
		{
			cout << "赋值重载 -- string& operator=(string s) " << endl;
			//string tmp(s);
			//swap(tmp);
			swap(s);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};

	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		tes::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		std::reverse(str.begin(), str.end());
		return str;
	}
}

1.左值引用的作用和局限

左值引用做参数或返回值都可以减少拷贝,提高效率:

void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
	 tes::string s1("hello world");
	 
	 // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景
	 func1(s1);
	 func2(s1);
	 
	 // string operator+=(char ch) 传值返回存在深拷贝
	 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率

	 return 0;
}

但是左值引用也有它的局限,当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回;当存在中间临时对象时,move 语义要比 copy 语义效率更高。

2.右值引用的作用

我们进行如下调用时会发现to_string()的函数返回值是一个右值,而将这个右值赋值给ret时发生了深拷贝,所以影响了效率:

在这里插入图片描述

我们可以通过右值引用来实现移动拷贝和移动赋值提高效率,移动构造本质是将参数右值的资源窃取过来,占为已有,那么就不用做深拷贝了。移动赋值原理也一样。:

	// 移动构造
string(string&& s)
{
	cout << "string(const string& s) -- 移动拷贝" << endl;
	swap(s);
}

	// 移动赋值
string& operator=(string&& s) noexcept
{
	cout << "string& operator=(string s) -- 移动赋值" << endl;
	swap(s);
	
	return *this;
}

右值就会匹配到移动赋值函数,就直接将资源拿了过来,避免了拷贝,提高了效率。

在这里插入图片描述

我们可以通过下面的内存窗口观察其原理:s2 自己开辟了一块空间然后进行拷贝构造,而 s3 是直接将 s1 的空间抢占过来进行构造(移动构造),从而避免了拷贝。当然,我们不希望别人抢占一个像左值这样生命周期长的资源,这带来的风险与收益不成正比,所以,我们往往希望抢占一个临时的中间变量,而右值往往充当这类角色,这样就可以减少拷贝,提高效率了!

在这里插入图片描述

而在STL容器的接口中,也增加了右值引用的版本:

在这里插入图片描述

3.重谈std::move()

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
	// forward _Arg as movable
 	return ((typename remove_reference<_Ty>::type&&)_Arg);
}

4.完美转发

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }


template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10);           // 右值
	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);      // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

在这里插入图片描述

我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发:

template<typename T>
void PerfectForward(T&& t)
{
	Fun(std::forward<T>(t));
}

通过完美转发,我们可以得到 t 的原生类型属性 :

在这里插入图片描述

文章来源:https://blog.csdn.net/m0_61227789/article/details/135724354
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。