Advanced Memory Management
C++ provides extensive control over memory management. Understanding allocators, alignment, and memory debugging tools is crucial for high-performance applications.
Memory Allocation Basics
// Stack allocation
int x = 42; // Automatic storage duration
char buffer[1024]; // Fixed size, fast
// Heap allocation
int* ptr = new int(42); // Manual lifetime
delete ptr;
auto vec = std::make_unique<std::vector<int>>(); // Automatic cleanup
Alignment
alignof and alignas
struct alignas(16) Aligned {
double x, y;
};
static_assert(alignof(Aligned) == 16);
static_assert(sizeof(Aligned) == 16); // Padded to alignment
// Cache line alignment
struct alignas(64) CacheAligned {
int data;
// Padded to 64 bytes
};
std::aligned_storage
#include <type_traits>
alignas(16) std::byte buffer[sizeof(MyClass)];
// Construct object in aligned buffer
MyClass* obj = new (buffer) MyClass(args);
// Destroy when done
obj->~MyClass();
Dynamic Aligned Allocation
// C++17: aligned new
void* ptr = ::operator new(size, std::align_val_t{64});
::operator delete(ptr, std::align_val_t{64});
// Aligned unique_ptr
template<typename T, std::size_t Alignment>
struct AlignedDeleter {
void operator()(T* ptr) {
ptr->~T();
::operator delete(ptr, std::align_val_t{Alignment});
}
};
template<typename T, std::size_t Alignment, typename... Args>
auto make_aligned_unique(Args&&... args) {
void* mem = ::operator new(sizeof(T), std::align_val_t{Alignment});
T* ptr = new (mem) T(std::forward<Args>(args)...);
return std::unique_ptr<T, AlignedDeleter<T, Alignment>>(ptr);
}
Custom Allocators
Basic Allocator Interface
template<typename T>
class MyAllocator {
public:
using value_type = T;
MyAllocator() = default;
template<typename U>
MyAllocator(const MyAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
if (n > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
throw std::bad_array_new_length();
}
void* ptr = ::operator new(n * sizeof(T));
return static_cast<T*>(ptr);
}
void deallocate(T* ptr, std::size_t) noexcept {
::operator delete(ptr);
}
};
template<typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) { return true; }
template<typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) { return false; }
// Usage
std::vector<int, MyAllocator<int>> vec;
Stack Allocator
Allocate from stack buffer:
template<typename T, std::size_t N>
class StackAllocator {
alignas(T) std::byte buffer_[N * sizeof(T)];
std::byte* ptr_ = buffer_;
public:
using value_type = T;
T* allocate(std::size_t n) {
if (ptr_ + n * sizeof(T) > buffer_ + sizeof(buffer_)) {
throw std::bad_alloc();
}
T* result = reinterpret_cast<T*>(ptr_);
ptr_ += n * sizeof(T);
return result;
}
void deallocate(T*, std::size_t) noexcept {
// No-op for stack allocator
}
};
// Fast vector for small sizes
std::vector<int, StackAllocator<int, 100>> small_vec;
Memory Pools
Fixed-Size Pool
template<typename T, std::size_t BlockSize = 4096>
class MemoryPool {
union Node {
T value;
Node* next;
};
struct Block {
std::byte data[BlockSize];
Block* next;
};
Block* blocks_ = nullptr;
Node* free_list_ = nullptr;
public:
~MemoryPool() {
while (blocks_) {
Block* next = blocks_->next;
::operator delete(blocks_);
blocks_ = next;
}
}
T* allocate() {
if (!free_list_) {
allocate_block();
}
Node* node = free_list_;
free_list_ = node->next;
return &node->value;
}
void deallocate(T* ptr) {
Node* node = reinterpret_cast<Node*>(ptr);
node->next = free_list_;
free_list_ = node;
}
private:
void allocate_block() {
Block* block = static_cast<Block*>(::operator new(sizeof(Block)));
block->next = blocks_;
blocks_ = block;
const std::size_t node_count = BlockSize / sizeof(Node);
Node* nodes = reinterpret_cast<Node*>(block->data);
for (std::size_t i = 0; i < node_count; ++i) {
nodes[i].next = free_list_;
free_list_ = &nodes[i];
}
}
};
Polymorphic Memory Resources (C++17)
std::pmr provides runtime polymorphism for allocators:
#include <memory_resource>
// Monotonic allocator: fast, no deallocation
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> vec1(&pool);
std::pmr::string str(&pool);
// All allocations from pool
// Pool memory released when pool destroyed
Pool Options
// Unsynchronized pool: single-threaded
std::pmr::unsynchronized_pool_resource pool;
// Synchronized pool: thread-safe
std::pmr::synchronized_pool_resource thread_safe_pool;
// New/delete resource: default
std::pmr::new_delete_resource();
// Null resource: throws on allocation
std::pmr::null_memory_resource();
Custom pmr Resource
class LoggingResource : public std::pmr::memory_resource {
std::pmr::memory_resource* upstream_;
public:
LoggingResource(std::pmr::memory_resource* upstream)
: upstream_(upstream) {}
protected:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
std::cout << "Allocating " << bytes << " bytes\n";
return upstream_->allocate(bytes, alignment);
}
void do_deallocate(void* ptr, std::size_t bytes, std::size_t alignment) override {
std::cout << "Deallocating " << bytes << " bytes\n";
upstream_->deallocate(ptr, bytes, alignment);
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
};
Memory Debugging
Address Sanitizer (ASan)
Detects memory errors:
# Compile with ASan
g++ -fsanitize=address -g program.cpp -o program
# Run
./program
Detects:
- Use after free
- Heap buffer overflow
- Stack buffer overflow
- Memory leaks
- Use after return
Example:
int main() {
int* ptr = new int[10];
delete[] ptr;
ptr[0] = 42; // ASan detects: heap-use-after-free
}
Valgrind
# Check for memory leaks
valgrind --leak-check=full ./program
# Memory profiler
valgrind --tool=massif ./program
ms_print massif.out.*
Memory Sanitizer (MSan)
Detects uninitialized memory reads:
clang++ -fsanitize=memory -g program.cpp -o program
Smart Pointer Internals
unique_ptr Implementation
template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
T* ptr_ = nullptr;
[[no_unique_address]] Deleter deleter_;
public:
unique_ptr() = default;
explicit unique_ptr(T* ptr) : ptr_(ptr) {}
~unique_ptr() {
if (ptr_) deleter_(ptr_);
}
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr(unique_ptr&& other) noexcept
: ptr_(other.ptr_), deleter_(std::move(other.deleter_)) {
other.ptr_ = nullptr;
}
unique_ptr& operator=(unique_ptr&& other) noexcept {
reset(other.release());
deleter_ = std::move(other.deleter_);
return *this;
}
T* release() noexcept {
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void reset(T* ptr = nullptr) noexcept {
T* old_ptr = ptr_;
ptr_ = ptr;
if (old_ptr) deleter_(old_ptr);
}
T* get() const noexcept { return ptr_; }
T& operator*() const { return *ptr_; }
T* operator->() const noexcept { return ptr_; }
explicit operator bool() const noexcept { return ptr_ != nullptr; }
};
shared_ptr Reference Counting
// Simplified shared_ptr internals
template<typename T>
class shared_ptr {
T* ptr_ = nullptr;
struct ControlBlock {
std::atomic<long> ref_count{1};
std::atomic<long> weak_count{1};
virtual ~ControlBlock() = default;
virtual void destroy_object() = 0;
virtual void destroy_control_block() = 0;
};
ControlBlock* control_ = nullptr;
public:
shared_ptr(T* ptr) : ptr_(ptr) {
control_ = new ControlBlockImpl(ptr);
}
shared_ptr(const shared_ptr& other)
: ptr_(other.ptr_), control_(other.control_) {
if (control_) {
control_->ref_count.fetch_add(1, std::memory_order_relaxed);
}
}
~shared_ptr() {
if (control_ && control_->ref_count.fetch_sub(1) == 1) {
control_->destroy_object();
if (control_->weak_count.fetch_sub(1) == 1) {
control_->destroy_control_block();
}
}
}
};
Memory Order and Synchronization
#include <atomic>
std::atomic<int> counter{0};
// Relaxed: no synchronization
counter.fetch_add(1, std::memory_order_relaxed);
// Acquire-Release: synchronize with matching operations
counter.store(1, std::memory_order_release);
int value = counter.load(std::memory_order_acquire);
// Sequential consistency: total order (default)
counter.fetch_add(1, std::memory_order_seq_cst);
RAII Patterns
Scope Guard
template<typename F>
class ScopeGuard {
F cleanup_;
bool active_ = true;
public:
explicit ScopeGuard(F cleanup) : cleanup_(std::move(cleanup)) {}
~ScopeGuard() {
if (active_) cleanup_();
}
void dismiss() { active_ = false; }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
};
template<typename F>
auto make_scope_guard(F cleanup) {
return ScopeGuard<F>(std::move(cleanup));
}
// Usage
void process_file(const char* filename) {
FILE* file = fopen(filename, "r");
auto guard = make_scope_guard([=] { if (file) fclose(file); });
// Use file...
// Automatically closed on scope exit
}
Best Practices
- Prefer stack allocation: Faster and automatic cleanup
- Use smart pointers: unique_ptr by default, shared_ptr when needed
- Profile memory usage: Identify allocation hotspots
- Consider custom allocators: For performance-critical code
- Enable sanitizers in development: Catch bugs early
- Align data properly: For SIMD and cache efficiency
- Use pmr for runtime flexibility: When allocator type isn’t fixed
- Avoid memory leaks: RAII and smart pointers
Conceptual Check
What does alignof return?
Interactive Lab
Complete the Code
// Force struct to be aligned to 64-byte boundary (cache line) struct (64) CacheAligned { int data[16]; };
Runtime Environment
Interactive Lab
1#include <iostream>
2#include <memory_resource>
3#include <vector>
4
5int main() {
6 // Stack buffer for allocations
7 std::byte buffer[1024];
8 std::pmr::monotonic_buffer_resource pool{buffer, sizeof(buffer)};
9
10 // Vector using custom allocator
11 std::pmr::vector<int> vec(&pool);
12
13 for (int i = 0; i < 10; ++i) {
14 vec.push_back(i);
15 }
16
17 std::cout << "Vector size: " << vec.size() << '\n';
18 std::cout << "Allocated from stack buffer\n";
19
20 // Memory automatically released when pool destroyed
21 return 0;
22}
System Console
Waiting for signal...