Когда нужен пул объектов
Куча — универсальна, но проблематична в системах реального времени:
- Недетерминированное время выделения
- Фрагментация при длительной работе
- Режим отказа — крэш во время выполнения, а не ошибка компиляции
Пул выделяет всю память заранее в массиве фиксированного размера. Каждый слот хранит один объект. Выделение — это извлечение указателя; освобождение — помещение указателя. Оба 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 (список свободных использует память слота) |
| Потокобезопасность | Нужна критическая секция для нескольких производителей |
| Время жизни объекта | Произвольное (в отличие от арены) |
| Ёмкость | Фиксирована на этапе компиляции |