【C++11】右值引用和移动语义的使用场景

目录

一、左值引用主要使用场景回顾

二、移动构造和移动赋值

三、右值引用和移动语义解决传值返回问题

右值对象构造,只有拷贝构造,没有移动构造的场景

场景1

场景2​

右值对象构造,有拷贝构造,也有移动构造的场景

右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

四、右值引用和移动语义在传参中的提效



一、左值引用主要使用场景回顾

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如addStrings和generate函数,C++98中的解决方案只能是被迫使用输出型参数解决。那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。

class Solution {
public:
	// 传值返回需要拷贝
	string addStrings(string num1, string num2) {
		string str;
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		// 进位
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			str += ('0' + ret);
		}
		if (next == 1)
			str += '1';
		reverse(str.begin(), str.end());
		return str;
	}
};
class Solution {
public:
	// 这里的传值返回拷贝代价就太大了
	vector<vector<int>> generate(int numRows) {
		vector<vector<int>> vv(numRows);
		for (int i = 0; i < numRows; ++i)
		{
			vv[i].resize(i + 1, 1);
		}
		for (int i = 2; i < numRows; ++i)
		{
			for (int j = 1; j < i; ++j)
			{
				vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
			}
		}
		return vv;
	}
};

二、移动构造和移动赋值

移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。

移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。

对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的bit::string样例实现了移动构造和移动赋值,我们需要结合场景理解。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<string.h>
#include<algorithm>
using namespace std;
namespace bit

{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		const_iterator begin() const
		{
			return _str;
		}
		const_iterator end() const
		{
			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);
		}
		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;

			reserve(s._capacity);
			for (auto ch : s)
			{
				push_back(ch);
			}
		}
		// 移动构造
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 拷贝赋值" <<
				endl;
			if (this != &s)
			{
				_str[0] = '\0';
				_size = 0;
				reserve(s._capacity);
				for (auto ch : s)
				{
					push_back(ch);
				}
			}
			return *this;
		}
		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
		~string()
		{
			cout << "~string() -- 析构" << endl;
			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];
				if (_str)
				{
					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)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
			size_t size() const
		{
			return _size;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}

int main()
{
	bit::string s1("xxxxx");
	// 拷贝构造
	bit::string s2 = s1;
	// 构造+移动构造,优化后直接构造
	bit::string s3 = bit::string("yyyyy");
	// 移动构造
	bit::string s4 = move(s1);
	cout << "******************************" << endl;
	return 0;
}

三、右值引用和移动语义解决传值返回问题

namespace bit
{
	string addStrings(string num1, string num2)
	{
		string str;
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			str += ('0' + ret);
		}

		if (next == 1)
				str += '1';
		reverse(str.begin(), str.end());
		cout << "******************************" << endl;
		return str;
	}
}

// 场景1
int main()
{
	bit::string ret = bit::addStrings("11111", "2222");
	cout << ret.c_str() << endl;
	return 0;
}


// 场景2
int main()
{
	bit::string ret;
	ret = bit::addStrings("11111", "2222");
	cout << ret.c_str() << endl;
	return 0;
}

右值对象构造,只有拷贝构造,没有移动构造的场景

下图展示了vs2019 debug环境下编译器对拷贝的优化,中间为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。

需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,可以看下图。

linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elideconstructors 的方式关闭构造优化,运行结果可以看到下图左边没有优化的两次拷贝。

场景1


场景2

右值对象构造,有拷贝构造,也有移动构造的场景

图中展示了vs2019 debug环境下编译器对拷贝的优化,中间为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。

需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图3所示。

linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elideconstructors
的方式关闭构造优化,运行结果可以看到左边没有优化的两次移动。

右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

图中展示了vs2019 debug和g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。

需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

就上就是C++11增加移动构造和移动赋值前后对比

在移动构造和移动赋值出来前,编译器的优化还是很重要的,在这之后优化就变成了锦上添花,优化与否差别并不大

总结:

  • 对于:深拷贝的自定义类型:如vector/string/map... ,实现移动构造是有很大价值的
  • 对于:浅拷贝的自定义类型如:Date/pair<int,int> ...,不需要实现移动构造和移动赋值

四、右值引用和移动语义在传参中的提效

查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本

当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象

当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上

把我们之前模拟实现的bit::list拷贝过来,支持右值引用参数版本的push_back和insert

其实这里还有一个emplace系列的接口,但是这个涉及可变参数模板,我们需要把可变参数模板讲解以后再讲解emplace系列的接口。

// void push_back (const value_type& val);
// void push_back (value_type&& val);
// iterator insert (const_iterator position, value_type&& val);
// iterator insert (const_iterator position, const value_type& val);

int main()
{
	std::list<bit::string> lt;
	bit::string s1("111111111111111111111");
	lt.push_back(s1);

	cout << "*************************" << endl;

	lt.push_back(bit::string("22222222222222222222222222222"));
	cout << "*************************" << endl;

	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;

	lt.push_back(move(s1));
	cout << "*************************" << endl;
	return 0;
}

运行结果:

string(char* str)
string(const string& s) -- 拷贝构造
*************************
string(char* str)
string(string&& s) -- 移动构造
~string() -- 析构
*************************
string(char* str)
string(string&& s) -- 移动构造
~string() -- 析构
*************************
string(string&& s) -- 移动构造
*************************
~string() -- 析构
~string() -- 析构
~string() -- 析构
~string() -- 析构
~string() -- 析构

 完整代码:

// List.h
// 以下代码为了控制篇幅,把跟这里无关的接口都删除了
namespace bit
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
		ListNode(const T& data = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(data)
		{}
		ListNode(T&& data)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(data))
		{}
	};
	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{}
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		Ref operator*()
		{
			return _node->_data;
		}
		bool operator!=(const Self & it)
		{
			return _node != it._node;
		}
	};
	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;
		iterator begin()
		{
			return iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		void empty_init()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
		}
		list()
		{
			empty_init();
		}
		void push_back(const T& x)
		{
			insert(end(), x);
		}
		void push_back(T&& x)
		{
			insert(end(), move(x));
		}
		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}
		iterator insert(iterator pos, T&& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(move(x));
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}
	private:
		Node* _head;
	};
}
// Test.cpp
#include"List.h"
int main()
{
	bit::list<bit::string> lt;
	cout << "*************************" << endl;

	bit::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;

	lt.push_back(bit::string("22222222222222222222222222222"));
	cout << "*************************" << endl;

	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;

	lt.push_back(move(s1));
	cout << "*************************" << endl;

	return 0;
}

本篇完,下篇继续讲解C++11!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值