通过自定义vector和string来理解move和forward
Posted Redamanc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过自定义vector和string来理解move和forward相关的知识,希望对你有一定的参考价值。
前言
在前面,我们分别实现了自定义的vector
和string
:
传送仓:
《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
的构造函数、allocator
的construct方法和vector
的push_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(类型完美转发)
,
看到这个名字了吗?完美
!
我们在上述改动的地方做出如下修改:
首先是allocator
的construct方法
:
template<typename Ty>
void construct(T* p, Ty&& val)
{
new (p) T(std::forward<Ty>(val));
}
接着是vector
的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
这么一句,它的功能是:
- 左值 + 右值 = 左值:&+&&=&;
- 右值 + 右值 = 右值:&&+&&=&&。
也就是说,根据Ty推演
出来的类型(可能是&
也可能是&&
)经过后面&&
的引用折叠,还是推演出来的类型本身。
之后std::forward(Ty)
会根据Ty
的类型返回左值
或者右值
;
达到完美转发的效果。
运行结果:
可以看到,结果正确!!!
以上是关于通过自定义vector和string来理解move和forward的主要内容,如果未能解决你的问题,请参考以下文章