Search code examples
c++new-operatordelete-operator

How to get total allocated size in custom operator delete[]?


I understand we can use Class-specific overloads of operator new[]/delete[] to specify our own memory allocation strategy.

So I write the following code. I barely want to use the held field to record how much memory of a type X<id> is allocated on heap.

However, I meet the problem that in operator new[], I can get the count, while I can't in operator delete[]. So in operator delete[], I don't know how much memory is actually released.

#include <cstdio>
#include <string>
#include <vector>

template <int Id>
struct X {
    static inline int held = 0;
    static void* operator new(std::size_t count)
    {
        held += count;
        return ::operator new(count);
    }
 
    static void* operator new[](std::size_t count)
    {
        held += count;
        return ::operator new[](count);
    }

    static void operator delete(void* ptr)
    {
        held -= sizeof(X<Id>);
        ::operator delete(ptr);
    }

    static void operator delete[] (void* ptr)
    {
        // This is not correct.
        // However, I don't know the count.
        held -= sizeof(X<Id>);
        ::operator delete [] (ptr);
    }

    std::string s1;
    std::vector<int> v1;
};

int main() {
    auto * dx2 = new X<1>[100]();
    printf("x1d2 %d\n", X<1>::held);
    delete [] dx2;
    printf("x1d2 released %d\n", X<1>::held); // exptected 0
}

I understand one way is that I can allocate an extra bytes to store the count, it introduces extra overhead though.

Moreover, the compiler will actually store the count somewhere when compiling operator delete [], however, it is not exposed to users. I think I am now allowed to access the count.


Solution

  • Since C++14, there are flavors of operator delete (number (5)) and operator delete[] (number (6)), that you can override and have an additional parameter for the allocated size:

    void operator delete ( void* ptr, std::size_t sz ) noexcept; (5)
    void operator delete[] ( void* ptr, std::size_t sz ) noexcept; (6)

    These operators have priority over the ones without the size (at least for classes with non-trivial destructor like yours):

    Called instead of (1,2) if a user-defined replacement is provided, except that it's unspecified whether (1,2) or (5,6) is called when deleting objects of incomplete type and arrays of non-class and trivially-destructible class types.

    In your case you can replace your operator delete and operator delete[] with:

    static void operator delete(void* ptr, std::size_t sz)
    {
        held -= sz;
        ::operator delete(ptr);
    }
    
    static void operator delete[](void* ptr, std::size_t sz)
    {
        held -= sz;
        ::operator delete[](ptr);
    }
    

    Output on MSVC (last value is 0 as expected):

    x1d2 7208
    x1d2 released 0
    

    Live demo

    Some notes:

    1. held should better be a size_t to match the type in the new and delete operators.
    2. My live demo is using std::cout instead of the less-recommnded C style printfs.
    3. In the live demo held has different values after the allocation in different compilers because the size of classes is implementation dependant (but it is always 0 after deallocation).
    4. Regarding classes with a trivial destructor (which is not your case, and mentioned just for completess):
      • Using this variant of operator delete[] may cause memory increase for classes with a trivial destructor, because it will force the compiler to store the size (that otherwise it would not have to).
      • If defined at global scope (not your case), the sized variant is not guaranteed to be called (it is only guaranteed to be called for classes with non-trivial destructors).