c++11 右值引用和移动语义

文章目录

    • 1. 左值引用和右值引用
    • 2. 左值引用与右值引用比较
    • 3. 右值引用使用场景和意义
      • 3.1左值引用的使用场景
      • 3.2左值引用的短板:
      • 3.3 移动构造
      • 3.4 移动赋值
      • 3.5 STL中的容器
      • 3.6 右值引用引用左值及其一些更深入的使用场景分析
      • 3.7 STL容器插入接口函数也增加了右值引用版本
    • 4. 完美转发
      • 4.1 模板中的&& 万能引用
      • 4.2 std::forward 完美转发在传参的过程中保留对象原生类型属性
      • 4.3 完美转发的使用场景

1. 左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),**我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。**定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

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

什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

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

需要注意的是
这里说函数的返回值是右值,指的是传值返回的函数,因为传值返回的函数在返回对象时返回的是对象的拷贝,这个拷贝出来的对象就是一个临时变量。

而对于左值引用返回的函数来说,这些函数返回的是左值。比如string类实现的[]运算符重载函数:

//模拟实现string类
	class string
	{
	public:
		//[]运算符重载(可读可写)
		char& operator[](size_t i)
		{
			assert(i < _size); //检测下标的合法性
			return _str[i]; //返回对应字符
		}
		//...
	private:
		char* _str;       //存储字符串
		size_t _size;     //记录字符串当前的有效长度
		//...
	};

需要注意的是
右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& 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;
}

2. 左值引用与右值引用比较

左值引用总结:

  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以后的左值,那么该左值可能被用来移动构造或者移动赋值,该左值可能有被交换的风险,慎用。

3. 右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
我们通过自己模拟实现string类进行分析。

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)
		:_str(nullptr)
	{
		cout << "string(const string& s) -- 深拷贝" << endl;
		string tmp(s._str);
		swap(tmp);
	}
	// 赋值重载
	string& operator=(const string& s)
	{
		cout << "string& operator=(string s) -- 深拷贝" << endl;
		string tmp(s);
		swap(tmp);
		return *this;
	}
	// 移动构造
	string(string&& s)
		:_str(nullptr)
		, _size(0)
		, _capacity(0)
	{
		cout << "string(string&& s) -- 移动语义" << endl;
		swap(s);
	}
	// 移动赋值
	string& operator=(string&& s)
	{
		cout << "string& operator=(string&& s) -- 移动语义" << endl;
		swap(s);
		return *this;
	}
	~string()
	{
		delete[] _str;
		_str = nullptr;
	}
	char& operator[](size_t pos)
	{
		assert(pos < _size);
		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;
	size_t _size;
	size_t _capacity; // 不包含最后做标识的\0
};

3.1左值引用的使用场景

做参数和做返回值都可以提高效率。

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

3.2左值引用的短板:

左值引用虽然能避免不必要的拷贝操作,但左值引用并不能完全避免。

  • 左值引用做参数,能够完全避免传参时不必要的拷贝操作。
  • 左值引用做返回值,并不能完全避免函数返回对象时不必要的拷贝操作。

当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
如下图:
c++11 右值引用和移动语义_第1张图片

c++11 右值引用和移动语义_第2张图片

右值引用和移动语义解决上述问题:

3.3 移动构造

在string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做拷贝构造的深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己,与对象资源进行交换。
to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用
了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

c++11 右值引用和移动语义_第3张图片
这里需要说明的是,对于返回局部对象的函数,就算只是调用函数而不接收该函数的返回值,也会存在一次拷贝构造或移动构造,因为函数的返回值不管你接不接收都必须要有,而当函数结束后该函数内的局部对象都会被销毁,所以就算不接收函数的返回值也会调用一次拷贝构造或移动构造生成临时对象。

3.4 移动赋值

移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,所以它叫移动赋值,就是窃取别人的资源来赋值给自己的意思。

// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动语义" << endl;
	swap(s);
	return *this;
}
int main()
{
	string ret1;
	ret1 = to_string(1234);
	return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象
接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是
我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时
对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。

3.5 STL中的容器

STL中的容器都是增加了移动构造和移动赋值:
c++11 右值引用和移动语义_第4张图片

3.6 右值引用引用左值及其一些更深入的使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能
真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过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);
}

说明一下:

  • move函数中_Arg参数的类型不是右值引用,而是万能引用。万能引用跟右值引用的形式一样,但是右值引用需要是确定的类型。
  • 一个左值被move以后,它的资源可能就被转移给别人了,因此要慎用一个被move后的左值。

3.7 STL容器插入接口函数也增加了右值引用版本

