Search code examples
c++arraysnew-operatorallocationdelete-operator

C++ size of allocated array extraced memory close to returned pointer from malloc


I have lately been experimenting with overloading the new and delete operators, and I noticed something very interesing.

When I allocate, let's say, class T with new T(); vs new T[1](), there is a difference in size of the allocated block. The difference is exactly sizeof(size_t).

Then I thought about this legendary question: "How does the delete operator know the size of the block it's about to delete?" With a single element, it's easy, but with an array, not so.

Legends say that for arrays, the size is put before the first element. So I extracted it and everything would be fine, but I notice that when the class does not have a destructor, this does not work. Same goes for primitive types like int, char, and so on.

Not only that (without destructor), the overloaded delete[] operator doesn't show the correct size of the block, but somehow does not cause memory leaks.

So, my questions are:

  1. Why is the destructor so crucial in what seems to be fixing allocation of the array and correcting the size in an overloaded delete[] operator?

  2. How is it possible that the incorrect size in an overloaded delete[] operator does not cause memory leaks?

  3. Is it possible to extract the number of allocated instances from arrays of primitive types or classes without destructors?

THANK GOD I CHECKED -> THIS PROBLEM OCCURES ONLY IN VISUAL STUDIO 2019 on x86 and x64 compiler version 192930147

For example: https://www.onlinegdb.com/online_c++_compiler does not have this behavior.

!!! BUT, extracting from primitives still fails, so the 3 questions STILL HOLD !!!

Code for the problem:

#include <iostream>
#include <cstring>
using namespace std;

#define log(x) cout << #x << " = " << x << '\n';


class T_with_destructor
{
    long long a;
    double b;

public:    
    ~T_with_destructor() {}

    void* operator new (size_t s)
    {
        cout << "new -> "; log(s);        
        return malloc(s);
    }
    void* operator new[](size_t s)
    {
        cout << "new [] -> "; log(s);        
        return malloc(s);
    }
    void operator delete(void* ptr, size_t s)
    {
        cout << "delete -> "; log(s);        
        free(ptr);
    }
    void operator delete[](void* ptr, size_t s)
    {
        cout << "delete[] -> "; log(s);        
        free(ptr);
    }
};

class T_without_destructor
{
    long long a;
    double b;

public:
    //~T_without_destructor() {}

    void* operator new (size_t s)
    {
        cout << "new -> "; log(s);
        return malloc(s);
    }
    void* operator new[](size_t s)
    {
        cout << "new [] -> "; log(s);
        return malloc(s);
    }
    void operator delete(void* ptr, size_t s)
    {
        cout << "delete -> "; log(s);
        free(ptr);
    }
    void operator delete[](void* ptr, size_t s)
    {
        cout << "delete[] -> "; log(s);
        free(ptr);
    }
};

int main()
{
const size_t size = 3;
    {
        T_with_destructor* arr = new T_with_destructor[size]();

        size_t* size_from_arr = (size_t*)((char*)arr - sizeof(size_t));
        cout << *size_from_arr << '\n';

        delete[] arr;
    }
    cout << "\n\n";
    {
        T_without_destructor* arr = new T_without_destructor[size]();

        size_t* size_from_arr = (size_t*)((char*)arr - sizeof(size_t));
        cout << *size_from_arr << '\n';

        delete[] arr;
    }
    cout << "\n\n";
    {
        int* arr = new int[size]();

        size_t* size_from_arr = (size_t*)((char*)arr - sizeof(size_t));
        cout << *size_from_arr << '\n';

        delete[] arr;
    }
    cout << "\n\n";
return 0;
}

Memory leak not happening -> prints -> new [] -> s = 160 delete[] -> s = 16

int main()
{
    for(int i=0; i<1000000000; i++)
    {
        T_without_destructor* arr = new T_without_destructor[10]();

        size_t* size_from_arr = (size_t*)((char*)arr - sizeof(size_t));
        cout << *size_from_arr << '\n';

        delete[] arr;
    }
return 0;
}

So, did I accidently find a bug in the VS compiler?


Solution

    1. Why is the destructor so crucial in what seems to be fixing allocation of the array and correcting the size in an overloaded delete[] operator?

    The compiler has to pass implicit this pointer to each member function of the class, including the destructor. So, when your class has a destructor, the compiler tells you the size of the block, so that you (knowing the size of the class) can figure out addresses of this pointers for each array member, and explicitly call each one of them.

    1. How is it possible that the incorrect size in an overloaded delete[] operator does not cause memory leaks?

    Because this is not the size used to deallocate the block. The real size of the block is stored by the OS and is tied to the block address.

    The size of the block given to the delete[] operator is irrelevant, since you don't have to call any destructors. The specific MSVC compiler that gave you incorrect size probably never stored it with the block (since it's unnecessary) and just gave you a garbage value.

    1. Is it possible to extract the number of allocated instances from arrays of primitive types or classes without destructors?

    According to this answer different OSes have different ways of extracting allocated block size given the pointer. You already know the size of the class/primitive, so you should be able to figure out length of the array.

    So, did I accidently find a bug in the VS compiler?

    Sorry, but no, this is not your lucky day. ;)