[STL source code analysis] summary notes: clever deque

00 in front [STL source code analysis] summary note (6): the design of iterator and magical traits After mastering the b...
00 in front
01 overview
02 structure of deque
03 iterator for deque
04 deque structure and memory management
05 stack and queue
06 summary

00 in front

[STL source code analysis] summary note (6): the design of iterator and magical traits

After mastering the basic design principles of iterators, we can look at the implementation of the remaining sequential containers. At this time, we can focus more on the design of each container itself.

01 overview

deque is a continuous linear space with two-way openings, that is, it can be inserted and deleted at both ends of the head and tail. We know that vector is a continuous linear space with one-way opening. When the space is insufficient, we need to find a larger space and transplant it.

Why is deque clever? Because deque has no concept of capacity and can dynamically increase space.

But this continuity is just an "illusion" created by deque. Let's take a look at how deque realizes continuity.

Don't look at the iterator first, just the middle part.

It can be found that many pointers are controlled in the map, and each pointer points to a buffer.

When adding elements, you only need to add them in the buffer. If the boundary is reached, the pointer maintained in the map can be extended. This is why deque can expand left and right.

But it also means that to maintain the "illusion" of continuity, the iterator will make great changes (overloads) to implement operations such as + +, –.

We will explain them one by one later.

02 structure of deque

From the figure above, we have a brief understanding of the structure of deque.

The map here is actually the control center of deque (note that this map is not the map in STL). A map is a small continuous space (the bottom layer is actually a vector). Each node points to a large continuous space buffer, and its size can be specified in STL.

Directly look at the implementation:

