📁 Header: <jh/concurrent/flat_pool.h>
📦 Namespace: jh::conc
📅 Version: 1.4.x (2025)
👤 Author: JeongHan-Bae <[email protected]>
jh::conc::flat_pool<Key, Value, Hash, Alloc> is a concurrent, key-based interning container that stores objects in
contiguous memory and provides GC-like lifetime semantics through explicit reference counting.
Each unique Key corresponds to at most one logical entry in the pool.
Acquisition returns a lightweight, reference-counted handle (flat_pool::ptr) that refers to a stable index, not a
raw pointer.
Unlike pointer-based pools, flat_pool:
- owns its objects
- does not rely on
std::shared_ptrfor synchronization - controls all concurrency internally
- reuses slots instead of eagerly destroying objects
This design minimizes allocation churn, avoids platform-specific shared_ptr behavior, and provides predictable
performance under high concurrency.
flat_poolstores values, control blocks, and indices usingstd::vectoror vector-like contiguous data structures. All internal memory regions grow monotonically and are reused in place, resulting in minimal fragmentation and highly predictable access patterns.Insertions write directly into contiguous storage. When a previously released slot exists, the pool locates the next available position using a lightweight scan over an occupation bitmap. Although this scan is theoretically amortized O(log N), in practice it degenerates into a sequence of simple
0/1checks with excellent branch predictability and cache locality.Once the pool reaches a stable working size, it is expected not to expand further. The intended usage pattern is:
- grow to a peak size,
- then continuously acquire and invalidate entries,
- reusing existing slots rather than allocating new ones.
Conceptually, this places
flat_poolcloser to an arena + GC-style reuse model than to a classical associative container.From a purely algorithmic standpoint, lookup and insertion in
flat_poolare theoretically inferior to hash-table–based designs such aspointer_pool(which relies onunordered_map). However, in practice:
- pointer-based containers suffer from pointer-induced fragmentation,
- cache-line scattering and L3 cache regression,
- and the non-trivial overhead of
std::shared_ptrcontrol blocks.As a result,
pointer_poolis ill-suited for high-frequency, large-volume, data-oriented workloads.
flat_pool, by contrast, is optimized for:
- pure or near-pure data objects,
- relatively large populations,
- frequent reuse rather than destruction,
- and cache-friendly, contiguous memory access.
In summary:
flat_poolis designed for data-centric storage with high churn and reuse.pointer_poolis better suited for semantic objects—objects whose identity, > lifetime, and destruction semantics are more important than raw storage efficiency.
Object identity is defined exclusively by Key.
- The stored object is never compared
- Hashing and equality are performed only on
Key - Keys are accepted in cv/ref-qualified form and are not retained unless insertion succeeds
Requirements for Key:
- cheap to hash
- cheap to compare
- capable of representing identity independently of storage
This enables lookup-before-construction and avoids provisional object creation.
| Member | Type | Description |
|---|---|---|
value_type |
Key or std::pair<Key, Value> |
Stored element type. Key in set-like mode, (Key, Value) in map-like mode. |
allocator_type |
Rebound allocator | Allocator used for contiguous value storage only. |
| Member | Type | Description |
|---|---|---|
flat_pool() |
Constructor | Constructs an empty pool with minimum reserved capacity. |
explicit flat_pool(size_t reserve) |
Constructor | Constructs an empty pool with explicit reserved capacity (≥ MIN_RESERVED_SIZE). |
explicit flat_pool(const allocator_type&) |
Constructor | Constructs a pool using a custom allocator. |
flat_pool(size_t, const allocator_type&) |
Constructor | Constructs a pool with custom allocator and reserved capacity. |
Deleted:
| Member | Description |
|---|---|
flat_pool(const flat_pool&) |
Copy construction disabled. |
flat_pool(flat_pool&&) |
Move construction disabled. |
| Copy / move assignment | Disabled to preserve index and handle validity. |
| Member | Type | Description |
|---|---|---|
ptr acquire(KArg&& key) |
Set-like | Interns and returns a handle to a key (Value == monostate). |
ptr acquire(KArg&& key, std::tuple<Args...>) |
Map-like | Interns a key–value entry, constructing value once if absent. |
ptr find(const Key&) |
Lookup | Returns a handle to an existing entry; returns nullptr if the key is not present. |
Version note
1.4.0:
flat_pool::ptrdoes not defineoperator bool().
Use:if (auto p = pool.find(...); p != nullptr) { ... }Since 1.4.1:
flat_pool::ptrprovidesexplicit operator bool().
You may write:if (auto p = pool.find(...)) { ... }which is semantically equivalent to comparing against
nullptr.
Deleted (by design):
| Member | Description |
|---|---|
acquire(key) (map-like) |
Deleted to prevent accidental value omission. |
acquire(key, args...) (set-like) |
Ill-formed: no value construction in set-like mode. |
| Member | Type | Description |
|---|---|---|
ptr() |
Constructor | Constructs a null handle. |
ptr(nullptr_t) |
Constructor | Explicit null handle. |
ptr(const ptr&) |
Copy | Shares the reference and increments refcount. |
ptr(ptr&&) |
Move | Transfers ownership without refcount change. |
~ptr() |
Destructor | Releases reference (GC-like). |
reset() |
Modifier | Releases the reference and becomes null. |
operator*() |
Access | Returns reference to stored object (guard required in MT). |
operator->() |
Access | Returns pointer to stored object (guard required in MT). |
operator== |
Comparison | Compares handle identity or against nullptr. |
guard() |
Guard | Prevents pool reallocation during dereference. |
explicit operator bool() |
Conversion | Returns true if the handle is non-null. (1.4.1+) |
| Member | Type | Description |
|---|---|---|
bool empty() |
Observer | Returns true if no active entries exist. |
size_t size() |
Observer | Number of active (live) entries. |
size_t capacity() |
Observer | Current storage capacity (historical peak). |
pair<size_t, size_t> occupancy_rate() |
Observer | Snapshot of (capacity, active_entries) under shared lock. |
| Member | Type | Description |
|---|---|---|
void resize_pool() |
Maintenance | Shrinks storage to fit active entries (blocking, cold-path only). |
| Member | Type | Description |
|---|---|---|
MIN_RESERVED_SIZE |
constexpr uint64_t |
Minimum reserved capacity for internal storage. |
flat_poolinternally relies on a hash-ordered, contiguous index structure based onordered_set.ordered_setis itself a public, reusable component and is documented separately.
👉 See: ordered_set
The pool behaves as a concurrent deduplicating set.
jh::conc::flat_pool<std::string> pool;
std::string key = "alpha";
auto p1 = pool.acquire(key); // const std::string&
auto p2 = pool.acquire(std::string(key)); // std::string&&Only keys are stored. No value construction occurs.
The pool behaves as a concurrent, deduplicating map.
jh::conc::flat_pool<int, std::string> pool;
auto p = pool.acquire(
42,
std::forward_as_tuple("meaning of life")
);Internally, the stored type is:
std::pair<Key, Value>-
Value construction occurs exactly once per key
-
If the key already exists:
- the provided argument tuple is discarded
- the existing value is reused
-
Repeated
acquire()calls never update the value
This is intentional: flat_pool is not a mutable map.
When Value is jh::typed::monostate, flat_pool operates in set-like mode.
In this mode, the pool represents a concurrent, deduplicating set of keys, rather than a key–value map.
In set-like mode:
- Only keys are stored
- There is no associated value
- Each unique
Keycorresponds to at most one logical entry - The stored element type is exactly:
Keyrather than std::pair<Key, Value>.
The pool therefore acts as a key-interning table with GC-like lifetime semantics and contiguous storage.
The only valid acquisition form in set-like mode is:
ptr acquire(KArg&& key);where KArg must be a cv/ref-qualified variant of Key:
Key&const Key&Key&&
There is no heterogeneous or transparent lookup.
If an argument of another type is provided, it is implicitly converted to Key first, and the lookup is performed
using the resulting Key object.
Example with Key = std::string:
jh::conc::flat_pool<std::string> pool;
std::string key = "alpha";
auto p1 = pool.acquire(key); // const std::string&
auto p2 = pool.acquire(std::string(key)); // std::string&&Properties:
- Both calls perform lookup using a
std::string - Deduplication is based solely on
std::stringhash and equality - The two handles refer to the same pooled slot
In set-like mode:
- There is no value
- There is no construction argument tuple
As a result:
acquire(key, args...)is ill-formed and deleted at compile time.
This enforces a strict separation between:
- set-like identity interning
- map-like key–value construction
Dereferencing a handle yields a reference to the stored key:
Key&Example:
auto p = pool.acquire(key);
[[maybe_unused]] auto g = p.guard();
use_key(*p); // *p is a Key&The key is stored by value inside the pool and participates fully in:
- contiguous storage
- reference counting
- slot reuse
- deferred destruction
Reference counting behaves identically to map-like mode:
-
Copying
ptrincrements the reference count -
Destroying or resetting
ptrdecrements it -
When the reference count reaches zero:
- the slot becomes unoccupied
- the key is not immediately destroyed
- the slot may later be reused for a different key
This GC-like behavior avoids frequent destruction and reallocation of keys.
Even though only keys are stored, the same dereferencing rules apply:
- keys reside in contiguous storage
- insertions and
resize_pool()may reallocate
Therefore:
- single-threaded code:
guard()is unnecessary - multi-threaded code:
guard()is required when dereferencing
Recommended, migration-safe style:
auto p = pool.acquire(key);
[[maybe_unused]] auto g = p.guard();
use_key(*p);Set-like flat_pool is well-suited for:
- string interning
- symbol or token tables
- canonical identifiers
- shared configuration keys
- high-frequency deduplication of lightweight objects
In these scenarios:
- identity is fully represented by the key
- delayed destruction and slot reuse are desirable
- contiguous storage improves cache locality
Set-like mode is not a secondary or restricted feature.
Conceptually:
- set-like mode is map-like mode with the value omitted
- all concurrency, lifetime, and reuse semantics are identical
- only value construction and storage differ
This allows users to:
- begin with pure key interning
- later migrate to key–value storage
- without changing the fundamental usage or mental model
flat_pool::ptr behaves logically like std::shared_ptr, but:
- it refers to a pool-managed slot
- it does not own memory directly
- destruction is deferred
-
Copy → increments refcount
-
Destruction /
reset()→ decrements refcount -
When refcount reaches zero:
- the slot becomes reusable
- the object is not immediately destroyed
This GC-like model avoids destructor / constructor churn.
flat_pool stores objects in contiguous storage (std::vector).
- Insertions or
resize_pool()may trigger reallocation - Reallocation invalidates references (
T&,T*) - The handle itself remains logically valid
To prevent this, dereferencing must be protected in concurrent scenarios.
In strictly single-threaded code:
auto p = pool.acquire(key);
use(*p); // safeguard() is not required and has no effect.
In multi-threaded contexts, if any thread may:
- insert
- release
- or call
resize_pool()
then dereferencing requires a guard:
auto p = pool.acquire(key);
[[maybe_unused]] auto g = p.guard();
use(*p);- returns an internal, private RAII type
- non-copyable
- non-movable
- non-operable
- exists purely for scope-based lifetime protection
It must be used with RAII / scope semantics.
The [[maybe_unused]] attribute is recommended to silence static analysis warnings.
If code may later migrate from single-threaded to multi-threaded, you may:
[[maybe_unused]] auto g = p.guard();
use(*p);even in single-threaded builds.
- The performance cost is negligible
- The code becomes future-proof
- No behavioral change is introduced
value_factory<Value> defines how values are constructed in map-like pools.
Its role is not to manage pooling itself, but to:
- customize how a
Valueinstance is created - optionally redirect allocation strategy
- reduce fragmentation for pointer-heavy types
This is especially useful when Value:
- is a handle type (
shared_ptr,unique_ptr) - internally owns dynamic buffers (
vector,string) - requires custom construction logic
Value(args...)Specializations are provided for:
std::shared_ptr<T>→std::make_shared<T>std::unique_ptr<T>→std::make_unique<T>
namespace jh::conc::extension {
template<>
struct value_factory<Foo> {
static Foo make(int a, int b) {
return Foo(a * 2, b * 2);
}
};
}This is an intentional public extension point. No subclassing or pool modification is required.
resize_pool():
- acquires exclusive locks
- scans for the highest active slot
- shrinks storage to the next power-of-two ≥ required size
- respects
MIN_RESERVED_SIZE
This is a blocking operation.
resize_pool() while holding pool-related locks.
Examples of incorrect usage:
-
calling
resize_pool()while holding:- a
no_reallocate_guard - an internal pool lock (directly or indirectly)
- a
This will immediately deadlock.
resize_pool()must be called from a cold path- no active dereferencing scopes
- no guards held
Holding a flat_pool::ptr is fine
(pointer ownership alone does not hold locks).
If your object:
- is not copyable or movable
- does not require immediate destruction
- can be deduplicated via a key
then this is a valid and recommended pattern:
jh::conc::flat_pool<Key, std::shared_ptr<V>>
jh::conc::flat_pool<Key, std::unique_ptr<V>>- The pool manages identity and reuse
- The pointer manages object lifetime
- You avoid
shared_ptr-based interning containers
On POSIX platforms (macOS, Linux GCC ≥13), std::shared_mutex implementations are typically backed by
pthread_rwlock or equivalent primitives that, in practice, exhibit strong global ordering behavior.
Under these environments, flat_pool does not introduce additional fences.
On Windows, the situation differs:
The ISO C++ standard does not require std::shared_mutex to establish a global total order.
It only guarantees acquire–release semantics.
To approximate POSIX-style behavior, flat_pool introduces:
std::atomic_thread_fence(std::memory_order_seq_cst);around shared mutex boundaries (via posix_smtx_*_lock).
This is the strongest ordering primitive available within ISO C++.
However:
- It operates strictly within the C++ abstract machine.
- It cannot strengthen ordering inside Windows runtime or kernel-level primitives.
- It cannot eliminate hardware-level propagation delays.
As a result:
- The implementation is algorithmically DRF.
- Undefined behavior (UB) risks have been eliminated at the ISO C++ level.
- All inter-thread interactions respect the C++ memory model.
Nevertheless, empirical behavior on certain Windows configurations (MinGW toolchains + MSVCRT/UCRT) may still exhibit rare instability under extreme contention.
This does not indicate undefined behavior in flat_pool,
but rather reflects environmental memory-model characteristics
that do not provide POSIX-level closure.
In summary:
- POSIX platforms: strong practical ordering, no additional fences required.
- Windows: language-level strengthening applied.
- Full global ordering equivalence is not guaranteed in practice.
- The design remains DRF and standards-compliant, but runtime behavior may not fully match theoretical expectations under stress.
Do not use flat_pool if:
- object destruction must occur immediately
- object identity depends on address stability
- object holds thread-affine or RAII-critical resources
- mutation-after-acquire semantics are required
Use pointer_pool / observe_pool instead.
flat_poolis a concurrent, key-addressed, GC-like object table with contiguous storage.
- Key defines identity
- Value is constructed once
- Handles behave like
shared_ptr - Destruction is deferred
- Slot reuse is preferred over churn
- Concurrency is pool-controlled and deterministic
This makes flat_pool ideal for high-frequency deduplication, cache-friendly layouts, and platform-stable
concurrency behavior.