C++(28)——从自定义String和vector理解移动语义和完美转发

自定义vector和Sring的结合使用

代码如下:

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的构造函数、allocator的construct方法和vector的push_back方法都提供了带右值引用参数的版本。

那么正常来说,上述代码的运行结果应该是:
两个----------线范围内的打印,分别是左值引用的拷贝构造和右值引用的拷贝构造。

但是结果并不是像我们想象的那样。

所以问题到底出在了哪里??
为什么临时对象去匹配了左值引用呢??

还是那句话,右值引用变量本身就是一个左值.

我们对程序进行一个单步调试,试着完成探究一下:
在这里插入图片描述
这里,在接受了右值之后,val自身变成了一个左值。
于是他就会去调用带有左值引用的空间配置器中的construct去进行对象的构造:
在这里插入图片描述
后面的一步,我们也可以想到,他就会调用左值引用的拷贝构造:
在这里插入图片描述
我们也就明白了;
所以最后打印的还是CMyString(const CMyString&)!

解决上述问题的关键在于:
如何避免将一个右值在被右值引用接收后,不被转化成为一个左值。

我们需要将其转化为一个右值!!!

我们想起了之前这种写法:我们之前也见过这种写法:
使用move来实现:
将那一行代码修改为:

_allocator.construct(_last, std::move(val));

于是我们发现它调用了我们所期望的构造函数:
在这里插入图片描述
但是还是一样的道理,他又将这个左值转化成了一个右值,再次修改construct函数:

new (p) T(std::move(val));

完成正常调用;
在这里插入图片描述
终于,我们调用的是带右值引用参数的拷贝构造函数了。

总结

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

forward(类型完美转发)

那么,既然move可以做到将左值变为右值,为什么还需要forward呢?
其实原因上面也说到了,用move的前提是:
你需要知道那个地方的参数是左值,但是你想要匹配右值,这样你可以使用move(移动语义)来实现。
简单说就是你需要知道参数的类型!!!
但是现实情况是,这样的实现较为复杂,你往往不知道那个地方的参数类型。
那么有没有办法让编译器自动识别参数是个左值还是右值呢?

这也就是完美转发存在的意义

我们在上述改动的地方做出如下修改:

将两种类型的构造函数写成模板
construct

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

push_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。,就有如下强大的功能:

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

也就是说,模板的推演会推演出不同类型(可能是&也可能是&&),但是,经过引用折叠之后,推演出的还是类型本身。

之后std::forward(Ty)会根据Ty的类型返回左值或者右值,从而达到完美转发的效果。
在这里插入图片描述

小总结

  • move(左值):移动语义,得到右值类型
  • forward:类型完美转发,能够识别左值或者右值类型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值