From scratch, build your own STL (5, deque)

Introduction to deque

A deque is a continuous linear space with two-way openings that supports random access to the elements inside. Seeing this concept, I believe everyone will think of vector. Vector is a continuous linear space with one-way opening, and the internal elements can also be accessed randomly.

The elements of deque are like this

mark

So what is the difference between deque and vector

  1. The head insertion of deque is not available in vector. Although from a technical point of view, it is not difficult to implement head insertion in vector, but the efficiency is too low, not worth it and unnecessary
  2. Except in some extreme cases, the space of the deque does not need to be reallocated. We all know that when the vector is insufficient in space, it will reallocate a new space, and then copy all the data in the original space into it. This operation is actually very inefficient. When deque has insufficient space, it only needs to allocate another space and link this space to the original space. How to link is basically the core of deque.
  3. The sorting efficiency of deque is very low, so generally copy the data of deque into vector, and then copy it back after sorting.
  4. Deque also supports random access of elements, thanks to the subtlety of its iterator design. Unlike vector, the iterator of vector itself is a pointer of element type, just because of its memory structure, it can support random access of elements

The memory structure of the deque

I believe that everyone is very interested in how deque links a new space to the original space. It was the same when I was learning deque. After I really read the source code of deque, the real feeling in my heart is that there is such an operation (manual black question mark)

Let's first look at a memory structure diagram of deque

mark

First of all, deque designs a map mapping area. This map is essentially a secondary pointer of element type, and uses a continuous space to store these secondary pointers. Each secondary pointer will point to a buffer of data. , so that when all the data areas are allocated, you only need to reallocate another piece of space, and make an area of ​​the map point to this space, so that the new space is successfully linked to the original space.

If all the space of the map has been mapped, and if you add new space at this time, the space of the map must be re-allocated. The allocation strategy is similar to that of the vector. Next, copy the pointer field in the map to the new space, and The data field does not need to be changed.

space allocation for deque

The size of the data area in the deque

After reading the above memory structure, I wonder if you have a question, how much memory should be allocated in the data area? Because the size of a piece of space in the data area is fixed, it will not grow dynamically. How much space we allocate will fix the amount of data it can store. When this space is used up, only another piece can be allocated. data area

It is designed like this in SGI's STL

inline size_t _Deque_buf_size(size_t _size)
{
    return _size < 512 ? size_t(512 / _size) : size_t(1);
}

The parameter size is the size of the element type. For example, int is 4B, which means that a data area can store 128 int data, and 512 is a magic number that is written to death. Of course, we can also pass the base 512 through the template parameter, let us decide how much to allocate

memory allocation for deque

The memory allocation method of deque is divided into two steps. Below we call the space of a data area as node
1. Allocate map
2. Allocate node

How to allocate it is the most intuitive from the source code

template<typename _Tp, typename _Alloc>
class _Deque_base
{
protected:
    iterator _m_start;          // 第一个有效node的位置
    iterator _m_finish;         // 最后一个有效node的位置
    _Tp** _m_map;
    size_t _m_map_size;
}

// map的大小默认为8, 超过8时为nodes+2 -- 使得map不会被占满
template <typename _Tp, typename _Alloc>
void _Deque_base<_Tp, _Alloc>::_M_initialize_map(size_t _num_elements)
{
    size_t _num_nodes = _num_elements / _Deque_buf_size(sizeof(_Tp)) + 1;
    _m_map_size = _S_initial_map_size > _num_nodes + 2 ? _S_initial_map_size : _num_nodes + 2;
    _m_map = _M_allocate_map(_m_map_size);

    // 使得start和finish所夹的空间尽量在整个空间的中央,使得向两边扩充的区域尽量相等
    _Tp** _start = _m_map + (_m_map_size - _num_nodes) / 2;
    _Tp** _finish = _start + _num_nodes;

    _M_create_nodes(_start, _finish);
    _m_start._M_set_node(_start);
    _m_finish._M_set_node(_finish - 1);
    _m_start._m_cur = _m_start._m_first;

    // finish.cur 指向的是有效元素的后一个位置
    _m_finish._m_cur = _m_finish._m_first + _num_elements % _Deque_buf_size(sizeof(_Tp));
}

