Search Knowledge

© 2026 LIBREUNI PROJECT

Modern C++ Programming / Tools and Practices

Memory Management Deep Dive

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

  1. Prefer stack allocation: Faster and automatic cleanup
  2. Use smart pointers: unique_ptr by default, shared_ptr when needed
  3. Profile memory usage: Identify allocation hotspots
  4. Consider custom allocators: For performance-critical code
  5. Enable sanitizers in development: Catch bugs early
  6. Align data properly: For SIMD and cache efficiency
  7. Use pmr for runtime flexibility: When allocator type isn’t fixed
  8. 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...