// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once #include "Common.h" #include "Luau/Common.h" #include #include #include #include #include namespace Luau { // `VecDeque` is a general double-ended implementation designed as a drop-in replacement for the // standard library `std::deque`. It's backed by a growable ring buffer, rather than using the // segmented queue design of `std::deque` which can degrade into a linked list in the worst case. // The motivation for `VecDeque` as a replacement is to maintain the asymptotic complexity of // `std::deque` while reducing overall allocations and promoting better usage of the cache. Its API // is intended to be compatible with `std::deque` and `std::vector` as appropriate, and as such // provides corresponding method definitions and supports the use of custom allocators. // // `VecDeque` offers pushing and popping from both ends with an amortized O(1) complexity. It also // supports `std::vector`-style random-access in O(1). The implementation of buffer resizing uses // a growth factor of 1.5x to enable better memory reuse after resizing, and reduce overall memory // fragmentation when using the queue. // // Since `VecDeque` is a ring buffer, its elements are not necessarily contiguous in memory. To // describe this, we refer to the two portions of the buffer as the `head` and the `tail`. The // `head` is the initial portion of the queue that is on the range `[head, capacity)` and the tail // is the (optionally) remaining portion on the range `[0, head + size - capacity)` whenever the // `head + size` exceeds the capacity of the buffer. // // `VecDeque` does not currently support iteration since its primary focus is on providing // double-ended queue functionality specifically, but it can be reasonably expanded to provide // an iterator if we have a use-case for one in the future. template> class VecDeque : Allocator { private: static_assert(std::is_nothrow_move_constructible_v); static_assert(std::is_nothrow_move_assignable_v); T* buffer = nullptr; // the existing allocation we have backing this queue size_t buffer_capacity = 0; // the size of our allocation size_t head = 0; // the index of the head of the queue size_t queue_size = 0; // the size of the queue void destroyElements() noexcept { size_t head_size = 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) 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) // we have to destroy every element in the head portion for (size_t index = head; index < head + head_size; index++) buffer[index].~T(); // and any in the tail portion, if one exists for (size_t index = 0; index < tail_size; index++) buffer[index].~T(); } bool is_full() { return queue_size == capacity(); } void grow() { size_t old_capacity = capacity(); // we use a growth factor of 1.5x (plus a constant) here in order to enable the // previous memory to be reused after a certain number of calls to grow. // see: https://github.com/facebook/folly/blob/main/folly/docs/FBVector.md#memory-handling size_t new_capacity = (old_capacity > 0) ? old_capacity * 3 / 2 + 1 : 4; // check that it's a legal allocation if (new_capacity > max_size()) throw std::bad_array_new_length(); // allocate a new backing buffer T* new_buffer = this->allocate(new_capacity); // we should not be growing if the capacity is not the current size LUAU_ASSERT(old_capacity == queue_size); size_t head_size = 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) 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) // move the head into the new buffer std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer); // move the tail into the new buffer immediately after if (head_size < queue_size) std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size); // destroy the old elements destroyElements(); // deallocate the old buffer this->deallocate(buffer, old_capacity); // set up the queue to be backed by the new buffer buffer = new_buffer; buffer_capacity = new_capacity; head = 0; } size_t logicalToPhysical(size_t pos) { return (head + pos) % capacity(); } public: VecDeque() = default; explicit VecDeque(const Allocator& alloc) noexcept : Allocator{alloc} { } VecDeque(const VecDeque& other) : buffer(this->allocate(other.buffer_capacity)) , buffer_capacity(other.buffer_capacity) , head(other.head) , queue_size(other.queue_size) { // copy the contents of the other buffer to this one std::uninitialized_copy(other.buffer, other.buffer + other.buffer_capacity, buffer); } VecDeque(const VecDeque& other, const Allocator& alloc) : Allocator{alloc} , buffer(this->allocate(other.buffer_capacity)) , buffer_capacity(other.buffer_capacity) , head(other.head) , queue_size(other.queue_size) { // copy the contents of the other buffer to this one std::uninitialized_copy(other.buffer, other.buffer + other.buffer_capacity, buffer); } VecDeque(VecDeque&& other) noexcept : buffer(std::exchange(other.buffer, nullptr)) , buffer_capacity(std::exchange(other.buffer_capacity, 0)) , head(std::exchange(other.head, 0)) , queue_size(std::exchange(other.queue_size, 0)) { } VecDeque(VecDeque&& other, const Allocator& alloc) noexcept : Allocator{alloc} , buffer(std::exchange(other.buffer, nullptr)) , buffer_capacity(std::exchange(other.buffer_capacity, 0)) , head(std::exchange(other.head, 0)) , queue_size(std::exchange(other.queue_size, 0)) { } VecDeque(std::initializer_list init, const Allocator& alloc = Allocator()) : Allocator{alloc} { buffer = this->allocate(init.size()); buffer_capacity = init.size(); queue_size = init.size(); std::uninitialized_copy(init.begin(), init.end(), buffer); } ~VecDeque() noexcept { // destroy any elements that exist destroyElements(); // free the allocated buffer this->deallocate(buffer, buffer_capacity); } VecDeque& operator=(const VecDeque& other) { if (this == &other) return *this; // destroy all of the existing elements destroyElements(); if (buffer_capacity < other.size()) { // free the current buffer this->deallocate(buffer, buffer_capacity); buffer = this->allocate(other.buffer_capacity); buffer_capacity = other.buffer_capacity; } size_t head_size = other.capacity() - other.head; // how many elements are in the head portion (i.e. from the head to the end of the buffer) size_t tail_size = other.size() - head_size; // how many elements are in the tail portion (i.e. any portion that wrapped to the front) // copy the contents of the other buffer's head into place std::uninitialized_copy(other.buffer + other.head, other.buffer + head + head_size, buffer); // copy the contents of the other buffer's tail into place immediately after std::uninitialized_copy(other.buffer, other.buffer + tail_size, buffer + head_size); return *this; } VecDeque& operator=(VecDeque&& other) { if (this == &other) return *this; // destroy all of the existing elements destroyElements(); // free the current buffer this->deallocate(buffer, buffer_capacity); buffer = std::exchange(other.buffer, nullptr); buffer_capacity = std::exchange(other.buffer_capacity, 0); head = std::exchange(other.head, 0); queue_size = std::exchange(other.queue_size, 0); return *this; } Allocator get_allocator() const noexcept { return this; } // element access T& at(size_t pos) { if (pos >= queue_size) throw std::out_of_range("VecDeque"); return buffer[logicalToPhysical(pos)]; } const T& at(size_t pos) const { if (pos >= queue_size) throw std::out_of_range("VecDeque"); return buffer[logicalToPhysical(pos)]; } [[nodiscard]] T& operator[](size_t pos) noexcept { LUAU_ASSERT(pos < queue_size); return buffer[logicalToPhysical(pos)]; } [[nodiscard]] const T& operator[](size_t pos) const noexcept { LUAU_ASSERT(pos < queue_size); return buffer[logicalToPhysical(pos)]; } T& front() { LUAU_ASSERT(!empty()); return buffer[head]; } const T& front() const { LUAU_ASSERT(!empty()); return buffer[head]; } T& back() { LUAU_ASSERT(!empty()); size_t back = logicalToPhysical(queue_size - 1); return buffer[back]; } const T& back() const { LUAU_ASSERT(!empty()); size_t back = logicalToPhysical(queue_size - 1); return buffer[back]; } // capacity bool empty() const noexcept { return queue_size == 0; } size_t size() const noexcept { return queue_size; } size_t max_size() const noexcept { return std::numeric_limits::max() / sizeof(T); } void reserve(size_t new_capacity) { // error if this allocation would be illegal if (new_capacity > max_size()) throw std::length_error("too large"); size_t old_capacity = capacity(); // do nothing if we're requesting a capacity that would not cause growth if (new_capacity <= old_capacity) return; size_t head_size = 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) 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) // allocate a new backing buffer T* new_buffer = this->allocate(new_capacity); // move the head into the new buffer std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer); // move the tail into the new buffer immediately after, if we have one if (head_size < queue_size) std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size); // move the tail into the new buffer immediately after std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size); // destroy all the existing elements before freeing the old buffer destroyElements(); // deallocate the old buffer this->deallocate(buffer, old_capacity); // set up the queue to be backed by the new buffer buffer = new_buffer; buffer_capacity = new_capacity; head = 0; } size_t capacity() const noexcept { return buffer_capacity; } void shrink_to_fit() { size_t old_capacity = capacity(); size_t new_capacity = queue_size; if (old_capacity == new_capacity) return; size_t head_size = 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) 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) // allocate a new backing buffer T* new_buffer = this->allocate(new_capacity); // move the head into the new buffer std::uninitialized_move(buffer + head, buffer + head + head_size, new_buffer); // move the tail into the new buffer immediately after, if we have one if (head_size < queue_size) std::uninitialized_move(buffer, buffer + tail_size, new_buffer + head_size); // destroy all the existing elements before freeing the old buffer destroyElements(); // deallocate the old buffer this->deallocate(buffer, old_capacity); // set up the queue to be backed by the new buffer buffer = new_buffer; buffer_capacity = new_capacity; head = 0; } [[nodiscard]] bool is_contiguous() const noexcept { // this is an overflow-safe alternative to writing `head + size <= capacity`. return head <= capacity() - queue_size; } // modifiers void clear() noexcept { destroyElements(); head = 0; queue_size = 0; } void push_back(const T& value) { if (is_full()) grow(); size_t next_back = logicalToPhysical(queue_size); new (buffer + next_back)T(value); queue_size++; } void pop_back() { LUAU_ASSERT(!empty()); queue_size--; size_t next_back = logicalToPhysical(queue_size); buffer[next_back].~T(); } void push_front(const T& value) { if (is_full()) grow(); head = (head == 0) ? capacity() - 1 : head - 1; new (buffer + head)T(value); queue_size++; } void pop_front() { LUAU_ASSERT(!empty()); buffer[head].~T(); head++; queue_size--; if (head == capacity()) head = 0; } }; } // namespace Luau