mirror of
https://github.com/kimwalisch/primecount.git
synced 2026-06-07 19:29:47 +09:00
675 lines
16 KiB
C++
675 lines
16 KiB
C++
///
|
|
/// @file Vector.hpp
|
|
///
|
|
/// Copyright (C) 2026 Kim Walisch, <kim.walisch@gmail.com>
|
|
///
|
|
/// 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 <algorithm>
|
|
#include <cstddef>
|
|
#include <memory>
|
|
#include <stdint.h>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
namespace primecount {
|
|
|
|
/// VectorBase holds the pointers of the Vector. This is required
|
|
/// to make the Vector's constructor exception safe.
|
|
template <typename T, typename Allocator>
|
|
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 <typename T,
|
|
typename Allocator = std::allocator<T>>
|
|
class Vector : private VectorBase<T, Allocator>
|
|
{
|
|
using VecBase = VectorBase<T, Allocator>;
|
|
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<Allocator>::is_always_equal::value,
|
|
"Vector<T> 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 <class... Args>
|
|
ALWAYS_INLINE void emplace_back(Args&&... args)
|
|
{
|
|
if_likely(end_ != capacity_)
|
|
{
|
|
new(end_) T(std::forward<Args>(args)...);
|
|
end_++;
|
|
}
|
|
else
|
|
emplace_back_reallocate(std::forward<Args>(args)...);
|
|
}
|
|
|
|
template <class InputIt>
|
|
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<T>::value,
|
|
"Vector<T> only supports nothrow moveable types!");
|
|
|
|
uninitialized_move_n(old, old_size, begin_);
|
|
destroy(old, old + old_size);
|
|
Allocator().deallocate(old, old_capacity);
|
|
}
|
|
}
|
|
|
|
template <class... Args>
|
|
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>(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<T>::value,
|
|
"Vector<T> 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 <class InputIt>
|
|
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<T>::value,
|
|
"Vector<T> 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 <typename TT = T>
|
|
ALWAYS_INLINE typename std::enable_if<std::is_trivially_default_constructible<TT>::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 <typename TT = T>
|
|
ALWAYS_INLINE typename std::enable_if<!std::is_trivially_default_constructible<TT>::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 <typename TT = T>
|
|
ALWAYS_INLINE typename std::enable_if<std::is_trivially_copyable<TT>::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 <typename TT = T>
|
|
ALWAYS_INLINE typename std::enable_if<!std::is_trivially_copyable<TT>::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 <typename TT = T>
|
|
ALWAYS_INLINE typename std::enable_if<std::is_trivially_destructible<TT>::value, void>::type
|
|
destroy(T*, T*)
|
|
{ }
|
|
|
|
/// Same as std::destroy() from C++17.
|
|
/// https://en.cppreference.com/w/cpp/memory/destroy
|
|
template <typename TT = T>
|
|
ALWAYS_INLINE typename std::enable_if<!std::is_trivially_destructible<TT>::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 <typename T, std::size_t N>
|
|
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
|