There should be a lot of allocation details from the source code
1. First determine the number of nodes required according to the number of elements and the size of the element type
2. Determine the size of the map according to the number of nodes
3. Allocate the map
4. Start and The space clipped by finish is the center of the entire map,
5. Allocate node

Take a look at the detailed code for allocating nodes

template <typename _Tp, typename _Alloc>
void _Deque_base<_Tp, _Alloc>::_M_create_nodes(_Tp** _start, _Tp** _finish)
{
    _Tp** _cur;
    for (_cur = _start; _cur != _finish; ++_cur)
    {
        *_cur = _M_allocate_node();
    }
}

_Tp *_M_allocate_node()
{
    return _Node_alloc_type::allocate(_Deque_buf_size(sizeof(_Tp)));
}

static _TP* allocate(size_t _n)
{
    return 0 == _n ? nullptr : (_TP*)_Alloc::allocate(_n * sizeof(_TP));
}

From the code, you can see that the [start, finish) area in the map stores the pointer of each node respectively, which also realizes the map management node, and then you can directly access the node through the map.

Iterator implementation of deque

As can be seen from the memory structure of the deque, the space in the deque is not a completely continuous space like a vector, but the space in a node is continuous, so how to achieve random access to the elements in the deque? Thanks to the iterator of deque, let's take a look at the implementation of iterator

template<typename _Tp, typename _Ref, typename _Ptr>
class _Deque_iterator
{
public:
    _Tp *_m_cur;            // 指向当前node的  当前位置      
    _Tp *_m_first;          // 指向当前node的  开始位置
    _Tp *_m_last;           // 指向当前node的  最后空间的下一个位置
    _Map_pointer _m_node;   // 指向当前node所在map

    // 通过该函数来跳到另一个缓冲区
    void _M_set_node(_Map_pointer _new_node)
    {
        _m_node = _new_node;
        _m_first = *_new_node;
        _m_last = _m_first + difference_type(_S_buffer_size());
    }

    reference operator[](difference_type _index)const
    {
        return *(*this + _index);
    }

    _Self operator+(difference_type _n)const
    {
        _Self _tmp = *this;
        return _tmp += _n;
    }

