mirror of
https://github.com/open-goal/jak-project.git
synced 2024-10-20 11:26:18 -04:00
645 lines
19 KiB
C++
645 lines
19 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <iterator>
|
|
#include <new>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
#include "common/util/Assert.h"
|
|
|
|
namespace cu {
|
|
// This might seem stupid, but compiling an empty file with #include <algorithm> takes 0.5 seconds.
|
|
// Avoiding the include for <algorithm> is a huge win for compile times.
|
|
template <typename T>
|
|
constexpr const T& max(const T& a, const T& b) {
|
|
return (a < b) ? b : a;
|
|
}
|
|
|
|
template <typename T>
|
|
constexpr const T& min(const T& a, const T& b) {
|
|
return (b < a) ? b : a;
|
|
}
|
|
/*!
|
|
* A SmallVector is a replacement for std::vector. It is optimized for the case where there
|
|
* are only a few elements by having a small inline array of objects. This uses more memory, but
|
|
* handles the very common case of having only a few elements much more efficiently.
|
|
* It will work correctly even with many elements by falling back to allocating on the heap.
|
|
* Even when it falls back to being a normal heap allocation, it is the same speed as a
|
|
* std::vector for most operations. Resizing operations may be a tiny bit slower, but this
|
|
* doesn't seem to cause issues in practice.
|
|
*
|
|
* You can specify the number of inline elements. If you don't exceed this number, it won't
|
|
* allocate. By default, it picks the largest nonzero number of elements that keeps the size
|
|
* under 128-bytes, which is somewhat arbitrary.
|
|
*
|
|
* It does not implement all features of a std::vector.
|
|
* Missing:
|
|
* - the "at" operator asserts instead of throwing an exception if the index is invalid.
|
|
* - no custom Allocators
|
|
* - no reverse iterators
|
|
* - max_size is not implemented
|
|
* - Can't have a SmallVector<X> as a member of X.
|
|
*
|
|
*/
|
|
template <typename T, std::size_t inline_elt_count = max(std::size_t(1), 128 / sizeof(T))>
|
|
class SmallVector {
|
|
private:
|
|
template <typename U>
|
|
constexpr U* launder(U* in) const {
|
|
#if __cpp_lib_launder >= 201606
|
|
return std::launder(in);
|
|
#else
|
|
return in;
|
|
#endif
|
|
}
|
|
// how much to increase the storage amount when we run out.
|
|
static constexpr double GROW_AMOUNT = 1.5;
|
|
|
|
// by default, store stuff in here. C++ always uses sizeof(T) as the stride of an array, so this
|
|
// is just like an uninitialized array of T's, which starts at the proper alignment.
|
|
// We can't use an array of T's because that would default construct them, which we don't want.
|
|
typename std::aligned_storage<sizeof(T), alignof(T)>::type m_inline[inline_elt_count];
|
|
|
|
// get a T* at the beginning of our inline storage.
|
|
constexpr const T* inline_begin() const { return launder(reinterpret_cast<const T*>(m_inline)); }
|
|
constexpr T* inline_begin() { return launder(reinterpret_cast<T*>(m_inline)); }
|
|
|
|
// regardless of our storage mode, these hold the beginning and end of the storage.
|
|
// by default, they are initialized to the inline storage.
|
|
T* m_storage_begin;
|
|
T* m_storage_end;
|
|
|
|
// the number of in-use elements
|
|
std::size_t m_size;
|
|
|
|
//// STORAGE HELPERS
|
|
|
|
/*!
|
|
* Allocate new heap storage and set it as the current.
|
|
* The objects in storage are uninitialized.
|
|
*/
|
|
void allocate_and_set_heap_storage(std::size_t elt_count) {
|
|
m_storage_begin = launder(reinterpret_cast<T*>(new uint8_t[elt_count * sizeof(T)]));
|
|
m_storage_end = m_storage_begin + elt_count;
|
|
}
|
|
|
|
/*!
|
|
* Free heap storage, without calling destructors of objects.
|
|
*/
|
|
void free_heap_storage(T* ptr) { delete[] launder(reinterpret_cast<uint8_t*>(ptr)); }
|
|
|
|
/*!
|
|
* Set the current storage to the inline memory.
|
|
* We rely on m_storage_begin == inline_begin() to determine if we are using the inline memory.
|
|
* This ends up being faster than storing a bool for the mode.
|
|
*/
|
|
void set_inline_storage() {
|
|
m_storage_begin = inline_begin();
|
|
m_storage_end = inline_begin() + inline_elt_count;
|
|
}
|
|
|
|
bool using_heap_storage() const { return m_storage_begin != inline_begin(); }
|
|
|
|
/*!
|
|
* Allocate (if needed) storage to hold size objects.
|
|
* Sets the container size.
|
|
* Assumes the current storage is stack (the default)
|
|
*/
|
|
void initialize_storage_and_size(std::size_t size) {
|
|
if (size > inline_elt_count) {
|
|
allocate_and_set_heap_storage(size);
|
|
} else {
|
|
set_inline_storage();
|
|
}
|
|
m_size = size;
|
|
}
|
|
|
|
void free_current_storage() {
|
|
if (using_heap_storage()) {
|
|
free_heap_storage(m_storage_begin);
|
|
}
|
|
}
|
|
|
|
//// CONSTRUCTION HELPERS
|
|
|
|
/*!
|
|
* Construct objects using the copy constructor, copying val for each.
|
|
*/
|
|
void construct_objects_by_filling(T* ptr, std::size_t count, const T& val) {
|
|
for (std::size_t i = 0; i < count; i++) {
|
|
new (ptr + i) T(val);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Construct objects using the default constructor.
|
|
*/
|
|
void construct_objects_by_default_ctor(T* ptr, std::size_t count) {
|
|
for (std::size_t i = 0; i < count; i++) {
|
|
new (ptr + i) T();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Call destructor on an array of objects.
|
|
*/
|
|
void destroy_objects(T* ptr, std::size_t count) {
|
|
for (std::size_t i = 0; i < count; i++) {
|
|
ptr[i].~T();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Move objects from src to dst, deleting them in src.
|
|
* Doing this in a single pass is more cache friendly for huge vectors.
|
|
* Compilers seem to be smart enough to turn this into a memcpy for trivial types still.
|
|
*/
|
|
void move_and_destroy(T* dst, T* src, std::size_t count) {
|
|
for (std::size_t i = 0; i < count; i++) {
|
|
new (dst + i) T(std::move(src[i]));
|
|
src[i].~T();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Move objects from src to dst, starting at the highest memory address. This is useful
|
|
* if src and dst overlap.
|
|
*/
|
|
void move_and_destroy_reverse(T* dst, T* src, std::size_t count) {
|
|
for (std::size_t i = count; i-- > 0;) {
|
|
new (dst + i) T(std::move(src[i]));
|
|
src[i].~T();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Copy objects from src to dst, using the copy constructor.
|
|
*/
|
|
void copy_construct_objects(T* dst, const T* src, std::size_t count) {
|
|
for (std::size_t i = 0; i < count; i++) {
|
|
new (dst + i) T(src[i]);
|
|
}
|
|
}
|
|
|
|
template <typename InputIt>
|
|
void copy_objects_from_range(T* dst, InputIt first, InputIt last) {
|
|
auto iter = first;
|
|
while (iter != last) {
|
|
new (dst) T(*iter);
|
|
iter++;
|
|
dst++;
|
|
}
|
|
}
|
|
|
|
public:
|
|
using iterator = T*;
|
|
using const_iterator = const T*;
|
|
using reverse_iterator = std::reverse_iterator<T*>;
|
|
using const_reverse_iterator = std::reverse_iterator<const T*>;
|
|
//// CONSTRUCTORS
|
|
/*!
|
|
* Constructor a SmallVector. By default the size is 0 and no elements are constructed.
|
|
*/
|
|
SmallVector() { initialize_storage_and_size(0); }
|
|
|
|
/*!
|
|
* Initialize vector with count elements that are default initialized
|
|
*/
|
|
explicit SmallVector(std::size_t count) {
|
|
initialize_storage_and_size(count);
|
|
construct_objects_by_default_ctor(m_storage_begin, count);
|
|
}
|
|
|
|
/*!
|
|
* Initialize vector with count values that are copied from the given value.
|
|
*/
|
|
SmallVector(std::size_t count, const T& value) {
|
|
// set up storage:
|
|
initialize_storage_and_size(count);
|
|
construct_objects_by_filling(m_storage_begin, count, value);
|
|
}
|
|
|
|
/*!
|
|
* Initialize vector from iterators.
|
|
* In the case where your input iterator doesn't support random access, this ends up
|
|
* making two passes through your data. The first to check the size, then the second to copy.
|
|
*/
|
|
template <typename InputIt>
|
|
SmallVector(InputIt first, InputIt last) {
|
|
// this does a subtraction for random access iterators, or increments one-by-one for others.
|
|
initialize_storage_and_size(std::distance(first, last));
|
|
copy_objects_from_range(m_storage_begin, first, last);
|
|
}
|
|
|
|
/*!
|
|
* Initialize vector using values from another vector.
|
|
*/
|
|
SmallVector(const SmallVector<T, inline_elt_count>& other) {
|
|
initialize_storage_and_size(other.size());
|
|
copy_construct_objects(m_storage_begin, other.m_storage_begin, other.size());
|
|
}
|
|
|
|
/*!
|
|
* Initialize vector by moving from existing vector. Will leave other vector with size = 0.
|
|
*/
|
|
SmallVector(SmallVector<T, inline_elt_count>&& other) {
|
|
if (other.using_heap_storage()) {
|
|
// steal the storage from the existing vector
|
|
m_storage_begin = other.m_storage_begin;
|
|
m_storage_end = other.m_storage_end;
|
|
// don't let the other vector reuse the storage by setting it back to inline.
|
|
other.set_inline_storage();
|
|
} else {
|
|
set_inline_storage();
|
|
// move from inline to inline
|
|
// we're going to set the other's size to 0, so we have to destroy its objects.
|
|
move_and_destroy(m_storage_begin, other.m_storage_begin, other.size());
|
|
}
|
|
|
|
m_size = other.m_size;
|
|
other.m_size = 0;
|
|
}
|
|
|
|
/*!
|
|
* Initialize vector from an initializer list.
|
|
*/
|
|
SmallVector(std::initializer_list<T> lst) {
|
|
initialize_storage_and_size(lst.size());
|
|
copy_objects_from_range(m_storage_begin, lst.begin(), lst.end());
|
|
}
|
|
|
|
//// ASSIGNMENT
|
|
/*!
|
|
* Copy all data from another vector.
|
|
* Uses copy ctor in all cases, never copy assignment.
|
|
*/
|
|
SmallVector& operator=(const SmallVector<T, inline_elt_count>& other) {
|
|
if (&other == this) {
|
|
return *this;
|
|
}
|
|
|
|
// destroy all existing objects
|
|
destroy_objects(m_storage_begin, size());
|
|
|
|
// if we're bothering to copy, let's resize the storage to exactly what we need.
|
|
if (capacity() != other.size()) {
|
|
free_current_storage();
|
|
initialize_storage_and_size(other.size());
|
|
}
|
|
m_size = other.size();
|
|
|
|
copy_construct_objects(m_storage_begin, other.m_storage_begin, other.size());
|
|
return *this;
|
|
}
|
|
|
|
/*!
|
|
* Move all data from another vector. Leaves the other vector with nothing.
|
|
*/
|
|
SmallVector& operator=(SmallVector<T, inline_elt_count>&& other) {
|
|
if (&other == this) {
|
|
return *this;
|
|
}
|
|
|
|
// destroy existing objects
|
|
destroy_objects(m_storage_begin, size());
|
|
|
|
// kill old storage. We will either steal it from other or use stack.
|
|
free_current_storage();
|
|
|
|
if (other.size() > inline_elt_count) {
|
|
// steal the heap array
|
|
m_storage_begin = other.m_storage_begin;
|
|
m_storage_end = other.m_storage_end;
|
|
other.set_inline_storage();
|
|
} else {
|
|
// move from inline storage
|
|
set_inline_storage();
|
|
move_and_destroy(m_storage_begin, other.m_storage_begin, other.size());
|
|
}
|
|
m_size = other.size();
|
|
other.m_size = 0;
|
|
return *this;
|
|
}
|
|
|
|
/*!
|
|
* Set the contents to count copies of a given value.
|
|
* Note - this is not super efficient and could avoid reallocating the heap memory in some cases.
|
|
*/
|
|
void assign(std::size_t count, const T& value) {
|
|
*this = SmallVector<T, inline_elt_count>(count, value);
|
|
}
|
|
|
|
/*!
|
|
* See note on other assign.
|
|
*/
|
|
template <typename InputIt>
|
|
void assign(InputIt first, InputIt last) {
|
|
*this = SmallVector<T, inline_elt_count>(first, last);
|
|
}
|
|
|
|
/*!
|
|
* See note on other assign.
|
|
*/
|
|
void assign(std::initializer_list<T> lst) { *this = SmallVector<T, inline_elt_count>(lst); }
|
|
|
|
/*!
|
|
* Get the element at the index. Assert the index is valid.
|
|
*/
|
|
T& at(std::size_t idx) {
|
|
ASSERT(idx < m_size);
|
|
return m_storage_begin[idx];
|
|
}
|
|
|
|
/*!
|
|
* Get the element at the index. Assert the index is valid.
|
|
*/
|
|
const T& at(std::size_t idx) const {
|
|
ASSERT(idx < m_size);
|
|
return m_storage_begin[idx];
|
|
}
|
|
|
|
/*!
|
|
* Get the element at the index. No range checking.
|
|
*/
|
|
T& operator[](std::size_t idx) { return m_storage_begin[idx]; }
|
|
|
|
/*!
|
|
* Get the element at the index. No range checking.
|
|
*/
|
|
const T& operator[](std::size_t idx) const { return m_storage_begin[idx]; }
|
|
|
|
/*!
|
|
* Get the first element. Not valid if size == 0
|
|
*/
|
|
T& front() { return m_storage_begin[0]; }
|
|
|
|
/*!
|
|
* Get the first element. Not valid if size == 0
|
|
*/
|
|
const T& front() const { return m_storage_begin[0]; }
|
|
|
|
/*!
|
|
* Get the last element. Not valid if size == 0
|
|
*/
|
|
T& back() { return m_storage_begin[m_size - 1]; }
|
|
|
|
/*!
|
|
* Get the last element. Not valid if size == 0
|
|
*/
|
|
const T& back() const { return m_storage_begin[m_size - 1]; }
|
|
|
|
/*!
|
|
* Get a pointer to data. This may become invalid after any resize, just like a std::vector.
|
|
*/
|
|
T* data() { return m_storage_begin; }
|
|
|
|
/*!
|
|
* Get a pointer to data. This may become invalid after any resize, just like a std::vector.
|
|
*/
|
|
const T* data() const { return m_storage_begin; }
|
|
|
|
iterator begin() { return m_storage_begin; }
|
|
const_iterator begin() const { return m_storage_begin; }
|
|
const_iterator cbegin() const { return m_storage_begin; }
|
|
iterator end() { return m_storage_begin + m_size; }
|
|
const_iterator end() const { return m_storage_begin + m_size; }
|
|
const_iterator cend() const { return m_storage_begin + m_size; }
|
|
|
|
reverse_iterator rbegin() { return m_storage_begin + m_size; }
|
|
const_reverse_iterator rbegin() const { return m_storage_begin + m_size; }
|
|
const_reverse_iterator crbegin() const { return m_storage_begin + m_size; }
|
|
reverse_iterator rend() { return m_storage_begin; }
|
|
const_reverse_iterator rend() const { return m_storage_begin; }
|
|
const_reverse_iterator crend() const { return m_storage_begin; }
|
|
|
|
bool empty() const { return m_size == 0; }
|
|
std::size_t size() const { return m_size; }
|
|
|
|
/*!
|
|
* Make sure we have enough storage to hold desired_count without allocating.
|
|
*/
|
|
void reserve(std::size_t desired_count) {
|
|
auto old_count = std::size_t(m_storage_end - m_storage_begin);
|
|
if (desired_count > old_count) {
|
|
// we need more storage.
|
|
auto old_storage = m_storage_begin;
|
|
auto old_uses_heap = using_heap_storage();
|
|
|
|
// replace current storage
|
|
allocate_and_set_heap_storage(desired_count);
|
|
// move objects into the new location and destroy the old ones.
|
|
move_and_destroy(m_storage_begin, old_storage, size());
|
|
// free old, if needed.
|
|
if (old_uses_heap) {
|
|
free_heap_storage(old_storage);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* The number of elements we can hold without allocating more memory.
|
|
*/
|
|
std::size_t capacity() const { return m_storage_end - m_storage_begin; }
|
|
|
|
/*!
|
|
* Shrink our heap allocation to the smallest possible size that can possibly hold the
|
|
* current elements. This can shrink it to zero and move us back to inline storage.
|
|
*/
|
|
void shrink_to_fit() {
|
|
if (using_heap_storage() && capacity() > size()) {
|
|
auto old_storage = m_storage_begin;
|
|
if (size() > inline_elt_count) {
|
|
// go to smaller heap
|
|
allocate_and_set_heap_storage(size());
|
|
} else {
|
|
// go back to inline
|
|
set_inline_storage();
|
|
}
|
|
move_and_destroy(m_storage_begin, old_storage, size());
|
|
free_heap_storage(old_storage); // was heap.
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* Destroy all objects and free all of our memory. Sets our size to 0.
|
|
*/
|
|
void clear() {
|
|
destroy_objects(m_storage_begin, m_size);
|
|
if (using_heap_storage()) {
|
|
free_heap_storage(m_storage_begin);
|
|
}
|
|
m_size = 0;
|
|
}
|
|
|
|
/*!
|
|
* Insert value _before_ pos.
|
|
*/
|
|
iterator insert(iterator pos, const T& value) {
|
|
auto objects_before_insert = pos - begin();
|
|
auto objects_after_insert = end() - pos;
|
|
iterator result;
|
|
if (size() + 1 > capacity()) {
|
|
// not enough room, need to reallocate
|
|
auto old_storage = m_storage_begin;
|
|
auto old_used_heap = using_heap_storage();
|
|
allocate_and_set_heap_storage(GROW_AMOUNT * (size() + 1)); // todo grow policy here.
|
|
|
|
// copy the objects before the insert
|
|
move_and_destroy(m_storage_begin, old_storage, objects_before_insert);
|
|
// copy the object to insert
|
|
result = new (m_storage_begin + objects_before_insert) T(value);
|
|
// copy the objects after the insert
|
|
move_and_destroy(m_storage_begin + objects_before_insert + 1,
|
|
old_storage + objects_before_insert, objects_after_insert);
|
|
|
|
if (old_used_heap) {
|
|
free_heap_storage(old_storage);
|
|
}
|
|
} else {
|
|
// we have enough room, just move.
|
|
move_and_destroy_reverse(pos + 1, pos, objects_after_insert);
|
|
// and insert!
|
|
result = new (pos) T(value);
|
|
}
|
|
|
|
m_size++;
|
|
return result;
|
|
}
|
|
|
|
// insert iterator T&& value
|
|
// insert iterator count value
|
|
// insert iterator (it, it)
|
|
|
|
// emplace
|
|
// erase
|
|
|
|
// push_back
|
|
void push_back(const T& value) {
|
|
if (size() + 1 > capacity()) {
|
|
// not enough room!
|
|
reserve(GROW_AMOUNT * (size() + 1));
|
|
}
|
|
// copy
|
|
new (m_storage_begin + size()) T(value);
|
|
m_size++;
|
|
}
|
|
|
|
void push_back(const T&& value) {
|
|
if (size() + 1 > capacity()) {
|
|
// not enough room!
|
|
reserve(GROW_AMOUNT * (size() + 1));
|
|
}
|
|
// move:
|
|
new (m_storage_begin + size()) T(std::move(value));
|
|
m_size++;
|
|
}
|
|
|
|
template <typename... Args>
|
|
void emplace_back(Args&&... args) {
|
|
if (size() + 1 > capacity()) {
|
|
// not enough room!
|
|
reserve(GROW_AMOUNT * (size() + 1));
|
|
}
|
|
new (m_storage_begin + size()) T(std::forward<Args>(args)...);
|
|
m_size++;
|
|
}
|
|
|
|
void pop_back() { m_size--; }
|
|
|
|
void relocate_some_and_destroy_rest(T* dst,
|
|
T* src,
|
|
std::size_t dst_count,
|
|
std::size_t src_count) {
|
|
std::size_t i = 0;
|
|
while (i < dst_count) {
|
|
new (dst + i) T(std::move(src[i]));
|
|
src[i].~T();
|
|
i++;
|
|
}
|
|
|
|
while (i < src_count) {
|
|
src[i].~T();
|
|
i++;
|
|
}
|
|
}
|
|
|
|
void resize(std::size_t new_size) {
|
|
if (size() == new_size) {
|
|
return;
|
|
}
|
|
|
|
auto copy_count = min(size(), new_size);
|
|
bool old_used_heap = using_heap_storage();
|
|
auto old_storage = m_storage_begin;
|
|
|
|
if (new_size < inline_elt_count) {
|
|
// heap to inline
|
|
set_inline_storage();
|
|
} else {
|
|
allocate_and_set_heap_storage(new_size);
|
|
}
|
|
|
|
relocate_some_and_destroy_rest(m_storage_begin, old_storage, copy_count, size());
|
|
if (old_used_heap) {
|
|
free_heap_storage(old_storage);
|
|
}
|
|
construct_objects_by_default_ctor(m_storage_begin + copy_count, new_size - copy_count);
|
|
m_size = new_size;
|
|
}
|
|
|
|
void resize(std::size_t new_size, const T& value) {
|
|
if (size() == new_size) {
|
|
return;
|
|
}
|
|
|
|
auto copy_count = min(size(), new_size);
|
|
bool old_used_heap = using_heap_storage();
|
|
auto old_storage = m_storage_begin;
|
|
|
|
if (new_size < inline_elt_count) {
|
|
// heap to inline
|
|
set_inline_storage();
|
|
} else {
|
|
allocate_and_set_heap_storage(new_size);
|
|
}
|
|
|
|
relocate_some_and_destroy_rest(m_storage_begin, old_storage, copy_count, size());
|
|
if (old_used_heap) {
|
|
free_heap_storage(old_storage);
|
|
}
|
|
construct_objects_by_filling(m_storage_begin + copy_count, new_size - copy_count, value);
|
|
m_size = new_size;
|
|
}
|
|
|
|
// swap
|
|
|
|
bool operator==(const SmallVector<T, inline_elt_count>& other) const {
|
|
if (other.size() != size()) {
|
|
return false;
|
|
}
|
|
for (std::size_t i = 0; i < size(); i++) {
|
|
if (operator[](i) != other[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool operator!=(const SmallVector<T, inline_elt_count>& other) const {
|
|
return !((*this) == other);
|
|
}
|
|
|
|
// std::swap
|
|
|
|
~SmallVector() {
|
|
// destroy allocated objects
|
|
destroy_objects(m_storage_begin, m_size);
|
|
// free heap storage.
|
|
if (using_heap_storage()) {
|
|
free_heap_storage(m_storage_begin);
|
|
}
|
|
}
|
|
};
|
|
} // namespace cu
|