deque
首先我们还是要讲讲deque与vector的不同,我们称deque为双端队列,理由是他可以同时在容器的结尾和开头进行O(1)的插入删除操作。
正是这一差别导致deque在内部于vector有着很大的不同。vector在扩增的时候时常不停的申请内存复制移动,但是deque却不存在这样的操作。这一行为让deque在某方面又和list有着惊人的相似,所以deque没有扩容的概念,因为他们内部实现也是向list一样,各个元素是连接起来的。
但是,deque却又允许O(1)的访问这又deque和list之间有了差别,也就是说deque内部的迭代器是随机访问迭代器。
为了让deque又如上的特性,STL内部不得不让deque的实现变得很复杂而且运行起来又相当耗费时间,所以deque的效率有时候会不如vector。什么时候deque是程序员们值得考虑的一件事情。
内部结构
依旧,我们打开deque的容器看看他包含着什么,于是我们再次发现了我们的老朋友
_Compressed_pair<_Alty, _Scary_val> _Mypair;
这东西我们见过好多次了,都知道他里面存着对控制容器至关重要的数据。而这些数据,存储在_Deque_val
中,也是_Scary_val
别名过去的东西。
在_Deque_val
内部:
private:
static constexpr size_t _Bytes = sizeof(value_type);
public:
// elements per block (a power of 2)
static constexpr int _Block_size = _Bytes <= 1 ? 16
: _Bytes <= 2 ? 8
: _Bytes <= 4 ? 4
: _Bytes <= 8 ? 2
: 1;
_Mapptr _Map; // pointer to array of pointers to blocks
size_type _Mapsize; // size of map array, zero or 2^N
size_type _Myoff; // offset of initial element
size_type _Mysize; // current length of sequence
deque使用一个指针数组用来掌控他的所有元素,这个数组,就是_Map
。注意,这个_Map
是一个二级指针,因为它是指向指针数组的指针,在_Deque_simple_types
中,我们能之间看到它的定义:
template <class _Ty>
struct _Deque_simple_types : _Simple_types<_Ty> {
using _Mapptr = _Ty**;
};
这个数组的用途是以下图示所指出的。
map中的每个指针指向一个缓冲区,那里存着deque中真正的数据。想必你已经明白为什么deque的可以如此方便在开头和结尾的插入,同时他又像list一样不需要容量的概念。
下面是_Deque_val
中各元素的解释:
- _Map:指向map指针数组。
- _Mapsize:指出了_Map数组的大小,它固定是2^n的数量。
- _Myoff:指出了deque中第一个元素的偏移量。
- _Mysize:deque容器目前的大小,即元素的数量。
之后我会详细解释map数组的扩容。
有了大致的实现我们就可以想想怎么去访问了。deque要求随机访问,就像数组的行为一样。这个责任就落在deque的iterator上。
iterator重载了一系列运算符,我挑一些重点的来说。
_NODISCARD reference operator*() const noexcept {
_Size_type _Block = _Mycont->_Getblock(_Myoff);
_Size_type _Off = _Myoff % _Block_size;
return _Mycont->_Map[_Block][_Off];
}
这个是iterator重载的*运算符,可以看到它最终是通过_Map[_Block][_Off]
来获取我们需要的元素的,这个_Block通过_Myoff算出,_Off通过求余得到。
注意:每一个iterator里面都有一个_Myoff,指出了该迭代器的偏移量。
下面是_Getblock
的内部实现:
return (_Off / _Block_size) & (_Mapsize - 1);
是的,它只有一行代码,但是却有着相当大的作用,之后在deque扩增的时候我们会再次提起它。
重载了operator*()
之后,只需要一个_Myoff
就能够返回我们需要的元素,这样,其他迭代器增加减少的操作只需要控制迭代器内部的_Myoff
即可。比如operator++(int)
_Deque_unchecked_const_iterator operator++(int) noexcept {
_Deque_unchecked_const_iterator _Tmp = *this;
++_Myoff;
return _Tmp;
}
接下来我们从一个比较特殊的头部插入开始,深入了解deque内部的运作机制。
现在假设,map的size为8,即8个缓冲区指针,假设我们用该deque存储char类型的数据,那么内部每个缓冲区能存16个char数据。
map一开始前后可能会留有空间以供用户插入使用。现在我们不停地在前面插入元素,map会移动自己_Myoff
同时向前申请新的缓冲区。但是这样地操作总会有个头,假如_Myoff==_Map
怎么办,前面没有空间可以插入了。
这种情况就是我们要需要的特殊地头部插入,我们也会就这种情况深入讨论。
首先我们来看看push_front()
吧
void push_front(const _Ty& _Val) {
emplace_front(_Val);
}
调用emplace_front()
:
template <class... _Valty>
decltype(auto) emplace_front(_Valty&&... _Val) {
_Orphan_all();
if (_Myoff() % _Block_size == 0 && _Mapsize() <= (_Mysize() + _Block_size) / _Block_size) {
_Growmap(1);
}
_Myoff() &= _Mapsize() * _Block_size - 1;
size_type _Newoff = _Myoff() != 0 ? _Myoff() : _Mapsize() * _Block_size;
size_type _Block = _Getblock(--_Newoff);
if (_Map()[_Block] == nullptr) {
_Map()[_Block] = _Getal().allocate(_Block_size);
}
_Alty_traits::construct(
_Getal(), _Unfancy(_Map()[_Block] + _Newoff % _Block_size), _STD forward<_Valty>(_Val)...);
_Myoff() = _Newoff;
++_Mysize();
#if _HAS_CXX17
return front();
#endif // _HAS_CXX17
}
我们的第一步就是研究这个:
if (_Myoff() % _Block_size == 0 && _Mapsize() <= (_Mysize() + _Block_size) / _Block_size) {
_Growmap(1);
}
_Myoff()
是_Deque_val::_Myoff
的引用,它指出了第一个元素的位置。对于第一个判断条件:
_Myoff() % _Block_size == 0
代表着第一个元素的位置正好处在某一个缓冲区的开头,而第二个条件则是查看deque中元素是否还留有最后一个空闲的位置。
之所以这样做……后面的步骤给我们了答案。
假设我们8个缓冲区占了6个多,比如目前deque中存在100个char,而前面第一个元素已经到头了。
那么根据判断(_Mysize() + _Block_size) / _Block_size
,得(100+16)/16 = 7 <= 8
所以在map的结尾,目前还有一个缓冲区未被使用,于是程序决定跳过_Growmap(1)
,暂时不分配新的map空间。
于是程序来到下面
//_Myoff在这一步不会改变
_Myoff() &= _Mapsize() * _Block_size - 1;
//_Myoff如果不等于0,那么在该缓冲区内前面一定还有空间,于是让_Newoff暂时等于_Myoff
//_Myoff如果等于0,那么第一个元素就在整个map的开头,前面没有任何空间,于是_Newoff被分配到map后面的空间
size_type _Newoff = _Myoff() != 0 ? _Myoff() : _Mapsize() * _Block_size;
//获得_Newoff对应的map_block,在我们例子中,它目前是7。
size_type _Block = _Getblock(--_Newoff);
因为_Newoff跑到了后面,所以整个map在被填满之前能够充分的利用。
这个时候你可能会想,第一个元素跑到了map中第7块缓冲区第15个char空间,假如我们要访问deque[1]、deque[2]该怎么办?
模拟一下,假如我们访问deque[2]
_NODISCARD reference operator[](size_type _Pos) noexcept /* strengthened */ {
return *(_Unchecked_begin() + static_cast<difference_type>(_Pos));
}
operator[]
使用begin()+pos的方式找到我们需要的元素,现在begin()应该返回_Newoff位置的迭代器,然后+2,这样想的话,相加后的迭代器应该超出范围了才是。
我们看看重载的operator+()
_NODISCARD _Deque_unchecked_const_iterator operator+(const difference_type _Off) const noexcept {
_Deque_unchecked_const_iterator _Tmp = *this;
_Tmp += _Off;
return _Tmp;
}
好吧,他又调用了operator+=()
,我省略之间各种调用,只展示最终调用的函数。
_Deque_unchecked_const_iterator& operator+=(const difference_type _Off) noexcept {
_Myoff += _Off;
return *this;
}
可以看到,迭代器相加最终代表着是迭代器内部的_Myoff
相加,deque[2]我们相加后的迭代器里面的_Myoff
就应该是16*8-1+2 = 129,还是超出了范围。
于是来到最后一步——*this
我们再次回到了operator*()
,还记得吗?它内部是通过_Getblock()
获得_Myoff对应的map block,我再次展示出他的实现:
return (_Off / _Block_size) & (_Mapsize - 1);
我们手动计算一下
目前_Off是129,_Off/_Block_size = 129/16=8,8&(_Mapsize-1)=8&7
也就是1000&0111 = 0
于是我们得到了_Off = 129对应的block——0,也就是第一块,然后operator*()
后面还有个求余的操作
129%16 = 1
于是我们最终得到了deque[2]的位置——_Map[0][1]
,答案是正确的。
可以看到,虽然我们第一个元素跑到了map的最后面,但是对于用户来说,deque的行为是正常的。
我们回到emplace_front()
...
//查看插入的_Block是否为空,我们的例子中,最后一个map block没被分配,所以是空的
if (_Map()[_Block] == nullptr) {
//如果是空的,分配缓冲区空间
_Map()[_Block] = _Getal().allocate(_Block_size);
}
//在缓冲区上构造插入的元素
_Alty_traits::construct(
_Getal(), _Unfancy(_Map()[_Block] + _Newoff % _Block_size), _STD forward<_Valty>(_Val)...);
//更新必要的东西
_Myoff() = _Newoff;
++_Mysize();
整个插入的过程就是这样,如果map的前面还存在空间,就直接在前面申请缓冲区然后插入,如果没有,就看map后面是否还存在空间,如果有,就在后面空间插入。
那假如……前后都没有呢?比如说现在,我们的例子第一个元素已经跑到map的最后面了,如果继续插入,那么头尾早晚会向连在一起,这样就再也没有空间让我们去插入了。
而这个时候,那个超长的if语句就会生效,执行_Growmap(1)
接下来,我们看看map是如何扩增的?当然,例子还是这个例子。
重新说一下,目前我们的map有8个block,现在里面前7个block已经满,最后一个block的最后面存储的整个deque的第一个元素。
当我们再次执行头插入操作,会导致map扩增。
void _Growmap(size_type _Count) { // grow map by at least _Count pointers, _Mapsize() a power of 2
static_assert(1 < _Minimum_map_size, "The _Xlen() test should always be performed.");
_Alpty _Almap(_Getal());
size_type _Newsize = 0 < _Mapsize() ? _Mapsize() : 1;
while (_Newsize - _Mapsize() < _Count || _Newsize < _Minimum_map_size) {
// scale _Newsize to 2^N >= _Mapsize() + _Count
if (max_size() / _Block_size - _Newsize < _Newsize) {
_Xlen(); // result too long
}
//每次map称2倍扩增
_Newsize *= 2;
}
_Count = _Newsize - _Mapsize();
//_Myboff获取第一个元素对应的block偏移量,我们例子中,它是7
size_type _Myboff = _Myoff() / _Block_size;
_Mapptr _Newmap = _Almap.allocate(_Mapsize() + _Count);
//_Myptr指出了new map中我们第一个元素的位置
_Mapptr _Myptr = _Newmap + _Myboff;
//这里是old map复制到new map的步骤
//根据参数,这一次的复制时从我们的第一个元素到old map的结尾,在我们的例子中,他只复制一个元素deque[0]
_Myptr = _STD uninitialized_copy(_Map() + _Myboff, _Map() + _Mapsize(), _Myptr);
if (_Myboff <= _Count) { // increment greater than offset of initial block
//这一步是将Old map中从_Map开头一直到第一个元素位置,在我们的例子中为deque[1]-deque[111]
//注意到它是从_Myptr之后开始复制的,所以复制完毕后我们的deque序列在map内部排序又变回了正常
_Myptr = _STD uninitialized_copy(_Map(), _Map() + _Myboff, _Myptr);
//把后面的空间清理
_Uninitialized_value_construct_n_unchecked1(_Myptr, _Count - _Myboff); // clear suffix of new
//清理前面的空间
_Uninitialized_value_construct_n_unchecked1(_Newmap, _Myboff); // clear prefix of new
} else { // increment not greater than offset of initial block
_STD uninitialized_copy(_Map(), _Map() + _Count, _Myptr); // copy more old
_Myptr = _STD uninitialized_copy(_Map() + _Count, _Map() + _Myboff, _Newmap); // copy rest of old
_Uninitialized_value_construct_n_unchecked1(_Myptr, _Count); // clear rest to initial block
}
_Destroy_range(_Map() + _Myboff, _Map() + _Mapsize());
if (_Map() != _Mapptr()) {
_Almap.deallocate(_Map(), _Mapsize()); // free storage for old
}
_Map() = _Newmap; // point at new
_Mapsize() += _Count;
}
到这里,我们对deque有了大致的了解,熟悉了内部map的运作方式,但是deque内部实现还有许许多多的细节。之后等我搞明白了再说吧……