STL container: deque implementation

Overview

Vector is a continuous linear space with one-way openings, and deque is a continuous linear space with two-way openings, which can perform element insertion and deletion operations at the head and tail ends respectively. Although vector can technically operate on both ends of the head and tail, but due to the characteristics of the underlying implementation of vector, the efficiency of its head operation is extremely poor, so STL does not implement this function for vector.


The biggest difference between deque and vector is that one is that deque allows insertion and deletion of headers in constant time, and the other is that deque is dynamically composed of segmented continuous spaces, and a new space can be added at any time and combined with existing ones. Spaces are linked, so things like "Reallocate a larger space due to insufficient old space, copy elements in the past and release the old space" like vector does not happen in deque.
Although deque also provides Ramdon Access Iterator, its iterator is not a normal pointer, and its complexity is much higher than that of vector. Implementation differences in iterators affect the efficiency of various algorithms, so unless necessary, use vectors instead of deques whenever possible. Some operations (such as sorting) can completely copy the elements in the deque into a vector, operate in the vector, and then copy back to the deque.

deque data structure

In order to implement a logically continuous linear space (actually a piecewise continuous linear space), the deque must define corresponding data structures to store and manage these piecewise linear spaces. As shown below:


First of all, another continuous space should be used to store the location of these segmented continuous spaces, that is, this space is used to store the starting position (pointer) of each segment of the space for storing elements. STL names this data structure map, which actually Equivalent to an array of pointers, each element of which points to another segment of contiguous linear space, called a buffer. The buffer is the main storage space of the deque, and the data we use the deque to store is placed in this segment of the buffer. SGI STL allows us to specify the size of the buffer, the default value of 0 means that a 512bytes buffer will be used.
template <typename T, typename Alloc = alloc, size_t Bufsiz = 0>
class and about {
public:
	typedef T value_type;
	typedef value_type* pointer;//Dereference to get element value
	...
protected:
	typedef pointer* map_pointer;//The pointer of the pointer, points to the buffer, dereference to get the address of the buffer


	map_pointer map;//Point to map, map is a continuous space, each element of which is a pointer (called a node), pointing to a buffer
	size_type map_size;
...
}

Second, deque's iterators must also be specially implemented. As can be inferred from the above diagram, the iterator of the deque must know where the segmented contiguous space (ie the buffer) is. Then, in order to move the iterator, it must be able to determine whether it is already at the edge of the buffer it is in, and if so, it needs to jump to the next or previous buffer when moving. In order to be able to jump correctly, the deque must have the map at all times. The iterator consists of 4 pieces of data, node points to the central controller map, first points to the first element of the buffer pointed to by *node, last points to the tail element, and cur points to the current (current access position) element in the buffer.

iterator

The iterator of deque is designed as follows:
template <typename T, typename Ref, typename Ptr, size_t BufSiz>
struct __deque_iterator { // cannot inherit std::iterator due to the special needs of deque iterators
	typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
	typedef __deque_iterator<T, const T&, T*, BufSiz> const_iterator;
	static size_t buffer_size() {return __deque_buf_size(BufSiz, sizeof(T));}

	//Not inheriting std::iterator, you must define 5 necessary iterator corresponding types by yourself
	typedef random_access_iterator_tag iterator_category;
	typedef Ptr pointer;
	typedef Ref reference;
	typedef size_t size_type;
	typedef ptrdiff_t defference_type;
	typedef T** map_pointer;

	typedef __deque_iterator self;

	//keep the connection to the container
	T* why
	T* first;
	T* last;
	map_pointer node;
...
}
The function buff_size() used to determine the buffer size calls __deque_buf_size(), which is a global function defined as follows:
inline size_t __deque_buf_size(size_t n, size_t sz) {
	//The default value is 0, which means the default buffer size is 512bytes. If it is not 0, the buffer size is user-defined
	return n != 0 ? n : (sz < 512 ? size_t(512/sz) : size_t(1));
}

deque iterator implementation

As mentioned above, deque's iterator is special, it needs to judge the boundary to "jump" the buffer operation, so its various addition and subtraction operations need to overload the pointer operator. The operation of jumping buffer is implemented by the set_node() function: void set_node( map_pointer new_node
) {
node = new_node;
first = *new_node;
last = first+difference_type(buffer_size()); For example: self& operator+=(defference_type n) { defference_type offset = n + (cur - first);//Calculate the offset if (offset >= 0 && offset < difference_type(buffer_size())) //The target is in the same buffer In the area cur += n; else { //Switch to the correct buffer difference_type node_offset = offset > 0 ? offset/difference_type(buffer_size()) : -difference_type((-offset - 1) / buffer_size()) - 1; //Subtract 1 in both places to consider the edge case of 0 set_node(node ​​+ node_offset); //Switch to the correct element











cur = first + (offset - node_offset * difference_type(buffer_size()));
}
return *this;
}
The handling of negative numbers in the above code is useful because when we define the opposite operator, we only need to change the value of n Negative;
self& operator-=(difference_type n) {
//call operator+= to complete
return *this += -n;
}

Iterator that maintains the map

In addition to maintaining a pointer to the map, deque also maintains two iterators, start and finish, which point to the first element of the first buffer and the next iterator of the last element of the last buffer (tail iterator) . This is designed to dynamically increase the size of the map. The underlying implementation of the growth is the same as the vector, which is to construct a larger space, copy the elements of the old space, and then destroy the old space. However, due to the characteristics of maintaining deque and the complexity of map, there are more factors to be considered in its code implementation.
The definition of deque is as follows:
template <typename T, typename Alloc = alloc, size_t BufSiz = 0>
class deque {
public:
typedef T value_type;
typedef value_type* pointer;
typedef size_t size_type;


typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
protected:
// pointer to the element's pointer
typedef pointer* map_pointer;


iterator start;
iterator finish;
map_pointer map;
size_type map_size;
...
}

In the source code implementation, when the deque's map is not enough, it will construct a buffer with a size of at least 8 and at most "the required buffer plus 2" (reserve one before and after, for easy expansion), and copy the elements. The difference is that since the deque can grow in both directions, the elements in the old space are placed in the middle of the new space. After copying, update the pointers in the two iterators of start and finish, and then construct the buffer, thus completing the growth process of a map.


This article is excerpted from "STL Source Code Analysis", with changes

Guess you like

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