/// /// @file Vector.hpp /// /// Copyright (C) 2026 Kim Walisch, /// /// This file is distributed under the BSD License. See the COPYING /// file in the top level directory. /// #ifndef VECTOR_HPP #define VECTOR_HPP #include "macros.hpp" #include #include #include #include #include #include namespace primecount { /// VectorBase holds the pointers of the Vector. This is required /// to make the Vector's constructor exception safe. template struct VectorBase { T* begin_ = nullptr; T* end_ = nullptr; T* capacity_ = nullptr; VectorBase() noexcept = default; ~VectorBase() noexcept { if (begin_) { ASSERT(capacity_ >= begin_); std::size_t cap = std::size_t(capacity_ - begin_); Allocator().deallocate(begin_, cap); } } // Move constructor VectorBase(VectorBase&& other) noexcept : begin_(other.begin_), end_(other.end_), capacity_(other.capacity_) { other.begin_ = nullptr; other.end_ = nullptr; other.capacity_ = nullptr; } /// We disable copying to avoid severe performance /// penalties caused by unintended copying. VectorBase(const VectorBase&) = delete; VectorBase& operator=(const VectorBase&) = delete; }; /// Vector is a dynamically growing array. /// It has the same API (though not complete) as std::vector but its /// resize() method does not default initialize memory for built-in /// integer types. It does however default initialize classes and /// struct types if they have a constructor. It also prevents bounds /// checks in release builds which is important for primecount's /// performance, e.g. the Fedora Linux distribution compiles with /// -D_GLIBCXX_ASSERTIONS which enables std::vector bounds checks. /// template > class Vector : private VectorBase { using VecBase = VectorBase; using VecBase::begin_; using VecBase::end_; using VecBase::capacity_; public: // The default C++ std::allocator is stateless. We use this // allocator and do not support other statefull allocators, // which simplifies our implementation. // // "The default allocator is stateless, that is, all instances // of the given allocator are interchangeable, compare equal // and can deallocate memory allocated by any other instance // of the same allocator type." // https://en.cppreference.com/w/cpp/memory/allocator // // "The member type is_always_equal of std::allocator_traits // is intendedly used for determining whether an allocator // type is stateless." // https://en.cppreference.com/w/cpp/named_req/Allocator static_assert(std::allocator_traits::is_always_equal::value, "Vector only supports stateless allocators!"); using value_type = T; Vector() noexcept = default; Vector(std::size_t size) { resize(size); } ~Vector() noexcept { destroy(begin_, end_); } /// Free all memory, the Vector /// can be reused afterwards. void deallocate() noexcept { if (begin_) { destroy(begin_, end_); Allocator().deallocate(begin_, capacity()); begin_ = nullptr; end_ = nullptr; capacity_ = nullptr; } } /// Reset the Vector, but do not free its /// memory. Same as std::vector.clear(). void clear() noexcept { destroy(begin_, end_); end_ = begin_; } /// We disable copying to avoid severe performance /// penalties caused by unintended copying. Vector(const Vector&) = delete; Vector& operator=(const Vector&) = delete; /// Move constructor Vector(Vector&& other) noexcept : VecBase(std::move(other)) { } /// Move assignment operator Vector& operator=(Vector&& other) noexcept { if (this != &other) { deallocate(); begin_ = other.begin_; end_ = other.end_; capacity_ = other.capacity_; other.begin_ = nullptr; other.end_ = nullptr; other.capacity_ = nullptr; } return *this; } /// Better assembly than std::swap(vect1, vect2) void swap(Vector& other) noexcept { T* tmp_array = begin_; T* tmp_end = end_; T* tmp_capacity = capacity_; begin_ = other.begin_; end_ = other.end_; capacity_ = other.capacity_; other.begin_ = tmp_array; other.end_ = tmp_end; other.capacity_ = tmp_capacity; } bool empty() const noexcept { return begin_ == end_; } T& operator[](std::size_t pos) noexcept { ASSERT(pos < size()); return begin_[pos]; } const T& operator[](std::size_t pos) const noexcept { ASSERT(pos < size()); return begin_[pos]; } T* data() noexcept { return begin_; } const T* data() const noexcept { return begin_; } std::size_t size() const noexcept { ASSERT(end_ >= begin_); return std::size_t(end_ - begin_); } std::size_t capacity() const noexcept { ASSERT(capacity_ >= begin_); return std::size_t(capacity_ - begin_); } T* begin() noexcept { return begin_; } const T* begin() const noexcept { return begin_; } T* end() noexcept { return end_; } const T* end() const noexcept { return end_; } T& front() noexcept { ASSERT(!empty()); return *begin_; } const T& front() const noexcept { ASSERT(!empty()); return *begin_; } T& back() noexcept { ASSERT(!empty()); return *(end_ - 1); } const T& back() const noexcept { ASSERT(!empty()); return *(end_ - 1); } ALWAYS_INLINE void push_back(const T& value) { if_likely(end_ != capacity_) { new(end_) T(value); end_++; } else emplace_back_reallocate(value); } ALWAYS_INLINE void push_back(T&& value) { if_likely(end_ != capacity_) { new(end_) T(std::move(value)); end_++; } else emplace_back_reallocate(std::move(value)); } template ALWAYS_INLINE void emplace_back(Args&&... args) { if_likely(end_ != capacity_) { new(end_) T(std::forward(args)...); end_++; } else emplace_back_reallocate(std::forward(args)...); } template void insert(T* const pos, InputIt first, InputIt last) { // We only support appending to the vector ASSERT(pos == end_); (void) pos; if_likely(first < last) { std::size_t count = std::size_t(last - first); if (size() + count <= capacity()) { std::uninitialized_copy(first, last, end_); end_ += count; } else insert_reallocate(first, last); } } void reserve(std::size_t n) { if (n > capacity()) reserve_unchecked(n); } void resize(std::size_t n) { if (n > size()) { if (n > capacity()) reserve_unchecked(n); // This default initializes memory of classes and structs // with constructors (and with in-class initialization of // non-static members). But it does not default initialize // memory for POD types like int, long. uninitialized_default_construct(end_, begin_ + n); end_ = begin_ + n; } else if (n < size()) { destroy(begin_ + n, end_); end_ = begin_ + n; } } private: std::size_t increase_capacity() const { // GCC & Clang's std::vector grow the capacity by 2x // for every call to resize() with n > capacity(). std::size_t new_capacity = capacity() * 2; // The minimum allocation size of malloc is usually 16 bytes std::size_t min_capacity = std::max(std::size_t(1), 16 / sizeof(T)); new_capacity = std::max(min_capacity, new_capacity); ASSERT(capacity() < new_capacity); return new_capacity; } /// Requires n > capacity() void reserve_unchecked(std::size_t n) { ASSERT(n > capacity()); ASSERT(size() <= capacity()); std::size_t old_size = size(); std::size_t old_capacity = capacity(); std::size_t new_capacity = increase_capacity(); new_capacity = std::max(new_capacity, n); ASSERT(capacity() < new_capacity); T* old = begin_; begin_ = Allocator().allocate(new_capacity); end_ = begin_ + old_size; capacity_ = begin_ + new_capacity; ASSERT(size() < capacity()); // The code below is guaranteed to not throw // any exceptions, hence no memory leaks are // possible here anymore. if (old) { static_assert(std::is_nothrow_move_constructible::value, "Vector only supports nothrow moveable types!"); uninitialized_move_n(old, old_size, begin_); destroy(old, old + old_size); Allocator().deallocate(old, old_capacity); } } template void emplace_back_reallocate(Args&&... args) { std::size_t old_size = size(); std::size_t old_capacity = capacity(); std::size_t new_capacity = increase_capacity(); ASSERT(capacity() < new_capacity); // If an exception is thrown during initialization further // down this will free the memory using RAII. struct AllocateMemory { T* begin = nullptr; std::size_t capacity = 0; AllocateMemory(std::size_t cap) { begin = Allocator().allocate(cap); capacity = cap; } ~AllocateMemory() { if (begin) Allocator().deallocate(begin, capacity); } }; AllocateMemory new_mem(new_capacity); new (new_mem.begin + old_size) T(std::forward(args)...); // The code below is guaranteed to not throw // any exceptions, hence no memory leaks are // possible here anymore. if (begin_) { static_assert(std::is_nothrow_move_constructible::value, "Vector only supports nothrow moveable types!"); uninitialized_move_n(begin_, old_size, new_mem.begin); destroy(begin_, end_); Allocator().deallocate(begin_, old_capacity); } begin_ = new_mem.begin; end_ = begin_ + old_size + 1; capacity_ = begin_ + new_capacity; new_mem.begin = nullptr; } template void insert_reallocate(InputIt first, InputIt last) { ASSERT(first < last); std::size_t old_size = size(); std::size_t count = std::size_t(last - first); std::size_t new_size = old_size + count; std::size_t old_capacity = capacity(); std::size_t new_capacity = increase_capacity(); new_capacity = std::max(new_capacity, new_size); ASSERT(capacity() < new_capacity); // If an exception is thrown during initialization further // down this will free the memory using RAII. struct AllocateMemory { T* begin = nullptr; std::size_t capacity = 0; AllocateMemory(std::size_t cap) { begin = Allocator().allocate(cap); capacity = cap; } ~AllocateMemory() { if (begin) Allocator().deallocate(begin, capacity); } }; AllocateMemory new_mem(new_capacity); std::uninitialized_copy(first, last, new_mem.begin + old_size); // The code below is guaranteed to not throw // any exceptions, hence no memory leaks are // possible here anymore. if (begin_) { static_assert(std::is_nothrow_move_constructible::value, "Vector only supports nothrow moveable types!"); uninitialized_move_n(begin_, old_size, new_mem.begin); destroy(begin_, end_); Allocator().deallocate(begin_, old_capacity); } begin_ = new_mem.begin; end_ = begin_ + new_size; capacity_ = begin_ + new_capacity; new_mem.begin = nullptr; } /// For POD types like int, long our Vector implementation does /// not default initialize memory. This allows allocating a /// large vector and initializing the memory later without any /// unnecessary performance overhead. template ALWAYS_INLINE typename std::enable_if::value, void>::type uninitialized_default_construct(T* first, T* last) { // Start object lifetimes without initializing storage. // Will be optimized away be the compiler. for (; first != last; first++) new (first) T; } /// This default initializes memory for classes and structs with /// constructors (and with in-class initialization of non-static /// members). template ALWAYS_INLINE typename std::enable_if::value, void>::type uninitialized_default_construct(T* first, T* last) { struct ExceptionGuard { Vector* vec_; T* pos_; T* first_; T* last_; ExceptionGuard(Vector* vector, T* first, T* last) : vec_(vector), pos_(first), first_(first), last_(last) { } ~ExceptionGuard() { if (pos_ != last_) vec_->destroy(first_, pos_); } }; // If an exception is thrown during initialization further // down this will destroy the new objects using RAII. ExceptionGuard guard(this, first, last); // Default initialize vector using placement new. // Note that `new (ptr) T();` zero initializes built-in integer types, // whereas `new (ptr) T;` does not initialize built-in integer types. for (; guard.pos_ != guard.last_; guard.pos_++) new (guard.pos_) T; } template ALWAYS_INLINE typename std::enable_if::value, void>::type uninitialized_move_n(T* __restrict first, std::size_t count, T* __restrict d_first) { // We can use memcpy to move trivially copyable types. // https://en.cppreference.com/w/cpp/language/classes#Trivially_copyable_class // https://stackoverflow.com/questions/17625635/moving-an-object-in-memory-using-stdmemcpy std::uninitialized_copy_n(first, count, d_first); } /// Same as std::uninitialized_move_n() from C++17. /// https://en.cppreference.com/w/cpp/memory/uninitialized_move_n /// /// Unlike std::uninitialized_move_n() our implementation uses /// __restrict pointers which improves the generated assembly /// (using GCC & Clang). We can do this because we only use this /// method for non-overlapping arrays. template ALWAYS_INLINE typename std::enable_if::value, void>::type uninitialized_move_n(T* __restrict first, std::size_t count, T* __restrict d_first) { for (std::size_t i = 0; i < count; i++) new (d_first++) T(std::move(*first++)); } template ALWAYS_INLINE typename std::enable_if::value, void>::type destroy(T*, T*) { } /// Same as std::destroy() from C++17. /// https://en.cppreference.com/w/cpp/memory/destroy template ALWAYS_INLINE typename std::enable_if::value, void>::type destroy(T* first, T* last) { // Theoretically deallocating in reverse order is more // cache efficient. Clang's std::vector implementation // also deallocates in reverse order. while (first != last) (--last)->~T(); } }; /// Array has the same API as std::array, but unlike std::array /// our Array is guaranteed to not use any bounds checks in release /// builds. E.g. the Fedora Linux distribution compiles with /// -D_GLIBCXX_ASSERTIONS which enables std::array bounds checks. /// template class Array { public: using value_type = T; T array_[N]; T& operator[](std::size_t pos) noexcept { ASSERT(pos < size()); return array_[pos]; } const T& operator[](std::size_t pos) const noexcept { ASSERT(pos < size()); return array_[pos]; } void fill(const T& value) { std::fill_n(begin(), size(), value); } T* data() noexcept { return array_; } const T* data() const noexcept { return array_; } T* begin() noexcept { return array_; } const T* begin() const noexcept { return array_; } T* end() noexcept { return array_ + N; } const T* end() const noexcept { return array_ + N; } T& back() noexcept { ASSERT(N > 0); return array_[N - 1]; } const T& back() const noexcept { ASSERT(N > 0); return array_[N - 1]; } constexpr std::size_t size() const noexcept { return N; } }; } // namespace #endif