Skip to content

Commit 62fd24a

Browse files
committed
util: add VecDeque
This is an STL-like container that interface-wise looks like std::deque, but is backed by a (fixed size, with vector-like capacity/reserve) circular buffer.
1 parent 1040a1f commit 62fd24a

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed

src/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ BITCOIN_CORE_H = \
333333
util/translation.h \
334334
util/types.h \
335335
util/ui_change_type.h \
336+
util/vecdeque.h \
336337
util/vector.h \
337338
validation.h \
338339
validationinterface.h \

src/util/vecdeque.h

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
// Copyright (c) The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_UTIL_VECDEQUE_H
6+
#define BITCOIN_UTIL_VECDEQUE_H
7+
8+
#include <util/check.h>
9+
10+
#include <cstring>
11+
#include <memory>
12+
13+
/** Data structure largely mimicking std::deque, but using single preallocated ring buffer.
14+
*
15+
* - More efficient and better memory locality than std::deque.
16+
* - Most operations ({push_,pop_,emplace_,}{front,back}(), operator[], ...) are O(1),
17+
* unless reallocation is needed (in which case they are O(n)).
18+
* - Supports reserve(), capacity(), shrink_to_fit() like vectors.
19+
* - No iterator support.
20+
* - Data is not stored in a single contiguous block, so no data().
21+
*/
22+
template<typename T>
23+
class VecDeque
24+
{
25+
/** Pointer to allocated memory. Can contain constructed and uninitialized T objects. */
26+
T* m_buffer{nullptr};
27+
/** m_buffer + m_offset points to first object in queue. m_offset = 0 if m_capacity is 0;
28+
* otherwise 0 <= m_offset < m_capacity. */
29+
size_t m_offset{0};
30+
/** Number of objects in the container. 0 <= m_size <= m_capacity. */
31+
size_t m_size{0};
32+
/** The size of m_buffer, expressed as a multiple of the size of T. */
33+
size_t m_capacity{0};
34+
35+
/** Returns the number of populated objects between m_offset and the end of the buffer. */
36+
size_t FirstPart() const noexcept { return std::min(m_capacity - m_offset, m_size); }
37+
38+
void Reallocate(size_t capacity)
39+
{
40+
Assume(capacity >= m_size);
41+
Assume((m_offset == 0 && m_capacity == 0) || m_offset < m_capacity);
42+
// Allocate new buffer.
43+
T* new_buffer = capacity ? std::allocator<T>().allocate(capacity) : nullptr;
44+
if (capacity) {
45+
if constexpr (std::is_trivially_copyable_v<T>) {
46+
// When T is trivially copyable, just copy the data over from old to new buffer.
47+
size_t first_part = FirstPart();
48+
if (first_part != 0) {
49+
std::memcpy(new_buffer, m_buffer + m_offset, first_part * sizeof(T));
50+
}
51+
if (first_part != m_size) {
52+
std::memcpy(new_buffer + first_part, m_buffer, (m_size - first_part) * sizeof(T));
53+
}
54+
} else {
55+
// Otherwise move-construct in place in the new buffer, and destroy old buffer objects.
56+
size_t old_pos = m_offset;
57+
for (size_t new_pos = 0; new_pos < m_size; ++new_pos) {
58+
std::construct_at(new_buffer + new_pos, std::move(*(m_buffer + old_pos)));
59+
std::destroy_at(m_buffer + old_pos);
60+
++old_pos;
61+
if (old_pos == m_capacity) old_pos = 0;
62+
}
63+
}
64+
}
65+
// Deallocate old buffer and update housekeeping.
66+
std::allocator<T>().deallocate(m_buffer, m_capacity);
67+
m_buffer = new_buffer;
68+
m_offset = 0;
69+
m_capacity = capacity;
70+
Assume((m_offset == 0 && m_capacity == 0) || m_offset < m_capacity);
71+
}
72+
73+
/** What index in the buffer does logical entry number pos have? */
74+
size_t BufferIndex(size_t pos) const noexcept
75+
{
76+
Assume(pos < m_capacity);
77+
// The expression below is used instead of the more obvious (pos + m_offset >= m_capacity),
78+
// because the addition there could in theory overflow with very large deques.
79+
if (pos >= m_capacity - m_offset) {
80+
return (m_offset + pos) - m_capacity;
81+
} else {
82+
return m_offset + pos;
83+
}
84+
}
85+
86+
/** Specialization of resize() that can only shrink. Separate so that clear() can call it
87+
* without requiring a default T constructor. */
88+
void ResizeDown(size_t size) noexcept
89+
{
90+
Assume(size <= m_size);
91+
if constexpr (std::is_trivially_destructible_v<T>) {
92+
// If T is trivially destructible, we do not need to do anything but update the
93+
// housekeeping record. Default constructor or zero-filling will be used when
94+
// the space is reused.
95+
m_size = size;
96+
} else {
97+
// If not, we need to invoke the destructor for every element separately.
98+
while (m_size > size) {
99+
std::destroy_at(m_buffer + BufferIndex(m_size - 1));
100+
--m_size;
101+
}
102+
}
103+
}
104+
105+
public:
106+
VecDeque() noexcept = default;
107+
108+
/** Resize the deque to be exactly size size (adding default-constructed elements if needed). */
109+
void resize(size_t size)
110+
{
111+
if (size < m_size) {
112+
// Delegate to ResizeDown when shrinking.
113+
ResizeDown(size);
114+
} else if (size > m_size) {
115+
// When growing, first see if we need to allocate more space.
116+
if (size > m_capacity) Reallocate(size);
117+
while (m_size < size) {
118+
std::construct_at(m_buffer + BufferIndex(m_size));
119+
++m_size;
120+
}
121+
}
122+
}
123+
124+
/** Resize the deque to be size 0. The capacity will remain unchanged. */
125+
void clear() noexcept { ResizeDown(0); }
126+
127+
/** Destroy a deque. */
128+
~VecDeque()
129+
{
130+
clear();
131+
Reallocate(0);
132+
}
133+
134+
/** Copy-assign a deque. */
135+
VecDeque& operator=(const VecDeque& other)
136+
{
137+
if (&other == this) [[unlikely]] return *this;
138+
clear();
139+
Reallocate(other.m_size);
140+
if constexpr (std::is_trivially_copyable_v<T>) {
141+
size_t first_part = other.FirstPart();
142+
Assume(first_part > 0 || m_size == 0);
143+
if (first_part != 0) {
144+
std::memcpy(m_buffer, other.m_buffer + other.m_offset, first_part * sizeof(T));
145+
}
146+
if (first_part != other.m_size) {
147+
std::memcpy(m_buffer + first_part, other.m_buffer, (other.m_size - first_part) * sizeof(T));
148+
}
149+
m_size = other.m_size;
150+
} else {
151+
while (m_size < other.m_size) {
152+
std::construct_at(m_buffer + BufferIndex(m_size), other[m_size]);
153+
++m_size;
154+
}
155+
}
156+
return *this;
157+
}
158+
159+
/** Swap two deques. */
160+
void swap(VecDeque& other) noexcept
161+
{
162+
std::swap(m_buffer, other.m_buffer);
163+
std::swap(m_offset, other.m_offset);
164+
std::swap(m_size, other.m_size);
165+
std::swap(m_capacity, other.m_capacity);
166+
}
167+
168+
/** Non-member version of swap. */
169+
friend void swap(VecDeque& a, VecDeque& b) noexcept { a.swap(b); }
170+
171+
/** Move-assign a deque. */
172+
VecDeque& operator=(VecDeque&& other) noexcept
173+
{
174+
swap(other);
175+
return *this;
176+
}
177+
178+
/** Copy-construct a deque. */
179+
VecDeque(const VecDeque& other) { *this = other; }
180+
/** Move-construct a deque. */
181+
VecDeque(VecDeque&& other) noexcept { swap(other); }
182+
183+
/** Equality comparison between two deques (only compares size+contents, not capacity). */
184+
bool friend operator==(const VecDeque& a, const VecDeque& b)
185+
{
186+
if (a.m_size != b.m_size) return false;
187+
for (size_t i = 0; i < a.m_size; ++i) {
188+
if (a[i] != b[i]) return false;
189+
}
190+
return true;
191+
}
192+
193+
/** Comparison between two deques, implementing lexicographic ordering on the contents. */
194+
std::strong_ordering friend operator<=>(const VecDeque& a, const VecDeque& b)
195+
{
196+
size_t pos_a{0}, pos_b{0};
197+
while (pos_a < a.m_size && pos_b < b.m_size) {
198+
auto cmp = a[pos_a++] <=> b[pos_b++];
199+
if (cmp != 0) return cmp;
200+
}
201+
return a.m_size <=> b.m_size;
202+
}
203+
204+
/** Increase the capacity to capacity. Capacity will not shrink. */
205+
void reserve(size_t capacity)
206+
{
207+
if (capacity > m_capacity) Reallocate(capacity);
208+
}
209+
210+
/** Make the capacity equal to the size. The contents does not change. */
211+
void shrink_to_fit()
212+
{
213+
if (m_capacity > m_size) Reallocate(m_size);
214+
}
215+
216+
/** Construct a new element at the end of the deque. */
217+
template<typename... Args>
218+
void emplace_back(Args&&... args)
219+
{
220+
if (m_size == m_capacity) Reallocate((m_size + 1) * 2);
221+
std::construct_at(m_buffer + BufferIndex(m_size), std::forward<Args>(args)...);
222+
++m_size;
223+
}
224+
225+
/** Move-construct a new element at the end of the deque. */
226+
void push_back(T&& elem) { emplace_back(std::move(elem)); }
227+
228+
/** Copy-construct a new element at the end of the deque. */
229+
void push_back(const T& elem) { emplace_back(elem); }
230+
231+
/** Construct a new element at the beginning of the deque. */
232+
template<typename... Args>
233+
void emplace_front(Args&&... args)
234+
{
235+
if (m_size == m_capacity) Reallocate((m_size + 1) * 2);
236+
std::construct_at(m_buffer + BufferIndex(m_capacity - 1), std::forward<Args>(args)...);
237+
if (m_offset == 0) m_offset = m_capacity;
238+
--m_offset;
239+
++m_size;
240+
}
241+
242+
/** Copy-construct a new element at the beginning of the deque. */
243+
void push_front(const T& elem) { emplace_front(elem); }
244+
245+
/** Move-construct a new element at the beginning of the deque. */
246+
void push_front(T&& elem) { emplace_front(std::move(elem)); }
247+
248+
/** Remove the first element of the deque. Requires !empty(). */
249+
void pop_front()
250+
{
251+
Assume(m_size);
252+
std::destroy_at(m_buffer + m_offset);
253+
--m_size;
254+
++m_offset;
255+
if (m_offset == m_capacity) m_offset = 0;
256+
}
257+
258+
/** Remove the last element of the deque. Requires !empty(). */
259+
void pop_back()
260+
{
261+
Assume(m_size);
262+
std::destroy_at(m_buffer + BufferIndex(m_size - 1));
263+
--m_size;
264+
}
265+
266+
/** Get a mutable reference to the first element of the deque. Requires !empty(). */
267+
T& front() noexcept
268+
{
269+
Assume(m_size);
270+
return m_buffer[m_offset];
271+
}
272+
273+
/** Get a const reference to the first element of the deque. Requires !empty(). */
274+
const T& front() const noexcept
275+
{
276+
Assume(m_size);
277+
return m_buffer[m_offset];
278+
}
279+
280+
/** Get a mutable reference to the last element of the deque. Requires !empty(). */
281+
T& back() noexcept
282+
{
283+
Assume(m_size);
284+
return m_buffer[BufferIndex(m_size - 1)];
285+
}
286+
287+
/** Get a const reference to the last element of the deque. Requires !empty(). */
288+
const T& back() const noexcept
289+
{
290+
Assume(m_size);
291+
return m_buffer[BufferIndex(m_size - 1)];
292+
}
293+
294+
/** Get a mutable reference to the element in the deque at the given index. Requires idx < size(). */
295+
T& operator[](size_t idx) noexcept
296+
{
297+
Assume(idx < m_size);
298+
return m_buffer[BufferIndex(idx)];
299+
}
300+
301+
/** Get a const reference to the element in the deque at the given index. Requires idx < size(). */
302+
const T& operator[](size_t idx) const noexcept
303+
{
304+
Assume(idx < m_size);
305+
return m_buffer[BufferIndex(idx)];
306+
}
307+
308+
/** Test whether the contents of this deque is empty. */
309+
bool empty() const noexcept { return m_size == 0; }
310+
/** Get the number of elements in this deque. */
311+
size_t size() const noexcept { return m_size; }
312+
/** Get the capacity of this deque (maximum size it can have without reallocating). */
313+
size_t capacity() const noexcept { return m_capacity; }
314+
};
315+
316+
#endif // BITCOIN_UTIL_VECDEQUE_H

0 commit comments

Comments
 (0)