void push_back (value_type&& val);
int main()
{
	list<string> lt;
	string s1("1111");
	// 这里调用的是拷贝构造
	lt.push_back(s1);
	// 下面调用都是移动构造
	lt.push_back("2222");
	lt.push_back(std::move(s1));
	return 0;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义
  • list容器的push_back函数需要先构造一个结点,然后将该结点插入到底层的双链表当中。
  • 在c++11之前都会去调用拷贝构造
  • c++11之后,上述代码中的插入第一个元素时就会匹配到push_back的左值引用版本,在push_back函数内部就会调用string的拷贝构造函数进行深拷贝,而插入后面两个元素时由于传入的是右值,因此会匹配到push_back的右值引用版本,此时在push_back函数内部就会调用string的移动构造函数进行资源的转移。

c++11 右值引用和移动语义_第5张图片

4. 完美转发

4.1 模板中的&& 万能引用

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。比如:

template<class T>
void PerfectForward(T&& t)
{
	//...
}
  • 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
  • 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
  • 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
  • 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

下面重载了四个Func函数,这四个Func函数的参数类型分别是左值引用、const左值引用、右值引用和const右值引用。在主函数中调用PerfectForward函数时分别传入左值、右值、const左值和const右值,在PerfectForward函数中再调用Func函数。如下:

void Func(int& x)
{
	cout << "左值引用" << endl;
}
void Func(const int& x)
{
	cout << "const 左值引用" << endl;
}
void Func(int&& x)
{
	cout << "右值引用" << endl;
}
void Func(const int&& x)
{
	cout << "const 右值引用" << endl;
}
template<class T>
void PerfectForward(T&& t)
{
	Func(t);
}
int main()
{
	int a = 10;
	PerfectForward(a);       //左值
	PerfectForward(move(a)); //右值

	const int b = 20;
	PerfectForward(b);       //const 左值
	PerfectForward(move(b)); //const 右值

	return 0;
}

由于PerfectForward函数的参数类型是万能引用,因此既可以接收左值也可以接收右值,而我们在PerfectForward函数中调用Func函数,就是希望调用PerfectForward函数时传入左值、右值、const左值、const右值,能够匹配到对应版本的Func函数。

  • 但实际调用PerfectForward函数时传入左值和右值,最终都匹配到了左值引用版本的Func函数,调用PerfectForward函数时传入const左值和const右值,最终都匹配到了const左值引用版本的Func函数。
  • 根本原因就是,右值被引用后会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,所以在PerfectForward函数中调用Func函数时会将t识别成左值。

也就是说,右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发。

4.2 std::forward 完美转发在传参的过程中保留对象原生类型属性

要想在参数传递过程中保持其原有的属性,需要在传参时调用forward函数。比如:

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

经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数,这就是完美转发的价值。

4.3 完美转发的使用场景

下面模拟实现了一个简化版的list类,类当中分别提供了左值引用版本和右值引用版本的push_back和insert函数。

namespace cl
{
	template<class T>
	struct ListNode
	{
		T _data;
		ListNode* _next = nullptr;
		ListNode* _prev = nullptr;
	};
	template<class T>
	class list
	{
		typedef ListNode<T> node;
	public:
		//构造函数
		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		//左值引用版本的push_back
		void push_back(const T& x)
		{
			insert(_head, x);
		}
		//右值引用版本的push_back
		void push_back(T&& x)
		{
			insert(_head, std::forward<T>(x)); //完美转发
		}
		//左值引用版本的insert
		void insert(node* pos, const T& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = x;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
		//右值引用版本的insert
		void insert(node* pos, T&& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = std::forward<T>(x); //完美转发

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
	private:
		node* _head; //指向链表头结点的指针
	};
}

说明:

  • push_back()函数和insert函数都有两个版本,左值引用版本和右值引用版本。
  • 左值引用版本的push_back()会调用左值引用版本的insert(),右值版本也一样
  • push_back()右值引用后,会将右值的属性变成左值,因此为了正确传递到同一个版本的insert,
  • 右值版本的push_back()调用insert()时需要使用完美转发,使传递下去的参数保持原来的属性。
  • 如果没有完美转发,那么右值版本的push_back()会调用左值版本的insert(),导致一次深拷贝。

模拟实现的list 与 STL:list的区别

我们来分析它们的区别:

如下图是我们模拟实现的list插入函数的两个版本,右值引用版本的新增节点后,会调用移动赋值,原因在于new会开辟空间并且调用构造函数,给存在的节点赋值应该调用移动赋值重载函数,而左值版本的会调用赋值重载函数。

c++11 右值引用和移动语义_第6张图片
如下图是STL库实现list插入函数的两个版本,这里使用了malloc来开辟节点的空间,然后再去使用定位new去调用新增节点的拷贝构造,左值版本调用拷贝构造,右值版本调用移动构造。

c++11 右值引用和移动语义_第7张图片

你可能感兴趣的:(c++,c++)