System Class and Factory: A Simple Example of Memory Allocation Control
May 19, 2026 · Luciano Muratore
System Class and Factory: A Simple Example of Memory Allocation Control
The Question
When you create an object in C++, two things happen at the same time: the object is constructed, and memory is allocated for it. In most code these two concerns live in the same place — the code that needs an object creates it, allocates its memory, and cleans it up when done.
The question this article explores is simple: what happens if you separate those two concerns?
The Two Approaches
Without a System Class — Direct Allocation
The worker allocates its own scratch buffer on every call:
class WorkerA final : public IWorker {
public:
void run(char* buffer, std::size_t size) override {
char* scratch = new char[size]; // ← ALLOC every run
std::memcpy(scratch, buffer, size);
for (std::size_t i = 0; i < size; ++i)
buffer[i] = scratch[i] + 1;
delete[] scratch; // ← FREE every run
}
};
The object is responsible for itself. Every time it runs, it decides how much memory it needs, acquires it, uses it, and destroys it. Creation and use are fused in the same place.
With a System Class — Pool Allocation
The worker receives its scratch buffer at construction. It never allocates:
class WorkerB final : public IWorker {
public:
WorkerB(char* scratch, std::size_t size)
: mScratch(scratch), mSize(size) {} // scratch came from pool
void run(char* buffer, std::size_t size) override {
std::size_t n = std::min(size, mSize);
std::memcpy(mScratch, buffer, n);
for (std::size_t i = 0; i < n; ++i)
buffer[i] = mScratch[i] + 1;
// ← NO allocation
}
private:
char* mScratch; // points into pool — not owned by this object
std::size_t mSize;
};
The object only knows how to do its work. It has no idea where its memory came from. Creation and use are dissociated.
The System Class
System is the coordinator. It owns both the MemoryPool and the factory registry. It is not a singleton and not a global variable — it is a plain object created in main() and passed by pointer to whoever needs it:
class System {
public:
explicit System(std::size_t slabSize = 256, std::size_t numSlabs = 8)
: mPool(std::make_unique<MemoryPool>(slabSize, numSlabs))
{}
void registerFactory(std::unique_ptr<IWorkerFactory> f) {
mFactories[f->workerName()] = std::move(f);
}
// Allocation happens HERE — once, when the worker is added
void addWorker(const std::string& name) {
auto& f = mFactories.at(name);
mWorkers.push_back(f->create(*mPool)); // factory uses system's pool
}
// Zero allocation during this call — ever
void runAll(char* buffer, std::size_t size) {
for (auto& w : mWorkers)
w->run(buffer, size);
}
private:
std::unique_ptr<MemoryPool> mPool;
std::unordered_map<std::string,
std::unique_ptr<IWorkerFactory>> mFactories;
std::vector<std::unique_ptr<IWorker>> mWorkers;
};
The factory acquires pool memory and passes it into the worker at construction. After that, runAll() touches zero heap — regardless of how many times it is called.
Measuring It
Both approaches are overriding global new and delete with atomic counters and measuring how many heap operations happen during 10 steady-state runs:
// Counter reset before steady-state measurement
Counter::reset();
for (int i = 0; i < RUNS; ++i)
worker.run(buffer, BUF_SIZE); // Version A
Counter::report("steady-state");
// ─────────────────────────────────────
Counter::reset();
for (int i = 0; i < RUNS; ++i)
system.runAll(buffer, BUF_SIZE); // Version B
Counter::report("steady-state");
Results
WITHOUT System Class (direct allocation)
[steady-state, 10 runs] new() calls: 11 delete() calls: 10
WITH System Class (pool allocation)
[setup phase (addWorker)] new() calls: 3 delete() calls: 0
[steady-state, 10 runs] new() calls: 0 delete() calls: 0
| Phase | Without System Class | With System Class |
|---|---|---|
| Setup | — | 3 allocations (expected) |
| Steady-state (10 runs) | 11 allocations | 0 allocations |
The difference is not optimisation — it is architecture. The System Class moves all allocation decisions to setup time. By the time runAll() is called, every memory decision has already been made.
The Pattern in One Sentence
Systemcoordinates when the factory creates objects and where the pool puts them. After setup, nothing is created or allocated again.
How to Build and Run It Yourself
The full example is a single self-contained .cpp file — no audio, no Qt, no external libraries. You only need a C++20 compiler and CMake.
Project structure:
SimpleExample/
SimpleExample.cpp
CMakeLists.txt
CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(SimpleExample LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(SimpleExample SimpleExample.cpp)
if(MSVC)
target_compile_options(SimpleExample PRIVATE /EHsc)
endif()
Build on Windows (Visual Studio 2022):
mkdir build && cd build
cmake ..
cmake --build . --config Release
.\Release\SimpleExample.exe
Build on Linux / macOS:
mkdir build && cd build
cmake ..
cmake --build .
./SimpleExample
Source Code
For the full benchmark comparing this pattern against a real audio architecture across four memory concepts — timing unpredictability, memory churn, fragmentation, and concurrency safety — see the full article:
- How Architecture Influences Memory Management https://absolute-azimuth.pages.dev/posts/cpp/memory-management/memory_architecture/