template <class T,class Alloc=alloc,size_t BufSize=0> class deque{ public: typedef T value_type; typedef _deque_iterator<T,T&,T*,BufSiz>iterator; protected: typedef pointer* map_pointer; protected: iterator start; iterator finish; map_pointer map;//1 size_type map_size;//2 }

Look at this code against the figure above

  1. First, focus on map. Map is a pointer to the space of the control center, which contains pointers, so the type of map is a pointer to the pointer. (T**)

  2. map_size is the size of the map. For example, 8 is shown in the figure above.

  3. We can think about the size of deque in bytes.

    Map is a pointer structure: 4 bytes. map_size 4 bytes. You also need to know the size of the iterator.

    In the iterator, there are four parts represented in the figure above: cur, first, last and node. A total of 16 bytes.

    So the size of deque is 16 + 16 + 4 + 4 = 40 bytes

03 iterator for deque

Next, we can take a closer look at the iterator of deque, which is also the soul of deque. Because the internal buffer of deque is separated, if you want to make it continuous, the iterator must be able to accurately find the location of the previous or next buffer and make some special judgment.

Structure of iterator

Because the lines in the figure above are complex, the iterator is shown here separately.

template <class T,class Ref,class Ptr,size_t BufSiz> struct _deque_iterator{ typedef random_access_iterator_tag iterator_category;//1 typedef T value_type;//2 typedef Ptr pointer;//3 typedef Ref reference;//4 typedef size_t size_type; typedef ptrdiff_t difference_type;//5 typedef T** map_pointer; typedef _deque_iterator self; T* cur; T* first; T* last; map_pointer node; ... };

First of all, we mentioned the five questions that the iterator must be able to answer in the iterator.

Here we mainly focus on the pointers defined at the bottom of the iterator.

  1. The most basic function of iterator is to point to the current element, and cur is this function.
  2. first points to the head of the buffer (including spare space)
  3. last points to the next position at the end of the buffer section (including spare space). Note that both pointers refer to the buffer section.
  4. Node points to the control center, which is used to distinguish which node in the control center controls this buffer (pointer to pointer)

Go back to the above and take a look at start and finish. Here, Start refers to the above pointer of the first buffer, and finish refers to the above pointer of the last buffer.

So begin() and end() are solved.

iterator begin() iterator end()

insert() implementation

Take insert() as an example to see how the iterator jumps between buffer s.

insert(position,x) allows you to insert an element at a certain point. For deque, you need to judge where this point is closer to the left and right ends, so that fewer elements can be moved.

iterator insert(iterator position,const value_type& x){ if(position.cur==start.cur){//First, judge whether it is at the head end. If so, give it to push_front() push_front(x); return start; } else if(position.cur==finish.cur){//Then judge whether it is at the end. If so, give it to push_back() push_back(x); iterator tmp=finish; --tmp; return tmp; } else{ return insert_aux(position,x); } }

Let's take a look at inert_ What did aux () do.

template <class T,class Alloc,size_t BufSize> typename deque<T,Alloc,BufSize>::iterator deque<T,Alloc,BufSize>::insert_aux(iterator pos,const value_type& x){ difference_type index=pos-start; value_type x_copy=x; if(index<size()/2){ push_front(front());//Add an element with the same value as the first element at the front end. iterator front1=start; ++front1; iterator front2=front1; ++front2; pos=start+index; iterator pos1=pos; ++pos1; copy(front2,pos1,front1);//Move element } else{ push_back(back());//Add an element with the same value as the last element at the last end. iterator back1=finish; --back1; iterator back2=back1; --back2; pos=start+index; copy_backward(pos,back2,back1);//Move element } *pos=x_copy; return pos; }

insert_aux() first calculates which side the placement point is close to.

If it's close to the left, push the element on the left. (we don't need to look at the specific implementation of the move operation first, that is, copy a header element in the header, and then use copy to move later.)

If it is close to the right, push the element on the right to insert.

Clever overloading

When size() is returned, deque is implemented as follows:

size_type size() const { return finish-start; }

It looks very consistent with the appearance of continuous space. In fact, this is the result of overloading operator -.

Let's take a look at how operators in deque's iterators are overloaded in order to achieve continuity.

++/– operation

In the deque structure, the most important thing to consider is whether it is in the same buffer.

++Operations are divided into pre + + and post + +. We can find the difference between them in the list article.

Front++

The general implementation process is to implement the pre + + first, and then use the overloaded pre + + to implement the post++

self& operator++(){ ++cur;//Move to next element if(cur==last){//If you reach the end set_node(node+1);//Change to the next buffer cur=first; } return *this; }

Note that here is + before comparison. Because the last position is the next position of the last element.

Post++

Post + + is implemented by pre + +.

self operator++(int){ self tmp=*this;//Record original value ++*this;//plus return tmp;//Return original value }
– operation

– the operation is the same.

self& operator--(){//Front-- if(cur==first){ set_node(node-1); cur=last; } --cur; return *this; } self operator--(int){//Post-- self tmp=*this; -- *this; return tmp; }
+=/-=Operation

deque supports random access, such as allowing iterators to jump directly to several locations. The focus of consideration is still the problem of regional jump

+=
self& operator+=(differenc_type n){ difference_type offset=n+(cur-first); if(offset>=0 && offset<difference_type(buffer_size())) cur+=n; else{ difference_type node_offset=offset>0?offset/difference_type(buffer_size()):-difference_type((-offset-1)/buffer_size())-1; set_node(node+node_offset); cur=first+(offset-node_offset*difference(buffer_size())); } return *this; }

First calculate the jump distance. If it does not exceed the length of a buffer, it will directly cur move.

If the length of a buffer is exceeded, calculate the number of buffers to jump to from now on, and adjust the cur direction at the same time.

Because the size of the buffer is fixed, the number of buffers can be found by division.

-=

-=The operation is done by + =.

self& operator-=(difference_type n){ return *this+=-n; }

+=-n this operation is also very clever.

+And-

+The operations of and - are realized by + = and - = respectively.

self operator+(difference_type n) const {//+ self tmp=*this; return tmp+=n; } self operator-(difference_type n) const {//- self tmp=*this; return tmp-=n; }

04 deque structure and memory management

Finally, let's talk about the memory structure of deque. This part is quite long in the book. Let's simplify some key points of implementation.

deque defines two dedicated space configurators for configuring element size and map size

typedef simple_alloc<value_type,Alloc>data_allocator;//Configure element size typedef simple_alloc<pointer,Alloc>map_allocator;//Configure pointer size

The constructor is as follows:

deque(int n,const value_type& value):statr(),finish(),map(0),map_size(0){ fill_initialize(n,value); }

fill_initialize is responsible for generating and arranging the structure of deque.

fill_initialize has create inside_ map_ and_ Node () is responsible for arranging the structure of deque

void deque<T,Alloc,BufSize>::create_map_and_nodes(size_type num_elements){ Number of nodes required=(Number of elements/(elements per buffer)+1; to configure map; take map Keep it in the middle so that the expandability at both ends of the head and tail is the same; }

Let's first look at the situation when a buffer is full and a new buffer needs to be configured

This part is implemented in the operation function, such as push_back()

void push_back(const value_type& t){ If there are two or more spare spaces in the buffer, continue to construct; There is only one spare space left. Call push_back_aux(); } void deque< T, Alloc,BufSize>::push_back_aux(const value_type& t){ Qualified call reserve_map_at_back(); Configure new buffer; }

Another situation is that the map is insufficient and needs to be expanded

At this time, the above mentioned reserve is involved_ map_ at_ Back(), and the corresponding reserve_map_at_front() in push_ In front().
If the location of the map pointer in the map is not enough, the space will be reallocated.

In fact, the implementation depends on reallocate_map()

The difference between front and back is that when the map is kept in the middle position, the front will make more space in front than the back.

When the map is expanded, a larger space is configured. Copying and releasing is the construction process of the vector.


05 stack and queue

Stack and queue are the key contents when we learn data structure. The structure of first in first out and first in last out can facilitate the operation of many data.

In the implementation of STL, both stack and queue are actually completed through the bottom container, so they are not called containers, but "container adapters".

Adapter is one of the six components of STL.


Need attention

stack and queue specify the way of data entry and exit, so traversal operation is not provided, which means that iterator is not provided.

Therefore, these two can only manipulate elements through their exits.


stack

stack is a first in and last out structure. If deque is used as the bottom layer, its head end opening needs to be closed.

template <class T,class Sequence = deque<T>> class stack{ protected: Sequence c; public: bool empty() const; ... }

It can be seen that all operations call deque.

According to our container knowledge, we can also understand that stack can also use list and vector as the bottom layer.


queue

queue is a first in first out structure, that is, the top is added and the bottom is taken out.

The same is also achieved through deque.

template <class T,class Sequence = deque<T>> class queue{ protected: Sequence c; public: bool empty() const reference front() void push(const value_type& x) void pop() ... }

According to our container knowledge, we can also understand that stack can also use list as the bottom layer, but we can't use vector because vector can't manipulate header elements.


06 summary

deque, as the bottom layer of our commonly used stack and queue, needs to master its ingenious design method.

The control center of deque is the core of the whole, and switching between map s is also the key point.

9 November 2021, 16:19 | Views: 8773

Add new comment

For adding a comment, please log in
or create account

0 comments