Memory Management and Allocators
Overview
Memory management is crucial for performance and correctness in C++. This chapter covers allocators, polymorphic memory resources (PMR), memory alignment, and custom memory management strategies.
┌───────────────────────────────────────────────────────┐
│ MEMORY MANAGEMENT COMPONENTS │
├───────────────────────────────────────────────────────┤
│ │
│ ALLOCATORS │ PMR (C++17) │ ALIGNMENT │
│ ────────── │ ─────────── │ ───────── │
│ • std::alloc │ • pmr::memory │ • alignas │
│ • Custom │ • pool_resource │ • alignof │
│ • Stateful │ • monotonic │ • aligned_alloc │
│ │ • synchronized │ │
│ │
│ PLACEMENT NEW │ MEMORY POOLS │ DEBUGGING │
│ ───────────── │ ──────────── │ ───────── │
│ • new (ptr) │ • Object pools │ • Sanitizers │
│ • Manual ctor │ • Arena │ • Valgrind │
│ • Alignment │ • Stack alloc │ • Tools │
│ │
└───────────────────────────────────────────────────────┘
Standard Allocators
std::allocator
#include <memory>
#include <iostream>
int main() {
std::allocator<int> alloc;
// Allocate memory for 10 ints (uninitialized)
int* p = alloc.allocate(10);
// Construct objects
for (int i = 0; i < 10; ++i) {
std::allocator_traits<std::allocator<int>>::construct(alloc, &p[i], i);
}
// Use objects
for (int i = 0; i < 10; ++i) {
std::cout << p[i] << ' ';
}
std::cout << '\n';
// Destroy objects
for (int i = 0; i < 10; ++i) {
std::allocator_traits<std::allocator<int>>::destroy(alloc, &p[i]);
}
// Deallocate memory
alloc.deallocate(p, 10);
return 0;
}
Allocator Phases
Memory Management Phases:
┌────────────────────────────────────────────────────────┐
│ │
│ 1. Allocation: allocate() → raw memory │
│ ┌──────────────┐ │
│ │ [???][???] │ (uninitialized) │
│ └──────────────┘ │
│ │
│ 2. Construction: construct() → initialized objects │
│ ┌──────────────┐ │
│ │ [10] [20] │ (valid objects) │
│ └──────────────┘ │
│ │
│ 3. Use: Access objects │
│ │
│ 4. Destruction: destroy() → call destructors │
│ ┌──────────────┐ │
│ │ [???][???] │ (memory still there)│
│ └──────────────┘ │
│ │
│ 5. Deallocation: deallocate() → return memory │
│ │
└────────────────────────────────────────────────────────┘
Custom Allocator
#include <memory>
#include <cstdlib>
#include <iostream>
template<typename T>
class LoggingAllocator {
public:
using value_type = T;
LoggingAllocator() = default;
template<typename U>
LoggingAllocator(const LoggingAllocator<U>&) {}
T* allocate(std::size_t n) {
std::cout << "Allocating " << n << " objects of size "
<< sizeof(T) << '\n';
if (n > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
throw std::bad_alloc();
}
void* p = std::malloc(n * sizeof(T));
if (!p) throw std::bad_alloc();
return static_cast<T*>(p);
}
void deallocate(T* p, std::size_t n) {
std::cout << "Deallocating " << n << " objects\n";
std::free(p);
}
};
template<typename T, typename U>
bool operator==(const LoggingAllocator<T>&, const LoggingAllocator<U>&) {
return true;
}
template<typename T, typename U>
bool operator!=(const LoggingAllocator<T>&, const LoggingAllocator<U>&) {
return false;
}
int main() {
std::vector<int, LoggingAllocator<int>> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
return 0;
}
Polymorphic Memory Resources (PMR - C++17)
Memory Resources
#include <memory_resource>
#include <vector>
#include <iostream>
int main() {
// Default resource (usually uses new/delete)
std::pmr::memory_resource* default_resource =
std::pmr::get_default_resource();
// Monotonic buffer resource (fast, no deallocation)
char buffer[1024];
std::pmr::monotonic_buffer_resource pool(buffer, sizeof(buffer));
// Use with pmr containers
std::pmr::vector<int> vec(&pool);
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
}
// Memory comes from buffer, not heap!
return 0;
} // All allocations from pool released at once
PMR Memory Resources
┌────────────────────────────────────────────────────────┐
│ PMR Memory Resource Types │
├────────────────────────────────────────────────────────┤
│ │
│ new_delete_resource: │
│ • Uses global new/delete │
│ • Default resource │
│ │
│ monotonic_buffer_resource: │
│ • Fast allocation │
│ • No per-object deallocation │
│ • Releases all at once │
│ • Good for: Temporary allocations │
│ │
│ synchronized_pool_resource: │
│ • Thread-safe pooling │
│ • Reduces fragmentation │
│ • Good for: Multi-threaded apps │
│ │
│ unsynchronized_pool_resource: │
│ • Single-threaded pooling │
│ • Faster than synchronized │
│ • Good for: Single-threaded perf │
│ │
└────────────────────────────────────────────────────────┘
PMR Container Performance
#include <memory_resource>
#include <vector>
#include <chrono>
void benchmark_pmr() {
using namespace std::chrono;
// Standard allocator
{
auto start = steady_clock::now();
std::vector<int> vec;
for (int i = 0; i < 100000; ++i) {
vec.push_back(i);
}
auto elapsed = steady_clock::now() - start;
std::cout << "Standard: "
<< duration_cast<milliseconds>(elapsed).count() << "ms\n";
}
// PMR with monotonic buffer
{
char buffer[1000000];
std::pmr::monotonic_buffer_resource pool(buffer, sizeof(buffer));
auto start = steady_clock::now();
std::pmr::vector<int> vec(&pool);
for (int i = 0; i < 100000; ++i) {
vec.push_back(i);
}
auto elapsed = steady_clock::now() - start;
std::cout << "PMR: "
<< duration_cast<milliseconds>(elapsed).count() << "ms\n";
}
}
Alignment
Memory Alignment Basics
#include <iostream>
#include <cstddef>
int main() {
// Query alignment
std::cout << "int alignment: " << alignof(int) << '\n';
std::cout << "double alignment: " << alignof(double) << '\n';
std::cout << "max_align_t: " << alignof(std::max_align_t) << '\n';
// Aligned struct
struct alignas(16) AlignedStruct {
int x;
double y;
};
std::cout << "AlignedStruct alignment: "
<< alignof(AlignedStruct) << '\n'; // 16
std::cout << "AlignedStruct size: "
<< sizeof(AlignedStruct) << '\n'; // Padded to 16
// Aligned variable
alignas(32) int aligned_var;
return 0;
}
Aligned Allocation (C++17)
#include <cstdlib>
#include <new>
int main() {
// C++17: aligned_alloc
void* p1 = std::aligned_alloc(64, 256); // 256 bytes, 64-byte aligned
if (p1) {
// Use aligned memory
std::free(p1);
}
// Over-aligned new (C++17)
struct alignas(64) OverAligned {
char data[64];
};
OverAligned* p2 = new OverAligned; // Automatically aligned to 64 bytes
delete p2;
return 0;
}
Visual: Memory Alignment
Unaligned:
Address: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[int ][int ][int ][int ]
0 4 8 12
Aligned to 8 bytes:
Address: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[int ][pad][int ][pad]
0 4 8 12
Padding ensures aligned access (faster on most architectures)
Placement New
Manual Object Construction
#include <new>
#include <iostream>
class Widget {
int value_;
public:
Widget(int v) : value_(v) {
std::cout << "Widget constructed with " << v << '\n';
}
~Widget() {
std::cout << "Widget destructed\n";
}
int get() const { return value_; }
};
int main() {
// Allocate raw memory
alignas(Widget) char buffer[sizeof(Widget)];
// Construct object in memory (placement new)
Widget* w = new (buffer) Widget(42);
// Use object
std::cout << "Value: " << w->get() << '\n';
// Explicitly destroy (required!)
w->~Widget();
// Memory (buffer) still exists
// No delete needed (buffer is on stack)
return 0;
}
Array Placement New
#include <new>
int main() {
const size_t N = 10;
alignas(int) char buffer[sizeof(int) * N];
// Construct array in buffer
int* arr = new (buffer) int[N];
for (size_t i = 0; i < N; ++i) {
arr[i] = i;
}
// Destroy array (explicit destructor call for each element)
for (size_t i = 0; i < N; ++i) {
arr[i].~int();
}
// For trivial types, destructor call can be omitted
return 0;
}
Memory Pools
Simple Object Pool
#include <vector>
#include <memory>
template<typename T>
class ObjectPool {
std::vector<std::unique_ptr<T>> pool_;
std::vector<T*> available_;
public:
ObjectPool(size_t initial_size = 10) {
pool_.reserve(initial_size);
available_.reserve(initial_size);
for (size_t i = 0; i < initial_size; ++i) {
auto obj = std::make_unique<T>();
available_.push_back(obj.get());
pool_.push_back(std::move(obj));
}
}
T* acquire() {
if (available_.empty()) {
// Pool exhausted, create new object
auto obj = std::make_unique<T>();
T* ptr = obj.get();
pool_.push_back(std::move(obj));
return ptr;
}
T* obj = available_.back();
available_.pop_back();
return obj;
}
void release(T* obj) {
// Reset object to default state if needed
*obj = T();
available_.push_back(obj);
}
size_t capacity() const { return pool_.size(); }
size_t available() const { return available_.size(); }
};
// Usage
int main() {
ObjectPool<std::string> pool(5);
std::string* s1 = pool.acquire();
*s1 = "Hello";
std::string* s2 = pool.acquire();
*s2 = "World";
pool.release(s1);
pool.release(s2);
return 0;
}
Visual: Object Pool
Object Pool:
┌────────────────────────────────────────────────────┐
│ Pool Storage: [obj1][obj2][obj3][obj4][obj5] │
│ ▲ ▲ │
│ │ │ │
│ Available List: [ptr1][ptr2] │
│ │
│ In Use: [obj3][obj4][obj5] │
└────────────────────────────────────────────────────┘
acquire() → Pops from available list
release() → Pushes back to available list
Benefits:
• Reduced allocation overhead
• Better cache locality
• Predictable performance
Stack Allocators
Fixed-Size Stack Allocator
#include <array>
#include <cstddef>
template<typename T, size_t N>
class StackAllocator {
alignas(T) std::array<std::byte, sizeof(T) * N> buffer_;
std::byte* current_;
public:
using value_type = T;
StackAllocator() : current_(buffer_.data()) {}
T* allocate(std::size_t n) {
if (current_ + n * sizeof(T) > buffer_.data() + buffer_.size()) {
throw std::bad_alloc();
}
T* result = reinterpret_cast<T*>(current_);
current_ += n * sizeof(T);
return result;
}
void deallocate(T* p, std::size_t n) {
// Stack allocator: no actual deallocation
// Memory reused when allocator destroyed
}
template<typename U>
struct rebind {
using other = StackAllocator<U, N>;
};
};
int main() {
std::vector<int, StackAllocator<int, 100>> vec;
vec.push_back(1);
vec.push_back(2);
// Uses stack memory, not heap!
return 0;
}
PMR Detailed Examples
Monotonic Buffer Resource
#include <memory_resource>
#include <vector>
#include <iostream>
void example_monotonic() {
// Stack buffer
std::array<std::byte, 10000> buffer;
std::pmr::monotonic_buffer_resource pool{buffer.data(), buffer.size()};
// Containers use pool
std::pmr::vector<int> vec(&pool);
std::pmr::vector<std::pmr::string> strings(&pool);
for (int i = 0; i < 100; ++i) {
vec.push_back(i);
}
strings.push_back("Hello");
strings.push_back("World");
std::cout << "Used stack memory, no heap allocations!\n";
// All memory released when pool destroyed
}
Pool Resources
#include <memory_resource>
#include <vector>
#include <thread>
void example_pool() {
// Unsynchronized pool (single-threaded)
std::pmr::unsynchronized_pool_resource pool;
std::pmr::vector<int> vec(&pool);
for (int i = 0; i < 1000; ++i) {
vec.push_back(i);
}
// Synchronized pool (thread-safe)
std::pmr::synchronized_pool_resource sync_pool;
auto worker = [&sync_pool]() {
std::pmr::vector<int> local_vec(&sync_pool);
for (int i = 0; i < 100; ++i) {
local_vec.push_back(i);
}
};
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
}
Chaining Memory Resources
#include <memory_resource>
void example_chain() {
// Create hierarchy of resources
std::pmr::synchronized_pool_resource global_pool;
// Monotonic buffer backed by pool
std::array<std::byte, 1000> buffer;
std::pmr::monotonic_buffer_resource local_buffer{
buffer.data(), buffer.size(), &global_pool
};
// Uses buffer first, then falls back to global_pool
std::pmr::vector<int> vec(&local_buffer);
// First allocations from buffer
// If exhausted, allocations from global_pool
}
Memory Debugging
Detecting Memory Leaks
// Compile with sanitizers:
// g++ -fsanitize=address program.cpp
void leak_example() {
int* p = new int(42);
// Forgot to delete - AddressSanitizer detects this
}
// Use smart pointers to avoid leaks
void safe_example() {
auto p = std::make_unique<int>(42);
// Automatically deleted
}
Custom Memory Tracking
#include <cstdlib>
#include <iostream>
static size_t total_allocated = 0;
static size_t total_deallocated = 0;
void* operator new(std::size_t size) {
total_allocated += size;
std::cout << "Allocating " << size << " bytes\n";
void* p = std::malloc(size);
if (!p) throw std::bad_alloc();
return p;
}
void operator delete(void* p) noexcept {
std::cout << "Deallocating\n";
total_deallocated++;
std::free(p);
}
void report_memory() {
std::cout << "Total allocated: " << total_allocated << '\n';
std::cout << "Total deallocations: " << total_deallocated << '\n';
}
Performance Tips
1. Reserve Capacity
// BAD: Multiple reallocations
std::vector<int> vec;
for (int i = 0; i < 10000; ++i) {
vec.push_back(i); // May reallocate many times
}
// GOOD: Single allocation
std::vector<int> vec;
vec.reserve(10000);
for (int i = 0; i < 10000; ++i) {
vec.push_back(i); // No reallocation
}
2. Use emplace for In-Place Construction
std::vector<std::string> vec;
// BAD: Creates temporary
vec.push_back(std::string("hello"));
// GOOD: Constructs in-place
vec.emplace_back("hello");
3. Small String Optimization (SSO)
// Most std::string implementations use SSO
std::string small = "short"; // No heap allocation
std::string large = "This is a very long string that exceeds SSO buffer";
// Heap allocation
4. Object Pools for Frequent Allocations
// Use object pool when:
// • Creating/destroying many objects
// • Objects of uniform size
// • Performance critical
ObjectPool<GameObject> pool(100);
Best Practices
1. Prefer Smart Pointers
// GOOD: Automatic cleanup
auto p = std::make_unique<Widget>();
// BAD: Manual management
Widget* p = new Widget();
delete p;
2. Use PMR for Performance-Critical Code
// Hot path with many allocations
std::array<std::byte, 100000> buffer;
std::pmr::monotonic_buffer_resource pool{buffer.data(), buffer.size()};
std::pmr::vector<Data> vec(&pool);
// Fast allocations from stack buffer
3. Align Data Appropriately
// For SIMD operations
alignas(32) float data[8]; // AVX alignment
// Cache line alignment (avoid false sharing)
alignas(64) std::atomic<int> counter;
4. Profile Before Optimizing
// Don't assume - measure!
// Use profilers: Valgrind, perf, custom timers
Common Pitfalls
1. Forgetting to Destroy Placement New Objects
// BAD
alignas(Widget) char buffer[sizeof(Widget)];
Widget* w = new (buffer) Widget();
// Forgot w->~Widget(); - leak!
// GOOD
w->~Widget();
2. Allocator Mismatch
std::allocator<int> alloc;
int* p = alloc.allocate(10);
delete p; // BAD: Must use alloc.deallocate!
alloc.deallocate(p, 10); // GOOD
3. Using After Free
int* p = new int(42);
delete p;
int x = *p; // UB: Use after free!
Complete Practical Example: Game Object Memory Manager
Here’s a comprehensive example integrating allocators, PMR, object pools, and custom memory management:
#include <iostream>
#include <memory>
#include <memory_resource>
#include <vector>
#include <list>
#include <string>
#include <chrono>
#include <array>
// Game object base class
struct GameObject {
int id;
float x, y, z;
std::string name;
GameObject(int i, std::string n)
: id(i), x(0), y(0), z(0), name(std::move(n)) {}
virtual ~GameObject() = default;
virtual void update() = 0;
};
struct Enemy : GameObject {
int health;
Enemy(int id, std::string name, int hp)
: GameObject(id, std::move(name)), health(hp) {}
void update() override {
// Enemy AI update
}
};
struct Projectile : GameObject {
float velocity_x, velocity_y;
Projectile(int id, float vx, float vy)
: GameObject(id, "projectile")
, velocity_x(vx), velocity_y(vy) {}
void update() override {
x += velocity_x;
y += velocity_y;
}
};
// 1. Custom allocator with logging
template<typename T>
class LoggingAllocator {
private:
static inline size_t allocations_ = 0;
static inline size_t deallocations_ = 0;
static inline size_t total_bytes_ = 0;
public:
using value_type = T;
LoggingAllocator() = default;
template<typename U>
LoggingAllocator(const LoggingAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
allocations_++;
total_bytes_ += n * sizeof(T);
std::cout << " [ALLOC] " << n << " × " << sizeof(T)
<< " bytes = " << (n * sizeof(T)) << " bytes\n";
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) noexcept {
deallocations_++;
std::cout << " [DEALLOC] " << (n * sizeof(T)) << " bytes\n";
::operator delete(p);
}
static void print_stats() {
std::cout << "\n=== Allocator Statistics ===\n";
std::cout << "Allocations: " << allocations_ << "\n";
std::cout << "Deallocations: " << deallocations_ << "\n";
std::cout << "Total allocated: " << total_bytes_ << " bytes\n";
std::cout << "Leaks: " << (allocations_ - deallocations_) << "\n";
}
};
template<typename T, typename U>
bool operator==(const LoggingAllocator<T>&, const LoggingAllocator<U>&) { return true; }
template<typename T, typename U>
bool operator!=(const LoggingAllocator<T>&, const LoggingAllocator<U>&) { return false; }
// 2. Object pool for frequent allocations
template<typename T>
class ObjectPool {
private:
union Slot {
T object;
Slot* next;
Slot() {}
~Slot() {}
};
std::vector<std::unique_ptr<Slot[]>> blocks_;
Slot* free_list_ = nullptr;
size_t block_size_;
size_t active_objects_ = 0;
public:
explicit ObjectPool(size_t block_size = 32)
: block_size_(block_size) {
allocate_block();
}
~ObjectPool() {
// Objects should be returned before destruction
if (active_objects_ > 0) {
std::cerr << "Warning: " << active_objects_
<< " objects not returned to pool\n";
}
}
template<typename... Args>
T* acquire(Args&&... args) {
if (!free_list_) {
allocate_block();
}
Slot* slot = free_list_;
free_list_ = slot->next;
new (&slot->object) T(std::forward<Args>(args)...);
active_objects_++;
return &slot->object;
}
void release(T* obj) {
if (!obj) return;
obj->~T();
Slot* slot = reinterpret_cast<Slot*>(obj);
slot->next = free_list_;
free_list_ = slot;
active_objects_--;
}
size_t active_count() const { return active_objects_; }
size_t capacity() const { return blocks_.size() * block_size_; }
private:
void allocate_block() {
auto block = std::make_unique<Slot[]>(block_size_);
for (size_t i = 0; i < block_size_; ++i) {
block[i].next = (i == block_size_ - 1) ? free_list_ : &block[i + 1];
}
free_list_ = &block[0];
blocks_.push_back(std::move(block));
std::cout << " [POOL] Allocated block of " << block_size_
<< " slots (" << (block_size_ * sizeof(T)) << " bytes)\n";
}
};
// 3. PMR monotonic buffer allocator
class FrameAllocator {
private:
std::array<std::byte, 1024 * 1024> buffer_; // 1MB stack buffer
std::pmr::monotonic_buffer_resource pool_;
public:
FrameAllocator() : pool_(buffer_.data(), buffer_.size()) {}
std::pmr::memory_resource* get_resource() {
return &pool_;
}
void reset() {
pool_.release();
std::cout << " [FRAME] Reset frame allocator\n";
}
size_t bytes_allocated() const {
// Approximate (PMR doesn't expose this directly)
return buffer_.size();
}
};
// 4. Stack allocator for small, short-lived objects
template<typename T, size_t N>
class StackAllocator {
private:
alignas(T) std::array<std::byte, sizeof(T) * N> buffer_;
std::byte* current_;
size_t allocated_ = 0;
public:
using value_type = T;
StackAllocator() : current_(buffer_.data()) {}
T* allocate(std::size_t n) {
if (current_ + n * sizeof(T) > buffer_.data() + buffer_.size()) {
throw std::bad_alloc();
}
T* result = reinterpret_cast<T*>(current_);
current_ += n * sizeof(T);
allocated_ += n;
std::cout << " [STACK] Allocated " << n << " objects on stack\n";
return result;
}
void deallocate(T*, std::size_t) noexcept {
// Stack allocator doesn't deallocate individual objects
}
size_t allocated_count() const { return allocated_; }
template<typename U>
struct rebind {
using other = StackAllocator<U, N>;
};
};
// Game memory manager
class GameMemoryManager {
private:
ObjectPool<Projectile> projectile_pool_;
ObjectPool<Enemy> enemy_pool_;
FrameAllocator frame_allocator_;
public:
GameMemoryManager()
: projectile_pool_(64) // Pool of 64 projectiles
, enemy_pool_(32) {} // Pool of 32 enemies
// Allocate projectile from pool
Projectile* create_projectile(int id, float vx, float vy) {
return projectile_pool_.acquire(id, vx, vy);
}
void destroy_projectile(Projectile* proj) {
projectile_pool_.release(proj);
}
// Allocate enemy from pool
Enemy* create_enemy(int id, const std::string& name, int health) {
return enemy_pool_.acquire(id, name, health);
}
void destroy_enemy(Enemy* enemy) {
enemy_pool_.release(enemy);
}
// Get frame allocator for temporary data
std::pmr::memory_resource* get_frame_resource() {
return frame_allocator_.get_resource();
}
void end_frame() {
frame_allocator_.reset();
}
void print_stats() const {
std::cout << "\n=== Game Memory Stats ===\n";
std::cout << "Projectiles: " << projectile_pool_.active_count()
<< "/" << projectile_pool_.capacity() << "\n";
std::cout << "Enemies: " << enemy_pool_.active_count()
<< "/" << enemy_pool_.capacity() << "\n";
}
};
// Demo functions
void demo_custom_allocator() {
std::cout << "\n--- Custom Allocator ---\n";
std::vector<int, LoggingAllocator<int>> vec;
vec.reserve(10);
for (int i = 0; i < 5; ++i) {
vec.push_back(i);
}
LoggingAllocator<int>::print_stats();
}
void demo_object_pool() {
std::cout << "\n--- Object Pool ---\n";
ObjectPool<Projectile> pool(8);
std::vector<Projectile*> projectiles;
// Spawn projectiles
for (int i = 0; i < 12; ++i) {
auto* proj = pool.acquire(i, 1.0f, 0.5f);
projectiles.push_back(proj);
std::cout << " Spawned projectile " << i << "\n";
}
std::cout << "\nActive: " << pool.active_count() << "\n";
// Destroy some
for (size_t i = 0; i < 6; ++i) {
pool.release(projectiles[i]);
std::cout << " Destroyed projectile " << i << "\n";
}
std::cout << "Active after cleanup: " << pool.active_count() << "\n";
// Cleanup remaining
for (size_t i = 6; i < projectiles.size(); ++i) {
pool.release(projectiles[i]);
}
}
void demo_pmr_allocator() {
std::cout << "\n--- PMR Allocator ---\n";
std::array<std::byte, 4096> buffer;
std::pmr::monotonic_buffer_resource pool{buffer.data(), buffer.size()};
std::pmr::vector<int> vec(&pool);
std::pmr::string str(&pool);
std::cout << " Using stack buffer for PMR allocations\n";
for (int i = 0; i < 100; ++i) {
vec.push_back(i);
}
str = "Hello from PMR!";
std::cout << " Vector size: " << vec.size() << "\n";
std::cout << " String: " << str << "\n";
std::cout << " All allocated from stack buffer!\n";
}
void demo_game_memory() {
std::cout << "\n--- Game Memory Manager ---\n";
GameMemoryManager memory_mgr;
// Create some enemies
std::vector<Enemy*> enemies;
for (int i = 0; i < 5; ++i) {
enemies.push_back(memory_mgr.create_enemy(
i, "Enemy" + std::to_string(i), 100));
}
// Create projectiles
std::vector<Projectile*> projectiles;
for (int i = 0; i < 10; ++i) {
projectiles.push_back(memory_mgr.create_projectile(
i, 1.0f, 0.5f));
}
memory_mgr.print_stats();
// Frame-local allocations
auto* frame_resource = memory_mgr.get_frame_resource();
std::pmr::vector<int> temp_data(frame_resource);
for (int i = 0; i < 100; ++i) {
temp_data.push_back(i);
}
std::cout << "\nFrame temp data size: " << temp_data.size() << "\n";
// End frame (resets frame allocator)
memory_mgr.end_frame();
// Cleanup
for (auto* enemy : enemies) {
memory_mgr.destroy_enemy(enemy);
}
for (auto* proj : projectiles) {
memory_mgr.destroy_projectile(proj);
}
memory_mgr.print_stats();
}
void demo_stack_allocator() {
std::cout << "\n--- Stack Allocator ---\n";
std::vector<int, StackAllocator<int, 100>> vec;
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
}
std::cout << " Vector uses stack memory only!\n";
}
int main() {
std::cout << "=== Memory Management Demo ===\n";
// 1. Custom allocator with logging
demo_custom_allocator();
// 2. Object pool
demo_object_pool();
// 3. PMR allocator
demo_pmr_allocator();
// 4. Stack allocator
demo_stack_allocator();
// 5. Game memory manager (integrates everything)
demo_game_memory();
std::cout << "\n=== Demo Complete ===\n";
return 0;
}
Concepts Demonstrated:
- Custom allocators: Logging and tracking allocations
- Object pools: Reusing memory for frequent allocations
- PMR (polymorphic memory resources): Stack-based allocation
- Monotonic buffer resource: Fast frame-local allocations
- Stack allocators: Fixed-size stack memory
- Placement new: Manual construction in memory
- RAII: Automatic cleanup of resources
- Memory statistics: Tracking allocations and leaks
- Game-style memory management: Pools + frame allocators
- Alignment: Proper memory alignment
- Move semantics: Efficient resource transfer
This example shows production-ready memory management for performance-critical applications!
Next Steps
- Next: Regular Expressions →
- Previous: ← Time and Chrono
Part 19 of 22 - Memory Management and Allocators