    _Self& operator+=(difference_type _n)
    {
        difference_type _offset = _n + (_m_cur - _m_first);
        if (_offset >= 0 && _offset < difference_type(_S_buffer_size()))
            _m_cur += _n;
        else//需要跳到另一个node中
        {
            // 计算向上或向下的node偏移量
            difference_type _node_offset = _offset > 0 ? _offset / difference_type(_S_buffer_size()) :
                -difference_type((-_offset - 1) / _S_buffer_size()) - 1;

            _M_set_node(_m_node + _node_offset);
            _m_cur = _m_first + (_offset - _node_offset * difference_type(_S_buffer_size()));
        }
        return *this;
    }


The operation mode of random access to elements is implemented by obj[num], that is to say, we need to implement the operator[] operator. As you can see in the code, op[] is implemented by op+=(). We focus on Also look at op+=().

Explain with a picture

mark

Assuming that our offset is greater than the total space of a node, that is, n+(cur-start)>_Deque_buf_size(sizeof(_Tp)), at this time we need to adjust to the next node, which may be to jump down, or it may be It is jumping up, we will use the downward example as an example. At this time, we transfer to the next node through the set_node function and access the element we want to access. The so-called jump to the next node is to change the point in the map where the current node is located.

The construction of the deque

deque(size_type _n, const_reference _value,) : _Base(_n)
{
    _M_fill_initialize(_value);
}

Let's take a look at the space allocation and iterators we mentioned earlier through this constructor

First allocate space through the base class of deque

_Deque_base(size_t _num_elements)
    :_m_start(), _m_finish(), _m_map(nullptr), _m_map_size(0)
{
    _M_initialize_map(_num_elements);
}

// map的大小默认为8, 超过8时为nodes+2 -- 使得map不会被占满
template <typename _Tp, typename _Alloc>
void _Deque_base<_Tp, _Alloc>::_M_initialize_map(size_t _num_elements)
{
    size_t _num_nodes = _num_elements / _Deque_buf_size(sizeof(_Tp)) + 1;
    _m_map_size = _S_initial_map_size > _num_nodes + 2 ? _S_initial_map_size : _num_nodes + 2;
    _m_map = _M_allocate_map(_m_map_size);

    // 使得start和finish所夹的空间尽量在整个空间的中央,使得向两边扩充的区域尽量相等
    _Tp** _start = _m_map + (_m_map_size - _num_nodes) / 2;
    _Tp** _finish = _start + _num_nodes;

    _M_create_nodes(_start, _finish);
    _m_start._M_set_node(_start);
    _m_finish._M_set_node(_finish - 1);
    _m_start._m_cur = _m_start._m_first;

    // finish.cur 指向的是有效元素的后一个位置
    _m_finish._m_cur = _m_finish._m_first + _num_elements % _Deque_buf_size(sizeof(_Tp));
}

After allocating the space of map and node through the above code, the next step is to fill in the value of the element

template <typename _Tp, typename _Alloc>
void deque<_Tp, _Alloc>::_M_fill_initialize(const_reference _value)
{
    _map_pointer _cur;
    for (_cur = _m_start._m_node; _cur < _m_finish._m_node; ++_cur)
        uninitialized_fill(*_cur, *_cur + _S_buffer_size(), _value);

    uninitialized_fill(_m_finish._m_first, _m_finish._m_cur, _value);
}

mark

Filling is done in two steps, first fill all the nodes to be filled, and then fill the remaining nodes

map redistribution strategy of deque

void _M_reserve_map_at_back(size_type _node_to_add = 1)
{
    if (_node_to_add + 1 > _m_map_size - (_m_finish._m_node - _m_map))
        _M_reallocate_map(_node_to_add, false);
}
void _M_reserve_map_at_front(size_type _node_to_add = 1)
{
    if (_node_to_add > size_type(_m_start._m_node - _m_map))
        _M_reallocate_map(_node_to_add, true);
}

void _M_reallocate_map(size_type _node_to_add, bool _add_at_front);

The space redistribution of the map is divided into two situations:
1. When adding data to the head, it is found that the front space is insufficient
. 2. When adding data to the tail, it is found that the tail space is insufficient.

template <typename _Tp, typename _Alloc>
void deque<_Tp, _Alloc>::_M_reallocate_map(size_type _node_to_add, bool _add_at_front)
{
    size_type _old_num_nodes = _m_finish._m_node - _m_start._m_node + 1;
    size_type _new_num_nodes = _old_num_nodes + _node_to_add;

    _map_pointer _new_start;

    //  TODO: 当向某一边添加元素过多时,会导致一边的空间被占满,而另一边还有很多空间,这个时候只需要调整
    // TODO: start 和 finish的指向,使其尽量在整个空间的中间,即可满足条件。不需要重新分配空间
    if (_m_map_size > 2 * _new_num_nodes)
    {
        _new_start = _m_map + (_m_map_size - _new_num_nodes) / 2 + (_add_at_front ? _node_to_add : 0);

        if (_new_start < _m_start._m_node)
            copy(_m_start._m_node, _m_finish._m_node + 1, _new_start);
        else
            copy_backward(_m_start._m_node, _m_finish._m_node + 1, _new_start + _old_num_nodes);
    }
    else
    {
        size_type _new_map_size = _m_map_size + 
            (_m_map_size > _node_to_add ? _m_map_size : _node_to_add) + 2;

        _map_pointer _new_map = _M_allocate_map(_new_map_size);
        _new_start = _new_map + (_new_map_size - _new_num_nodes) / 2 +
            (_add_at_front ? _node_to_add : 0);

        copy(_m_start._m_node, _m_finish._m_node + 1, _new_start);
        _M_deallocate_map(_m_map);

        _m_map = _new_map;
        _m_map_size = _new_map_size;
    }
    _m_start._M_set_node(_new_start);
    _m_finish._M_set_node(_new_start + _old_num_nodes - 1);

If you find that there is insufficient space when adding data, you may not necessarily re-allocate the space of the map. If you keep adding data at the end and find that the space is insufficient, go back and check whether there is enough space in the head. If there is, directly adjust the [start , finish) position in the map, so that it is in the middle of the map, so that the space of the map is relatively balanced, and there is no need to reallocate the map space.

When the space of the map is really insufficient, the map space is reallocated, and the size is

size_type _new_map_size = _m_map_size + 
            (_m_map_size > _node_to_add ? _m_map_size : _node_to_add) + 2;

Then copy the data in the original map to the current map, of course [start, finish) is still in the center of the new map

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324688580&siteId=291194637