Skip to content

Instantly share code, notes, and snippets.

@Pugemon
Created July 14, 2026 01:58
Show Gist options
  • Select an option

  • Save Pugemon/7bf971044b2901fc0aa1d22cc2cda202 to your computer and use it in GitHub Desktop.

Select an option

Save Pugemon/7bf971044b2901fc0aa1d22cc2cda202 to your computer and use it in GitHub Desktop.
Modern C++20 allocator-aware SmallVec with Small Buffer Optimization (SBO), trivial relocation, and strict exception safety.
#pragma once
#include <algorithm>
#include <compare>
#include <cstddef>
#include <cstring>
#include <initializer_list>
#include <iostream>
#include <iterator>
#include <memory>
#include <type_traits>
#include <utility>
#if defined(__GNUC__) || defined(__clang__)
#define CD_FORCEINLINE __attribute__((always_inline)) inline
#define CD_NOINLINE __attribute__((noinline))
#define CD_PURE_FN __attribute__((pure))
#define CD_ARTIFICIAL __attribute__((artificial))
#define CD_HOT __attribute__((hot))
#define CD_COLD __attribute__((cold))
#define CD_RESTRICT __restrict__
#define CD_UNLIKELY(x) __builtin_expect(!!(x), 0)
#define CD_LIKELY(x) __builtin_expect(!!(x), 1)
#define CD_DEBUG_BREAK() __builtin_trap()
#elif defined(_MSC_VER)
#define CD_FORCEINLINE __forceinline
#define CD_NOINLINE __declspec(noinline)
#define CD_PURE_FN
#define CD_ARTIFICIAL
#define CD_HOT
#define CD_COLD
#define CD_RESTRICT __restrict
#define CD_UNLIKELY(x) (x)
#define CD_LIKELY(x) (x)
#define CD_DEBUG_BREAK() __debugbreak()
#else
#define CD_FORCEINLINE inline
#define CD_NOINLINE
#define CD_PURE_FN
#define CD_ARTIFICIAL
#define CD_HOT
#define CD_COLD
#define CD_RESTRICT
#define CD_UNLIKELY(x) (x)
#define CD_LIKELY(x) (x)
#define CD_DEBUG_BREAK() std::abort()
#endif
#if !defined(NDEBUG)
#define CD_DEBUG 1
#define CD_ASSERT(expr, msg) \
do { \
if (!(expr)) { \
std::cerr << "[ASSERT FAILED] " << #expr << "\n" \
<< "Message: " << (msg) << "\n" \
<< "File: " << __FILE__ << ":" << __LINE__ << "\n"; \
CD_DEBUG_BREAK(); \
} \
} while (false)
#define CD_ASSUME(expr) CD_ASSERT(expr, "Assumption violated")
#else
#define CD_DEBUG 0
#define CD_ASSERT(expr, msg) \
do { \
(void)sizeof(expr); \
} while (false)
#if defined(__clang__)
#define CD_ASSUME(expr) __builtin_assume(expr)
#elif defined(_MSC_VER)
#define CD_ASSUME(expr) __assume(expr)
#elif defined(__GNUC__)
#define CD_ASSUME(expr) \
do { \
if (!(expr)) __builtin_unreachable(); \
} while (false)
#else
#define CD_ASSUME(expr) \
do { \
(void)sizeof(expr); \
} while (false)
#endif
#endif
namespace cd::containers {
/// @brief An allocator-aware vector container with an inline local buffer.
///
/// @details Stores up to `N` elements directly within the object footprint (SBO),
/// falling back to heap allocation only when the inline capacity is exceeded.
///
/// @note **The Cost of SBO**: Moving or copying a local container (where `data_ == local_buf_`)
/// is an `O(M)` operation (where `M` is `size()`), as elements must be moved/copied individually.
/// Pointer swaps are only used for heap-allocated containers.
///
/// @note **Trivial Relocation**: If `std::is_trivially_copyable_v<T>` is `true`, reallocations
/// (`grow` and `shrink_to_fit`) bypass move-constructors and destructors, utilizing `std::memcpy`.
///
/// @note **Aliasing-Safe Shifts**: Elements are shifted using standard `std::move` and
/// `std::move_backward` to safely handle overlapping memory during mutating operations.
///
/// @warning **Cache Pressure**: Best suited for temporary stack buffers or small collections
/// where the expected element count is `<= N`. Large values of `N` or nesting `SmallVec` inside
/// other containers can cause significant cache pressure due to the increased object footprint.
///
/// @warning **Self-Insertion**: Self-insertion (e.g., `vec.push_back(vec[0])`) under reallocation
/// is safe, guarded by internal bounds checks.
///
/// @tparam T The type of the elements.
/// @tparam N The capacity of the inline local buffer. Must be > 0.
/// @tparam Allocator The allocator to use when falling back to the heap.
template <typename T, size_t N, typename Allocator = std::allocator<T>>
class SmallVec {
static_assert(N > 0, "SmallVec capacity must be greater than 0");
public:
using value_type = T;
using allocator_type = Allocator;
using size_type = size_t;
using difference_type = ptrdiff_t;
using reference = T&;
using const_reference = const T&;
using pointer = T*;
using const_pointer = const T*;
using iterator = T*;
using const_iterator = const T*;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
private:
using alloc_traits = std::allocator_traits<Allocator>;
[[no_unique_address]] Allocator alloc_;
pointer data_{nullptr};
pointer end_{nullptr};
pointer cap_end_{nullptr};
alignas(T) std::byte local_buf_[sizeof(T) * N];
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL constexpr bool is_local() const noexcept {
return data_ == reinterpret_cast<const_pointer>(local_buf_);
}
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL bool is_in_buffer(const_pointer ptr) const noexcept {
return ptr >= data_ && ptr < end_;
}
CD_FORCEINLINE void init_local() noexcept {
data_ = reinterpret_cast<pointer>(local_buf_);
end_ = data_;
cap_end_ = data_ + N;
}
CD_FORCEINLINE void deallocate_if_heap() noexcept {
if (!is_local()) {
alloc_traits::deallocate(alloc_, data_, capacity());
}
}
CD_COLD CD_NOINLINE void move_to_heap(const size_t new_cap) {
pointer new_data = alloc_traits::allocate(alloc_, new_cap);
const size_t current_size = size();
if constexpr (std::is_trivially_copyable_v<T>) {
if (current_size > 0) {
std::memcpy(new_data, data_, current_size * sizeof(T));
}
} else {
try {
if constexpr (std::is_nothrow_move_constructible_v<T>) {
std::uninitialized_move(data_, end_, new_data);
} else {
std::uninitialized_copy(data_, end_, new_data);
}
} catch (...) {
alloc_traits::deallocate(alloc_, new_data, new_cap);
throw;
}
std::destroy(data_, end_);
}
deallocate_if_heap();
data_ = new_data;
end_ = new_data + current_size;
cap_end_ = new_data + new_cap;
}
CD_COLD CD_NOINLINE void grow(const size_t required_cap) {
const size_t old_cap = capacity();
const size_t new_cap = std::max(old_cap * 2, required_cap);
move_to_heap(new_cap);
}
CD_FORCEINLINE void insert_impl(size_t index, auto&& val) {
iterator pos = data_ + index;
if (pos == end_) {
std::construct_at(end_, std::forward<decltype(val)>(val));
} else {
std::construct_at(end_, std::move(*(end_ - 1)));
std::move_backward(pos, end_ - 1, end_);
*pos = std::forward<decltype(val)>(val);
}
}
public:
#pragma region CONSTRUCTORS
SmallVec() noexcept(std::is_nothrow_default_constructible_v<Allocator>) { // NOLINT(*-pro-type-member-init)
init_local();
}
explicit SmallVec(const size_t count, const Allocator& alloc = Allocator()) // NOLINT(*-pro-type-member-init)
: alloc_(alloc) {
init_local();
if (CD_UNLIKELY(count > N)) move_to_heap(count);
pointer current = data_;
try {
for (size_t i = 0; i < count; ++i) {
std::construct_at(current++);
}
} catch (...) {
std::destroy(data_, current);
deallocate_if_heap();
throw;
}
end_ = data_ + count;
}
SmallVec(const size_t count, const T& value, const Allocator& alloc = Allocator()) // NOLINT(*-pro-type-member-init)
: alloc_(alloc) {
init_local();
if (CD_UNLIKELY(count > N)) move_to_heap(count);
pointer current = data_;
try {
for (size_t i = 0; i < count; ++i) {
std::construct_at(current++, value);
}
} catch (...) {
std::destroy(data_, current);
deallocate_if_heap();
throw;
}
end_ = data_ + count;
}
SmallVec(std::initializer_list<T> il, const Allocator& alloc = Allocator()) // NOLINT(*-pro-type-member-init)
: alloc_(alloc) {
init_local();
const size_t count = il.size();
if (CD_UNLIKELY(count > N)) move_to_heap(count);
pointer current = data_;
try {
for (const auto& val : il) {
std::construct_at(current++, val);
}
} catch (...) {
std::destroy(data_, current);
deallocate_if_heap();
throw;
}
end_ = data_ + count;
}
template <std::input_iterator It, std::sentinel_for<It> S>
SmallVec(It first, S last, const Allocator& alloc = Allocator()) : alloc_(alloc) { // NOLINT(*-pro-type-member-init)
init_local();
if constexpr (std::forward_iterator<It>) {
const auto count = static_cast<size_t>(std::distance(first, last));
if (CD_UNLIKELY(count > N)) move_to_heap(count);
pointer current = data_;
try {
for (auto it = first; it != last; ++it) {
std::construct_at(current++, *it);
}
} catch (...) {
std::destroy(data_, current);
deallocate_if_heap();
throw;
}
end_ = data_ + count;
} else {
for (; first != last; ++first) {
push_back(*first);
}
}
}
SmallVec(const SmallVec& other) // NOLINT(*-pro-type-member-init)
: alloc_(alloc_traits::select_on_container_copy_construction(other.alloc_)) {
init_local();
const size_t count = other.size();
if (CD_UNLIKELY(count > N)) move_to_heap(count);
pointer current = data_;
try {
for (auto it = other.begin(); it != other.end(); ++it) {
std::construct_at(current++, *it);
}
} catch (...) {
std::destroy(data_, current);
deallocate_if_heap();
throw;
}
end_ = data_ + count;
}
SmallVec(SmallVec&& other) noexcept : alloc_(std::move(other.alloc_)) { // NOLINT(*-pro-type-member-init)
if (other.is_local()) {
init_local();
std::uninitialized_move(other.begin(), other.end(), data_);
end_ = data_ + other.size();
other.clear();
} else {
auto local_ptr = reinterpret_cast<pointer>(other.local_buf_);
data_ = std::exchange(other.data_, local_ptr);
end_ = std::exchange(other.end_, local_ptr);
cap_end_ = std::exchange(other.cap_end_, local_ptr + N);
}
}
~SmallVec() {
std::destroy(data_, end_);
deallocate_if_heap();
}
#pragma endregion
#pragma region ASSIGNMENT
SmallVec& operator=(const SmallVec& other) {
if (CD_UNLIKELY(this == &other)) return *this;
SmallVec tmp(other);
swap(tmp);
return *this;
}
SmallVec& operator=(SmallVec&& other) noexcept {
if (CD_UNLIKELY(this == &other)) return *this;
std::destroy(data_, end_);
deallocate_if_heap();
if constexpr (alloc_traits::propagate_on_container_move_assignment::value) {
alloc_ = std::move(other.alloc_);
if (other.is_local()) {
init_local();
std::uninitialized_move(other.begin(), other.end(), data_);
end_ = data_ + other.size();
other.clear();
} else {
auto local_ptr = reinterpret_cast<pointer>(other.local_buf_);
data_ = std::exchange(other.data_, local_ptr);
end_ = std::exchange(other.end_, local_ptr);
cap_end_ = std::exchange(other.cap_end_, local_ptr + N);
}
} else {
if (alloc_ == other.alloc_) {
if (other.is_local()) {
init_local();
std::uninitialized_move(other.begin(), other.end(), data_);
end_ = data_ + other.size();
other.clear();
} else {
auto local_ptr = reinterpret_cast<pointer>(other.local_buf_);
data_ = std::exchange(other.data_, local_ptr);
end_ = std::exchange(other.end_, local_ptr);
cap_end_ = std::exchange(other.cap_end_, local_ptr + N);
}
} else {
init_local();
reserve(other.size());
pointer current = data_;
try {
for (auto it = other.begin(); it != other.end(); ++it) {
std::construct_at(current++, std::move(*it));
}
} catch (...) {
std::destroy(data_, current);
deallocate_if_heap();
throw;
}
end_ = data_ + other.size();
other.clear();
}
}
return *this;
}
SmallVec& operator=(std::initializer_list<T> il) {
assign(il.begin(), il.end());
return *this;
}
#pragma endregion
#pragma region ACCESS_AND_ITERATORS
[[nodiscard]] CD_HOT CD_FORCEINLINE CD_ARTIFICIAL reference operator[](const size_t pos) noexcept {
CD_ASSUME(pos < size());
return data_[pos];
}
[[nodiscard]] CD_HOT CD_FORCEINLINE CD_ARTIFICIAL const_reference operator[](const size_t pos) const noexcept {
CD_ASSUME(pos < size());
return data_[pos];
}
[[nodiscard]] reference at(const size_t pos) {
if (CD_UNLIKELY(pos >= size())) throw std::out_of_range("SmallVec::at");
return data_[pos];
}
[[nodiscard]] const_reference at(const size_t pos) const {
if (CD_UNLIKELY(pos >= size())) throw std::out_of_range("SmallVec::at");
return data_[pos];
}
[[nodiscard]] CD_HOT CD_FORCEINLINE CD_ARTIFICIAL reference front() noexcept {
CD_ASSUME(!empty());
return *data_;
}
[[nodiscard]] CD_HOT CD_FORCEINLINE CD_ARTIFICIAL const_reference front() const noexcept {
CD_ASSUME(!empty());
return *data_;
}
[[nodiscard]] CD_HOT CD_FORCEINLINE CD_ARTIFICIAL reference back() noexcept {
CD_ASSUME(!empty());
return *(end_ - 1);
}
[[nodiscard]] CD_HOT CD_FORCEINLINE CD_ARTIFICIAL const_reference back() const noexcept {
CD_ASSUME(!empty());
return *(end_ - 1);
}
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL pointer data() noexcept { return data_; }
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL const_pointer data() const noexcept { return data_; }
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL iterator begin() noexcept { return data_; }
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL const_iterator begin() const noexcept { return data_; }
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL const_iterator cbegin() const noexcept { return data_; }
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL iterator end() noexcept { return end_; }
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL const_iterator end() const noexcept { return end_; }
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL const_iterator cend() const noexcept { return end_; }
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL reverse_iterator rbegin() noexcept {
return reverse_iterator(end_);
}
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL const_reverse_iterator rbegin() const noexcept {
return const_reverse_iterator(end_);
}
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL reverse_iterator rend() noexcept {
return reverse_iterator(data_);
}
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL const_reverse_iterator rend() const noexcept {
return const_reverse_iterator(data_);
}
#pragma endregion
#pragma region CAPACITY
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL bool empty() const noexcept { return data_ == end_; }
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL size_t size() const noexcept {
return static_cast<size_t>(end_ - data_);
}
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE CD_ARTIFICIAL size_t capacity() const noexcept {
return static_cast<size_t>(cap_end_ - data_);
}
[[nodiscard]] CD_PURE_FN CD_FORCEINLINE size_t max_size() const noexcept { return alloc_traits::max_size(alloc_); }
void reserve(const size_t new_cap) {
if (CD_UNLIKELY(new_cap > capacity())) {
move_to_heap(new_cap);
}
}
void shrink_to_fit() {
const size_t current_size = size();
const size_t current_cap = capacity();
if (is_local() || current_cap == current_size) {
return;
}
pointer old_data = data_;
const size_t old_cap = current_cap;
if (current_size <= N) {
init_local();
if constexpr (std::is_trivially_copyable_v<T>) {
if (CD_LIKELY(current_size > 0)) {
std::memcpy(data_, old_data, current_size * sizeof(T));
}
} else {
try {
if constexpr (std::is_nothrow_move_constructible_v<T>) {
std::uninitialized_move(old_data, old_data + current_size, data_);
} else {
std::uninitialized_copy(old_data, old_data + current_size, data_);
}
} catch (...) {
data_ = old_data;
end_ = old_data + current_size;
cap_end_ = old_data + old_cap;
throw;
}
std::destroy(old_data, old_data + current_size);
}
end_ = data_ + current_size;
alloc_traits::deallocate(alloc_, old_data, old_cap);
} else {
pointer new_data = alloc_traits::allocate(alloc_, current_size);
if constexpr (std::is_trivially_copyable_v<T>) {
std::memcpy(new_data, old_data, current_size * sizeof(T));
} else {
try {
if constexpr (std::is_nothrow_move_constructible_v<T>) {
std::uninitialized_move(old_data, old_data + current_size, new_data);
} else {
std::uninitialized_copy(old_data, old_data + current_size, new_data);
}
} catch (...) {
alloc_traits::deallocate(alloc_, new_data, current_size);
throw;
}
std::destroy(old_data, old_data + current_size);
}
alloc_traits::deallocate(alloc_, old_data, old_cap);
data_ = new_data;
end_ = new_data + current_size;
cap_end_ = new_data + current_size;
}
}
#pragma endregion
#pragma region MODIFIERS
void clear() noexcept {
std::destroy(data_, end_);
end_ = data_;
}
template <std::input_iterator It, std::sentinel_for<It> S>
void assign(It first, S last) {
clear();
if constexpr (std::forward_iterator<It>) {
const auto count = static_cast<size_t>(std::distance(first, last));
if (CD_UNLIKELY(count > capacity())) {
deallocate_if_heap();
move_to_heap(count);
}
std::uninitialized_copy(first, last, data_);
end_ = data_ + count;
} else {
for (; first != last; ++first) {
push_back(*first);
}
}
}
void assign(const size_t count, const T& value) {
clear();
if (CD_UNLIKELY(count > capacity())) {
deallocate_if_heap();
move_to_heap(count);
}
std::uninitialized_fill_n(data_, count, value);
end_ = data_ + count;
}
void assign(std::initializer_list<T> il) { assign(il.begin(), il.end()); }
iterator insert(const_iterator pos, const T& value) {
const auto index = static_cast<size_t>(pos - data_);
if (CD_UNLIKELY(end_ == cap_end_)) {
if (is_in_buffer(&value)) {
T tmp(value);
grow(size() + 1);
insert_impl(index, std::move(tmp));
} else {
grow(size() + 1);
insert_impl(index, value);
}
} else {
insert_impl(index, value);
}
return data_ + index;
}
iterator insert(const_iterator pos, T&& value) {
const auto index = static_cast<size_t>(pos - data_);
if (CD_UNLIKELY(end_ == cap_end_)) {
grow(size() + 1);
}
insert_impl(index, std::move(value));
return data_ + index;
}
iterator erase(const_iterator pos) {
auto dst = const_cast<iterator>(pos);
if (dst != end_ - 1) {
// Безопасный сдвиг влево
std::move(dst + 1, end_, dst);
}
--end_;
std::destroy_at(end_);
return dst;
}
iterator erase(const_iterator first, const_iterator last) {
if (first != last) {
auto dst = const_cast<iterator>(first);
auto src = const_cast<iterator>(last);
iterator new_end = std::move(src, end_, dst);
std::destroy(new_end, end_);
end_ = new_end;
}
return const_cast<iterator>(first);
}
template <typename... Args>
CD_HOT reference emplace_back(Args&&... args) {
if (CD_UNLIKELY(end_ == cap_end_)) {
grow(size() + 1);
}
std::construct_at(end_, std::forward<Args>(args)...);
++end_;
return *(end_ - 1);
}
CD_HOT void push_back(const T& value) {
if (CD_UNLIKELY(end_ == cap_end_)) {
if (is_in_buffer(&value)) {
T tmp(value);
grow(size() + 1);
std::construct_at(end_, std::move(tmp));
} else {
grow(size() + 1);
std::construct_at(end_, value);
}
} else {
std::construct_at(end_, value);
}
++end_;
}
CD_HOT void push_back(T&& value) {
if (CD_UNLIKELY(end_ == cap_end_)) {
if (is_in_buffer(&value)) {
T tmp(std::move(value));
grow(size() + 1);
std::construct_at(end_, std::move(tmp));
} else {
grow(size() + 1);
std::construct_at(end_, std::move(value));
}
} else {
std::construct_at(end_, std::move(value));
}
++end_;
}
CD_HOT void pop_back() noexcept {
CD_ASSERT(!empty(), "Cannot pop_back from empty SmallVec");
CD_ASSUME(!empty());
--end_;
std::destroy_at(end_);
}
void resize(const size_t count) {
if (count < size()) {
std::destroy(data_ + count, end_);
end_ = data_ + count;
} else if (count > size()) {
if (CD_UNLIKELY(count > capacity())) grow(count);
std::uninitialized_default_construct(end_, data_ + count);
end_ = data_ + count;
}
}
void resize(const size_t count, const T& value) {
if (count < size()) {
std::destroy(data_ + count, end_);
end_ = data_ + count;
} else if (count > size()) {
if (CD_UNLIKELY(count > capacity())) grow(count);
std::uninitialized_fill(end_, data_ + count, value);
end_ = data_ + count;
}
}
void swap(SmallVec& other) noexcept {
if (CD_UNLIKELY(this == &other)) return;
if (!is_local() && !other.is_local()) {
std::swap(data_, other.data_);
std::swap(end_, other.end_);
std::swap(cap_end_, other.cap_end_);
if constexpr (alloc_traits::propagate_on_container_swap::value) {
std::swap(alloc_, other.alloc_);
}
return;
}
SmallVec tmp(std::move(*this));
*this = std::move(other);
other = std::move(tmp);
}
#pragma endregion
#pragma region COMPARISON
[[nodiscard]] bool operator==(const SmallVec& other) const {
if (size() != other.size()) return false;
return std::equal(begin(), end(), other.begin());
}
[[nodiscard]] std::strong_ordering operator<=>(const SmallVec& other) const {
return std::lexicographical_compare_three_way(begin(), end(), other.begin(), other.end(), std::compare_three_way{});
}
#pragma endregion
};
} // namespace cd::containers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment