Когда нужен пул объектов

Куча — универсальна, но проблематична в системах реального времени:

  • Недетерминированное время выделения
  • Фрагментация при длительной работе
  • Режим отказа — крэш во время выполнения, а не ошибка компиляции

Пул выделяет всю память заранее в массиве фиксированного размера. Каждый слот хранит один объект. Выделение — это извлечение указателя; освобождение — помещение указателя. Оба O(1) и детерминированы.

Используйте пул когда:

  • Объекты одного типа (или одного максимального размера)
  • Время жизни произвольное (в отличие от аллокаторов-арен)
  • Скорость выделения ограничена и известна на этапе компиляции

Реализация

 1// object_pool.h
 2#pragma once
 3#include <array>
 4#include <cassert>
 5#include <cstddef>
 6#include <memory>
 7#include <new>
 8
 9template <typename T, size_t Capacity>
10class ObjectPool {
11public:
12    ObjectPool() noexcept {
13        // Строим интрузивный список свободных через хранилище
14        for (size_t i = 0; i < Capacity - 1; ++i)
15            slots_[i].next = &slots_[i + 1];
16        slots_[Capacity - 1].next = nullptr;
17        head_ = &slots_[0];
18    }
19
20    // Выделить сырое хранилище — вызывающий должен создать объект через placement-new
21    T* allocate() noexcept {
22        if (!head_) return nullptr;
23        Slot* s = head_;
24        head_   = s->next;
25        --free_;
26        return reinterpret_cast<T*>(s->storage);
27    }
28
29    // Освободить — вызывающий должен уже уничтожить объект
30    void deallocate(T* ptr) noexcept {
31        Slot* s = reinterpret_cast<Slot*>(ptr);
32        s->next = head_;
33        head_   = s;
34        ++free_;
35    }
36
37    size_t capacity() const noexcept { return Capacity; }
38    size_t available() const noexcept { return free_; }
39    bool   full()  const noexcept { return free_ == 0; }
40    bool   empty() const noexcept { return free_ == Capacity; }
41
42private:
43    union Slot {
44        alignas(T) char storage[sizeof(T)];
45        Slot* next;
46    };
47
48    std::array<Slot, Capacity> slots_;
49    Slot*  head_ = nullptr;
50    size_t free_ = Capacity;
51};

Использование — ручное получение/освобождение

 1static ObjectPool<SensorSample, 32> pool;
 2
 3// Выделить + сконструировать
 4SensorSample* s = pool.allocate();
 5if (s) {
 6    new (s) SensorSample{HAL_GetTick(), readVoltage()};
 7    processQueue.push(s);
 8}
 9
10// После обработки — уничтожить + освободить
11SensorSample* s = processQueue.pop();
12if (s) {
13    s->~SensorSample();       // явный вызов деструктора
14    pool.deallocate(s);
15}

Использование — с unique_ptr (RAII)

unique_ptr с кастомным удалителем автоматизирует шаги уничтожения и освобождения:

 1template <typename T, size_t Cap>
 2class ObjectPool {
 3public:
 4    // ...
 5
 6    struct Deleter {
 7        ObjectPool* pool;
 8        void operator()(T* ptr) const noexcept {
 9            ptr->~T();
10            pool->deallocate(ptr);
11        }
12    };
13
14    using UniquePtr = std::unique_ptr<T, Deleter>;
15
16    template <typename... Args>
17    UniquePtr make(Args&&... args) {
18        T* ptr = allocate();
19        if (!ptr) return {nullptr, {this}};
20        new (ptr) T(std::forward<Args>(args)...);
21        return {ptr, {this}};
22    }
23};

Использование:

 1static ObjectPool<Message, 16> msgPool;
 2
 3// Выделить, сконструировать, вернуть RAII-дескриптор
 4auto msg = msgPool.make(0x01, payload, len);
 5if (!msg) {
 6    // пул исчерпан
 7    return;
 8}
 9
10sendQueue.push(std::move(msg));  // передаёт владение в очередь
11
12// Когда msg выходит из области видимости — деструктор вызывается, слот возвращается в пул
13// Никакой ручной очистки

Потокобезопасный пул для ISR

На одноядерном Cortex-M пул безопасен для ISR, если обе стороны работают на одном ядре и выделение/освобождение не перемежаются (паттерн SPSC: ISR выделяет, задача освобождает, или наоборот).

Для вытесняющей RTOS или многоядерных систем оборачивайте операции со списком свободных в критическую секцию:

 1T* allocate() noexcept {
 2    taskENTER_CRITICAL();         // критическая секция FreeRTOS (или __disable_irq)
 3    Slot* s = head_;
 4    if (s) { head_ = s->next; --free_; }
 5    taskEXIT_CRITICAL();
 6    return s ? reinterpret_cast<T*>(s->storage) : nullptr;
 7}
 8
 9void deallocate(T* ptr) noexcept {
10    Slot* s = reinterpret_cast<Slot*>(ptr);
11    taskENTER_CRITICAL();
12    s->next = head_;
13    head_   = s;
14    ++free_;
15    taskEXIT_CRITICAL();
16}

На настольных системах используйте std::mutex или lock-free список свободных (атомарный compare-exchange на указатель head с одним записывателем).


Размещение в памяти

Объединение Slot — ключевой элемент: оно повторно использует одну и ту же память как для хранимого объекта, так и для указателя списка свободных. Никакого отдельного массива для учёта.

slots_[0]: [ T или next* ]  ← 32 байта (sizeof(T) или sizeof(ptr))
slots_[1]: [ T или next* ]
...
slots_[N-1]: [ T или next* ]

Общий объём памяти: N * max(sizeof(T), sizeof(void*)) — ровно столько же, что и для сырого массива, с нулевыми дополнительными накладными расходами на слот.


Проверка ёмкости на этапе компиляции

1template <typename T, size_t Cap>
2class ObjectPool {
3    static_assert(Cap > 0, "Ёмкость пула должна быть положительной");
4    static_assert(sizeof(T) >= sizeof(void*),
5                  "T должен быть не меньше указателя для списка свободных");
6};

Если нужен пул для очень маленьких типов (меньше указателя), дополните слот:

1union Slot {
2    alignas(alignof(T)) char storage[std::max(sizeof(T), sizeof(Slot*))];
3    Slot* next;
4};

Полный заголовочный файл

 1// object_pool.h — один заголовок, без зависимостей
 2#pragma once
 3#include <array>
 4#include <cstddef>
 5#include <memory>
 6#include <new>
 7#include <type_traits>
 8
 9template <typename T, size_t Cap>
10class ObjectPool {
11    static_assert(Cap > 0);
12    union Slot {
13        alignas(T) char buf[sizeof(T) < sizeof(Slot*) ? sizeof(Slot*) : sizeof(T)];
14        Slot* next;
15    };
16
17    std::array<Slot, Cap> pool_;
18    Slot*  head_  = nullptr;
19    size_t avail_ = Cap;
20
21    void buildFreeList() {
22        for (size_t i = 0; i < Cap - 1; ++i) pool_[i].next = &pool_[i + 1];
23        pool_[Cap - 1].next = nullptr;
24        head_ = &pool_[0];
25    }
26
27public:
28    ObjectPool() noexcept { buildFreeList(); }
29
30    struct Deleter {
31        ObjectPool* p;
32        void operator()(T* ptr) const noexcept {
33            ptr->~T();
34            p->deallocate(ptr);
35        }
36    };
37    using Ptr = std::unique_ptr<T, Deleter>;
38
39    template <typename... Args>
40    Ptr make(Args&&... args) noexcept(std::is_nothrow_constructible_v<T, Args...>) {
41        T* raw = allocate();
42        if (!raw) return {nullptr, {this}};
43        return {new (raw) T(std::forward<Args>(args)...), {this}};
44    }
45
46    T* allocate() noexcept {
47        if (!head_) return nullptr;
48        Slot* s = head_; head_ = s->next; --avail_;
49        return reinterpret_cast<T*>(s->buf);
50    }
51
52    void deallocate(T* ptr) noexcept {
53        Slot* s = reinterpret_cast<Slot*>(ptr);
54        s->next = head_; head_ = s; ++avail_;
55    }
56
57    size_t available() const noexcept { return avail_; }
58    size_t capacity()  const noexcept { return Cap; }
59};

Краткий справочник

Свойство Значение
Выделение O(1) — извлечение из списка свободных
Освобождение O(1) — помещение в список свободных
Фрагментация Нет — фиксированные слоты
Накладные расходы на слот 0 (список свободных использует память слота)
Потокобезопасность Нужна критическая секция для нескольких производителей
Время жизни объекта Произвольное (в отличие от арены)
Ёмкость Фиксирована на этапе компиляции