通过自定义vector和string来理解move和forward

Posted Redamanc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过自定义vector和string来理解move和forward相关的知识,希望对你有一定的参考价值。

前言

在前面,我们分别实现了自定义的vectorstring
传送仓:
《C++实现自定义vector以及allocator》
《从自定义string类型理解右值引用》

那么今天,我们就将二者结合起来:
用自定义的vector来存储自定义的string
看看又会有什么问题产生。

自定义vector和string的结合

我们将两个自定义的代码融合如下:

class CMyString
{
public:
	CMyString(const char* str = nullptr)
	{
		cout << "CMyString(const char*)" << endl;
		if (str != nullptr)
		{
			mptr = new char[strlen(str) + 1];
			strcpy(mptr, str);
		}
		else
		{
			mptr = new char[1];
			*mptr = '\\0';
		}
	}
	~CMyString()
	{
		cout << "~CMyString" << endl;
		delete[]mptr;
		mptr = nullptr;
	}
	// 带左值引用参数的拷贝构造
	CMyString(const CMyString& str)
	{
		cout << "CMyString(const CMyString&)" << endl;
		mptr = new char[strlen(str.mptr) + 1];
		strcpy(mptr, str.mptr);
	}
	// 带右值引用参数的拷贝构造
	CMyString(CMyString&& str) // str引用的就是一个临时对象
	{
		cout << "CMyString(CMyString&&)" << endl;
		mptr = str.mptr;
		str.mptr = nullptr;
	}
	// 带左值引用参数的赋值重载函数
	CMyString& operator=(const CMyString& str)
	{
		cout << "operator=(const CMyString&)" << endl;
		if (this == &str)
			return *this;

		delete[]mptr;

		mptr = new char[strlen(str.mptr) + 1];
		strcpy(mptr, str.mptr);
		return *this;
	}
	// 带右值引用参数的赋值重载函数
	CMyString& operator=(CMyString&& str) // 临时对象
	{
		cout << "operator=(CMyString&&)" << endl;
		if (this == &str)
			return *this;

		delete[]mptr;

		mptr = str.mptr;
		str.mptr = nullptr;
		return *this;
	}
	const char* c_str()const { return mptr; }
private:
	char* mptr;

	friend CMyString operator+(const CMyString& lhs,	const CMyString& rhs);
	friend ostream& operator<<(ostream& out, const CMyString& str);
};
CMyString operator+(const CMyString& lhs, const CMyString& rhs)
{
	CMyString tmpStr;
	tmpStr.mptr = new char[strlen(lhs.mptr) + strlen(rhs.mptr) + 1];
	strcpy(tmpStr.mptr, lhs.mptr);
	strcat(tmpStr.mptr, rhs.mptr);

	return tmpStr; 
}
ostream& operator<<(ostream& out, const CMyString& str)
{
	out << str.mptr;
	return out;
}

template<typename T>
struct Allocator
{
	T* allocate(size_t size) // 负责内存开辟
	{
		return (T*)malloc(sizeof(T) * size);
	}
	void deallocate(void* p) // 负责内存释放
	{
		free(p);
	}
	// 带有左值引用的对象构造
	void construct(T *p, const T &val) // 负责对象构造
	{
		new (p) T(val); // 定位new
	}
	// 带有右值引用的对象构造
	void construct(T *p, T &&val) // 负责对象构造
	{
		new (p) T(val); // 定位new
	}
	void destroy(T* p) // 负责对象析构
	{
		p->~T(); // ~T()代表了T类型的析构函数
	}
};

template<typename T, typename Alloc = Allocator<T>>
class vector
{
public:
	vector(int size = 10)
	{
		// 需要把内存开辟和对象构造分开处理
		_first = _allocator.allocate(size);
		_last = _first;
		_end = _first + size;
	}
	~vector()
	{
		// 析构容器有效的元素,然后释放_first指针指向的堆内存
		for (T* p = _first; p != _last; ++p)
		{
			_allocator.destroy(p); // 把_first指针指向的数组的有效元素进行析构操作
		}
		_allocator.deallocate(_first); // 释放堆上的数组内存
		_first = _last = _end = nullptr;
	}
	vector(const vector<T>& rhs)
	{
		int size = rhs._end - rhs._first;
		_first = _allocator.allocate(size);
		int len = rhs._last - rhs._first;
		for (int i = 0; i < len; ++i)
		{
			_allocator.construct(_first + i, rhs._first[i]);
		}
		_last = _first + len;
		_end = _first + size;
	}
	vector<T>& operator=(const vector<T>& rhs)
	{
		if (this == &rhs)
			return *this;

		for (T* p = _first; p != _last; ++p)
		{
			_allocator.destroy(p); // 把_first指针指向的数组的有效元素进行析构操作
		}
		_allocator.deallocate(_first);

		int size = rhs._end - rhs._first;
		_first = _allocator.allocate(size);
		int len = rhs._last - rhs._first;
		for (int i = 0; i < len; ++i)
		{
			_allocator.construct(_first + i, rhs._first[i]);
		}
		_last = _first + len;
		_end = _first + size;
		return *this;
	}
	void pop_back() // 从容器末尾删除元素
	{
		if (empty())
			return;
		// 不仅要把_last指针--,还需要析构删除的元素
		--_last;
		_allocator.destroy(_last);
	}
	T back()const // 返回容器末尾的元素的值
	{
		return *(_last - 1);
	}
	bool full()const { return _last == _end; }
	bool empty()const { return _first == _last; }
	int size()const { return _last - _first; }
	
	void push_back(const T &val) // 接收左值
	{
		if (full())
			expand();

		_allocator.construct(_last, val);
		_last++;
	}

	void push_back(T &&val) // 接收右值 
	{
		if (full())
			expand();

		_allocator.construct(_last, val);
		_last++;
	}
private:
	T* _first; // 指向数组起始的位置
	T* _last;  // 指向数组中有效元素的后继位置
	T* _end;   // 指向数组空间的后继位置
	Alloc _allocator; // 定义容器的空间配置器对象

	void expand() // 容器的二倍扩容
	{
		int size = _end - _first;
		T* ptmp = _allocator.allocate(2 * size);
		for (int i = 0; i < size; ++i)
		{
			_allocator.construct(ptmp + i, _first[i]);
		}
		for (T* p = _first; p != _last; ++p)
		{
			_allocator.destroy(p);
		}
		_allocator.deallocate(_first);
		_first = ptmp;
		_last = _first + size;
		_end = _first + 2 * size;
	}
};

测试代码

我们可以通过如下代码进行测试:

int main()
{
	CMyString str1 = "aaa";
	vector<CMyString> vec;

	cout << "-----------------------" << endl;
	vec.push_back(str1); 
	vec.push_back(CMyString("bbb"));  
	cout << "-----------------------" << endl;

	return 0;
}

可以看到,我们使用自定义的vector存储自定义的类型CMyString,在向容器里面添加push_back()的时候,一个添加了正常对象str1),一个添加了临时对象CMyString("bbb"))。

我们已经意识到临时对象对于代码效率的影响,所以在上面的融合版本中,对于CMyString构造函数allocatorconstruct方法和vectorpush_back方法都提供了带右值引用参数的版本。

那么正常来说,上述代码的运行结果应该是:
两个----------线范围内的打印,分别是左值引用的拷贝构造右值引用的拷贝构造
我们来看结果:
在这里插入图片描述
发现问题了!
明明参数是临时对象:CMyString("bbb")
为什么还是匹配的左值引用呢?

问题剖析

其实,问题就出在:
右值引用变量本身还是一个左值
什么意思呢?
在这里插入图片描述
通过调试我们发现,程序运行到这一步的时候,虽然参数接收的是一个右值,但是右值引用变量val本身却还是一个左值
在这里插入图片描述
接着运行,还是调用的带有左值引用的拷贝构造:
在这里插入图片描述
所以最后打印的还是CMyString(const CMyString&)

move(移动语义)

如何解决上述问题呢?
其实关键就在于:
在这里插入图片描述
这一步的val还是一个左值。
如何将它变成右值呢?

我们可以用move来实现:
将上面的代码改为:
_allocator.construct(_last, std::move(val));

并且将allocator中的construct(T* p, T&& val)改为:
new (p) T(std::move(val));

这样我们再次运行代码:
在这里插入图片描述
可以看到这一次就调用的是右值引用参数的拷贝构造了!

简单总结

move(移动语义)其实就是将一个左值变为右值
前提是你要知道那个地方用的是右值

forward(类型完美转发)

那么,既然move可以做到将左值变为右值,为什么还需要forward呢?
其实原因上面也说到了,用move的前提是:
你需要知道那个地方的参数是左值,但是你想要匹配右值,这样你可以使用move(移动语义)来实现。
简单说就是你需要知道参数的类型!!!

但是现实情况是,你往往不知道那个地方的参数类型。
那么有没有办法让编译器自动识别参数是个左值还是右值呢?

这就是forward(类型完美转发)
看到这个名字了吗?完美
我们在上述改动的地方做出如下修改:
首先是allocatorconstruct方法

template<typename Ty>
void construct(T* p, Ty&& val)
{
	new (p) T(std::forward<Ty>(val));
}

接着是vectorpush_back()

template<typename Ty> 
void push_back(Ty&& val)
{
	if (full())
		expand();
		
	_allocator.construct(_last, std::forward<Ty>(val));
	_last++;
}

我们可以发现,一般使用forward的时候会搭配模板来使用,并且定义的参数是:Ty&& val
这个是什么意思呢?
其实这个是模板函数类型推演+引用折叠
类型推演很好理解,根据实际参数的类型来推演出Ty的类型;
引用折叠是什么?
其实这个就是我们可以偷懒的一种方式:
如果没有应用折叠的话,我们在写代码的时候,需要提供两个版本:带左值引用参数带右值引用参数的版本。
代码非常的冗余

有了引用折叠以后,我们就可以只写Ty&& val这么一句,它的功能是:

  • 左值 + 右值 = 左值:&+&&=&;
  • 右值 + 右值 = 右值:&&+&&=&&。

也就是说,根据Ty推演出来的类型(可能是&也可能是&&)经过后面&&的引用折叠,还是推演出来的类型本身。

之后std::forward(Ty)会根据Ty的类型返回左值或者右值
达到完美转发的效果。
运行结果:
在这里插入图片描述
可以看到,结果正确!!!

以上是关于通过自定义vector和string来理解move和forward的主要内容,如果未能解决你的问题,请参考以下文章

通过自定义string类型来理解运算符重载

从自定义string类型理解右值引用

从自定义string类型理解右值引用

从自定义string类型理解右值引用

vector 3 构造 析构

理解std::move和std::forward