Custom Memory Management and Access Tracking in C++

Tracking Member Variable Access Count

To monitor how often a specific member variable is accessed, one approach uses the mutable keyword:

#include <iostream>

using namespace std;

class Counter {
private:
    int value;
    mutable int access_count;

public:
    Counter(int v) : value(v), access_count(0) {}

    void setValue(int v) {
        value = v;
        ++access_count;
    }

    int getValue() const {
        ++access_count;
        return value;
    }

    int getAccessCount() const {
        return access_count;
    }
};

int main() {
    const Counter obj(42);
    cout << obj.getValue() << endl;        // 42
    cout << obj.getAccessCount() << endl;  // 1
    return 0;
}

The mutable keyword allows modification of a member even within const member functiosn. However, it breaks logical const-correctness by permitting internal state changes in otherwise read-only objects. While useful for instrumentation like access counting, its use is discouraged in production code due to potential violations of object immutability guarantees.

An alternative avoids mutable by storing the counter on the heap:

#include <iostream>

using namespace std;

class SafeCounter {
private:
    int value;
    int* const counter_ptr;

public:
    SafeCounter(int v = 0) : value(v), counter_ptr(new int(0)) {}

    ~SafeCounter() {
        delete counter_ptr;
    }

    void setValue(int v) {
        value = v;
        ++(*counter_ptr);
    }

    int getValue() const {
        ++(*counter_ptr);  // Modifies pointed-to data, not member itself
        return value;
    }

    int getAccessCount() const {
        return *counter_ptr;
    }
};

int main() {
    const SafeCounter obj(10);
    cout << obj.getValue() << endl;        // 10
    cout << obj.getAccessCount() << endl;  // 1
    return 0;
}

Here, the const correctness is preserved because the pointer itself (counter_ptr) is not modified—only the data it points to changes. This maintains the illusion of immutability from the object’s interface perspective.

Overloading new and delete

In C++, new and delete are operators that can be overloaded to customize memory allocation. Their default behavior involves:

  • new: Allocates raw memory (typically from the heap) and calls the constructor.
  • delete: Calls the destructor and deallocates the memory.

Operators can be overloaded globally (affects all types) or per-class (preferred). Class-specific overloads must be static.

Example: Pre-allocating a fixed pool of objects in static storage:

#include <iostream>

using namespace std;

class PooledObject {
private:
    static const unsigned int MAX_INSTANCES = 4;
    static char buffer[MAX_INSTANCES * sizeof(PooledObject)];
    static char usage_map[MAX_INSTANCES];
    int data;

public:
    void* operator new(size_t size) {
        for (int i = 0; i < MAX_INSTANCES; ++i) {
            if (!usage_map[i]) {
                usage_map[i] = 1;
                void* addr = buffer + i * sizeof(PooledObject);
                cout << "Allocated at: " << addr << endl;
                return addr;
            }
        }
        return nullptr;  // Pool exhausted
    }

    void operator delete(void* ptr) {
        if (ptr) {
            auto* mem = static_cast<char*>(ptr);
            ptrdiff_t offset = mem - buffer;
            if (offset % sizeof(PooledObject) == 0) {
                int idx = offset / sizeof(PooledObject);
                if (idx >= 0 && idx < MAX_INSTANCES) {
                    usage_map[idx] = 0;
                    cout << "Deallocated: " << ptr << endl;
                }
            }
        }
    }
};

char PooledObject::buffer[] = {0};
char PooledObject::usage_map[] = {0};

int main() {
    PooledObject* arr[5] = {nullptr};

    for (int i = 0; i < 5; ++i) {
        arr[i] = new PooledObject;
        cout << "arr[" << i << "] = " << arr[i] << endl;
    }

    for (int i = 0; i < 5; ++i) {
        cout << "Deleting arr[" << i << "]" << endl;
        delete arr[i];
    }

    return 0;
}

This implementation uses a static buffer as an object pool. The fifth allocation fails (returns nullptr) since only four slots are available.

Constructing Objects at Specific Memory Addresses

By overloading new, you can place objects at predetermined addresses:

#include <iostream>
#include <cstdlib>

using namespace std;

class PlacedObject {
    static char* memory_pool;
    static char* allocation_map;
    static unsigned int pool_size;
    int payload;

public:
    static bool configureMemory(char* mem, size_t size) {
        pool_size = size / sizeof(PlacedObject);
        allocation_map = static_cast<char*>(calloc(pool_size, 1));
        if (!allocation_map) return false;
        memory_pool = mem;
        return true;
    }

    void* operator new(size_t size) {
        if (pool_size > 0) {
            for (size_t i = 0; i < pool_size; ++i) {
                if (!allocation_map[i]) {
                    allocation_map[i] = 1;
                    void* addr = memory_pool + i * sizeof(PlacedObject);
                    cout << "Placed at: " << addr << endl;
                    return addr;
                }
            }
        }
        return malloc(size);  // Fallback
    }

    void operator delete(void* ptr) {
        if (!ptr) return;
        if (pool_size > 0) {
            auto* mem = static_cast<char*>(ptr);
            ptrdiff_t offset = mem - memory_pool;
            if (offset % sizeof(PlacedObject) == 0) {
                size_t idx = offset / sizeof(PlacedObject);
                if (idx < pool_size) {
                    allocation_map[idx] = 0;
                    cout << "Released: " << ptr << endl;
                    return;
                }
            }
        }
        free(ptr);
    }
};

char* PlacedObject::memory_pool = nullptr;
char* PlacedObject::allocation_map = nullptr;
unsigned int PlacedObject::pool_size = 0;

int main() {
    alignas(PlacedObject) char custom_buffer[12] = {0};  // Holds 3 objects (assuming 4 bytes each)

    PlacedObject::configureMemory(custom_buffer, sizeof(custom_buffer));

    cout << "=== Single Object ===" << endl;
    auto* p1 = new PlacedObject;
    delete p1;

    cout << "=== Array Simulation ===" << endl;
    PlacedObject* arr[5];
    for (int i = 0; i < 5; ++i) {
        arr[i] = new PlacedObject;
        cout << "arr[" << i << "] = " << arr[i] << endl;
    }

    for (int i = 0; i < 5; ++i) {
        delete arr[i];
    }

    return 0;
}

Note: The buffer must be properly aligned for the object type (use alignas if needed).

Difference Between new[]/delete[] and new/delete

Array versionns (new[] and delete[]) are distinct operators. The compiler may allocate extra space to store array metadata (e.g., element count) so destructors are called the correct number of times.

#include <iostream>
#include <cstdlib>

using namespace std;

class Tracked {
    int x;
public:
    void* operator new(size_t s) {
        cout << "new: " << s << endl;
        return malloc(s);
    }

    void operator delete(void* p) {
        cout << "delete: " << p << endl;
        free(p);
    }

    void* operator new[](size_t s) {
        cout << "new[]: " << s << endl;
        return malloc(s);
    }

    void operator delete[](void* p) {
        cout << "delete[]: " << p << endl;
        free(p);
    }
};

int main() {
    auto* p = new Tracked;      // Calls operator new
    delete p;

    auto* arr = new Tracked[5]; // Calls operator new[]
    delete[] arr;

    return 0;
}

Output shows new[] requests more bytes than 5 * sizeof(Tracked) due to metadata overhead.

Tags: C++ memory-management operator-overloading mutable const-correctness

Posted on Mon, 21 Sep 2026 16:43:15 +0000 by TobesC