Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/Common/include/Luau/VecDeque.h
2727 views
1
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
2
#pragma once
3
4
#include "Luau/Common.h"
5
6
#include <algorithm>
7
#include <limits>
8
#include <memory>
9
#include <new>
10
#include <stdexcept>
11
#include <type_traits>
12
#include <utility>
13
14
namespace Luau
15
{
16
// `VecDeque` is a general double-ended implementation designed as a drop-in replacement for the
17
// standard library `std::deque`. It's backed by a growable ring buffer, rather than using the
18
// segmented queue design of `std::deque` which can degrade into a linked list in the worst case.
19
// The motivation for `VecDeque` as a replacement is to maintain the asymptotic complexity of
20
// `std::deque` while reducing overall allocations and promoting better usage of the cache. Its API
21
// is intended to be compatible with `std::deque` and `std::vector` as appropriate, and as such
22
// provides corresponding method definitions and supports the use of custom allocators.
23
//
24
// `VecDeque` offers pushing and popping from both ends with an amortized O(1) complexity. It also
25
// supports `std::vector`-style random-access in O(1). The implementation of buffer resizing uses
26
// a growth factor of 1.5x to enable better memory reuse after resizing, and reduce overall memory
27
// fragmentation when using the queue.
28
//
29
// Since `VecDeque` is a ring buffer, its elements are not necessarily contiguous in memory. To
30
// describe this, we refer to the two portions of the buffer as the `head` and the `tail`. The
31
// `head` is the initial portion of the queue that is on the range `[head, capacity)` and the tail
32
// is the (optionally) remaining portion on the range `[0, head + size - capacity)` whenever the
33
// `head + size` exceeds the capacity of the buffer.
34
//
35
// `VecDeque` does not currently support iteration since its primary focus is on providing
36
// double-ended queue functionality specifically, but it can be reasonably expanded to provide
37
// an iterator if we have a use-case for one in the future.
38
template<typename T, class Allocator = std::allocator<T>>
39
class VecDeque : Allocator
40
{
41
private:
42
static_assert(std::is_nothrow_move_constructible_v<T>);
43
static_assert(std::is_nothrow_move_assignable_v<T>);
44
45
T* buffer = nullptr; // the existing allocation we have backing this queue
46
size_t buffer_capacity = 0; // the size of our allocation
47
48
size_t head = 0; // the index of the head of the queue
49
size_t queue_size = 0; // the size of the queue
50
51
void destroyElements() noexcept
52
{
53
size_t head_size =
54
std::min(queue_size, capacity() - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
55
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
56
57
// we have to destroy every element in the head portion
58
for (size_t index = head; index < head + head_size; index++)
59
buffer[index].~T();
60
61
// and any in the tail portion, if one exists
62
for (size_t index = 0; index < tail_size; index++)
63
buffer[index].~T();
64
}
65
66
bool is_full()
67
{
68
return queue_size == capacity();
69
}
70
71
void grow()
72
{
73
size_t old_capacity = capacity();
74
75
// we use a growth factor of 1.5x (plus a constant) here in order to enable the
76
// previous memory to be reused after a certain number of calls to grow.
77
// see: https://github.com/facebook/folly/blob/main/folly/docs/FBVector.md#memory-handling
78
size_t new_capacity = (old_capacity > 0) ? old_capacity * 3 / 2 + 1 : 4;
79
80
// check that it's a legal allocation
81
if (new_capacity > max_size())
82
throw std::bad_array_new_length();
83
84
// allocate a new backing buffer
85
T* new_buffer = this->allocate(new_capacity);
86
87
// we should not be growing if the capacity is not the current size
88
LUAU_ASSERT(old_capacity == queue_size);
89
90
size_t head_size =
91
std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
92
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
93
94
// move the head into the new buffer
95
if (head_size != 0)
96
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
97
98
// move the tail into the new buffer immediately after
99
if (tail_size != 0)
100
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
101
102
// destroy the old elements
103
destroyElements();
104
// deallocate the old buffer
105
this->deallocate(buffer, old_capacity);
106
107
// set up the queue to be backed by the new buffer
108
buffer = new_buffer;
109
buffer_capacity = new_capacity;
110
head = 0;
111
}
112
113
size_t logicalToPhysical(size_t pos)
114
{
115
return (head + pos) % capacity();
116
}
117
118
public:
119
VecDeque() = default;
120
121
explicit VecDeque(const Allocator& alloc) noexcept
122
: Allocator{alloc}
123
{
124
}
125
126
VecDeque(const VecDeque& other)
127
: buffer(this->allocate(other.buffer_capacity))
128
, buffer_capacity(other.buffer_capacity)
129
, head(other.head)
130
, queue_size(other.queue_size)
131
{
132
// copy the initialized contents of the other buffer to this one
133
size_t head_size = std::min(
134
other.queue_size,
135
other.buffer_capacity - other.head
136
); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
137
size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
138
139
if (head_size != 0)
140
std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer + head);
141
142
if (tail_size != 0)
143
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer);
144
}
145
146
VecDeque(const VecDeque& other, const Allocator& alloc)
147
: Allocator{alloc}
148
, buffer(this->allocate(other.buffer_capacity))
149
, buffer_capacity(other.buffer_capacity)
150
, head(other.head)
151
, queue_size(other.queue_size)
152
{
153
// copy the initialized contents of the other buffer to this one
154
size_t head_size = std::min(
155
other.queue_size,
156
other.buffer_capacity - other.head
157
); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
158
size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
159
160
if (head_size != 0)
161
std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer + head);
162
163
if (tail_size != 0)
164
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer);
165
}
166
167
VecDeque(VecDeque&& other) noexcept
168
: buffer(std::exchange(other.buffer, nullptr))
169
, buffer_capacity(std::exchange(other.buffer_capacity, 0))
170
, head(std::exchange(other.head, 0))
171
, queue_size(std::exchange(other.queue_size, 0))
172
{
173
}
174
175
VecDeque(VecDeque&& other, const Allocator& alloc) noexcept
176
: Allocator{alloc}
177
, buffer(std::exchange(other.buffer, nullptr))
178
, buffer_capacity(std::exchange(other.buffer_capacity, 0))
179
, head(std::exchange(other.head, 0))
180
, queue_size(std::exchange(other.queue_size, 0))
181
{
182
}
183
184
VecDeque(std::initializer_list<T> init, const Allocator& alloc = Allocator())
185
: Allocator{alloc}
186
{
187
buffer = this->allocate(init.size());
188
buffer_capacity = init.size();
189
queue_size = init.size();
190
191
std::uninitialized_copy(init.begin(), init.end(), buffer);
192
}
193
194
~VecDeque() noexcept
195
{
196
// destroy any elements that exist
197
destroyElements();
198
// free the allocated buffer
199
this->deallocate(buffer, buffer_capacity);
200
}
201
202
VecDeque& operator=(const VecDeque& other)
203
{
204
if (this == &other)
205
return *this;
206
207
// destroy all of the existing elements
208
destroyElements();
209
210
if (buffer_capacity < other.size())
211
{
212
// free the current buffer
213
this->deallocate(buffer, buffer_capacity);
214
215
buffer = this->allocate(other.buffer_capacity);
216
buffer_capacity = other.buffer_capacity;
217
}
218
219
size_t head_size = std::min(
220
other.queue_size,
221
other.buffer_capacity - other.head
222
); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
223
size_t tail_size = other.queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
224
225
// Assignment doesn't try to match the capacity of 'other' and thus makes the buffer contiguous
226
head = 0;
227
queue_size = other.queue_size;
228
229
if (head_size != 0)
230
std::uninitialized_copy(other.buffer + other.head, other.buffer + other.head + head_size, buffer);
231
232
if (tail_size != 0)
233
std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer + head_size);
234
235
return *this;
236
}
237
238
VecDeque& operator=(VecDeque&& other)
239
{
240
if (this == &other)
241
return *this;
242
243
// destroy all of the existing elements
244
destroyElements();
245
// free the current buffer
246
this->deallocate(buffer, buffer_capacity);
247
248
buffer = std::exchange(other.buffer, nullptr);
249
buffer_capacity = std::exchange(other.buffer_capacity, 0);
250
head = std::exchange(other.head, 0);
251
queue_size = std::exchange(other.queue_size, 0);
252
253
return *this;
254
}
255
256
Allocator get_allocator() const noexcept
257
{
258
return this;
259
}
260
261
// element access
262
263
T& at(size_t pos)
264
{
265
if (pos >= queue_size)
266
throw std::out_of_range("VecDeque");
267
268
return buffer[logicalToPhysical(pos)];
269
}
270
271
const T& at(size_t pos) const
272
{
273
if (pos >= queue_size)
274
throw std::out_of_range("VecDeque");
275
276
return buffer[logicalToPhysical(pos)];
277
}
278
279
[[nodiscard]] T& operator[](size_t pos) noexcept
280
{
281
LUAU_ASSERT(pos < queue_size);
282
283
return buffer[logicalToPhysical(pos)];
284
}
285
286
[[nodiscard]] const T& operator[](size_t pos) const noexcept
287
{
288
LUAU_ASSERT(pos < queue_size);
289
290
return buffer[logicalToPhysical(pos)];
291
}
292
293
T& front()
294
{
295
LUAU_ASSERT(!empty());
296
297
return buffer[head];
298
}
299
300
const T& front() const
301
{
302
LUAU_ASSERT(!empty());
303
304
return buffer[head];
305
}
306
307
T& back()
308
{
309
LUAU_ASSERT(!empty());
310
311
size_t back = logicalToPhysical(queue_size - 1);
312
return buffer[back];
313
}
314
315
const T& back() const
316
{
317
LUAU_ASSERT(!empty());
318
319
size_t back = logicalToPhysical(queue_size - 1);
320
return buffer[back];
321
}
322
323
// capacity
324
325
bool empty() const noexcept
326
{
327
return queue_size == 0;
328
}
329
330
size_t size() const noexcept
331
{
332
return queue_size;
333
}
334
335
size_t max_size() const noexcept
336
{
337
return std::numeric_limits<size_t>::max() / sizeof(T);
338
}
339
340
void reserve(size_t new_capacity)
341
{
342
// error if this allocation would be illegal
343
if (new_capacity > max_size())
344
throw std::length_error("too large");
345
346
size_t old_capacity = capacity();
347
348
// do nothing if we're requesting a capacity that would not cause growth
349
if (new_capacity <= old_capacity)
350
return;
351
352
size_t head_size =
353
std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
354
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
355
356
// allocate a new backing buffer
357
T* new_buffer = this->allocate(new_capacity);
358
359
// move the head into the new buffer
360
if (head_size != 0)
361
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
362
363
// move the tail into the new buffer immediately after
364
if (tail_size != 0)
365
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
366
367
// destroy all the existing elements before freeing the old buffer
368
destroyElements();
369
370
// deallocate the old buffer
371
this->deallocate(buffer, old_capacity);
372
373
// set up the queue to be backed by the new buffer
374
buffer = new_buffer;
375
buffer_capacity = new_capacity;
376
head = 0;
377
}
378
379
size_t capacity() const noexcept
380
{
381
return buffer_capacity;
382
}
383
384
void shrink_to_fit()
385
{
386
size_t old_capacity = capacity();
387
size_t new_capacity = queue_size;
388
389
if (old_capacity == new_capacity)
390
return;
391
392
size_t head_size =
393
std::min(queue_size, old_capacity - head); // how many elements are in the head portion (i.e. from the head to the end of the buffer)
394
size_t tail_size = queue_size - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front)
395
396
// allocate a new backing buffer
397
T* new_buffer = this->allocate(new_capacity);
398
399
// move the head into the new buffer
400
if (head_size != 0)
401
std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer);
402
403
// move the tail into the new buffer immediately after, if we have one
404
if (tail_size != 0)
405
std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size);
406
407
// destroy all the existing elements before freeing the old buffer
408
destroyElements();
409
// deallocate the old buffer
410
this->deallocate(buffer, old_capacity);
411
412
// set up the queue to be backed by the new buffer
413
buffer = new_buffer;
414
buffer_capacity = new_capacity;
415
head = 0;
416
}
417
418
[[nodiscard]] bool is_contiguous() const noexcept
419
{
420
// this is an overflow-safe alternative to writing `head + size <= capacity`.
421
return head <= capacity() - queue_size;
422
}
423
424
// modifiers
425
426
void clear() noexcept
427
{
428
destroyElements();
429
430
head = 0;
431
queue_size = 0;
432
}
433
434
void push_back(const T& value)
435
{
436
if (is_full())
437
grow();
438
439
size_t next_back = logicalToPhysical(queue_size);
440
new (buffer + next_back) T(value);
441
queue_size++;
442
}
443
444
void pop_back()
445
{
446
LUAU_ASSERT(!empty());
447
448
queue_size--;
449
size_t next_back = logicalToPhysical(queue_size);
450
buffer[next_back].~T();
451
}
452
453
void push_front(const T& value)
454
{
455
if (is_full())
456
grow();
457
458
head = (head == 0) ? capacity() - 1 : head - 1;
459
new (buffer + head) T(value);
460
queue_size++;
461
}
462
463
void pop_front()
464
{
465
LUAU_ASSERT(!empty());
466
467
buffer[head].~T();
468
head++;
469
queue_size--;
470
471
if (head == capacity())
472
head = 0;
473
}
474
};
475
476
} // namespace Luau
477
478