I'm new to C and I've been playing around with memory allocation lately. I have found out that when you declare a class with a destructor, like so:
class B
{
public:
~B() { }
};
And then create a heap array of it like so:
B* result = new B[8];
The allocator allocates 12 bytes but when I remove the destructor, it only allocates 8 bytes. This is how I'm measuring the allocation:
size_t allocated = 0;
void* operator new(size_t size)
{
allocated = size;
return malloc(size);
}
void deallocate(B* array, size_t size)
{
delete[] array;
allocated -= size * sizeof(B);
}
Of course I have to call deallocate
manually while the new
operator is called automatically.
I have found this problem while working with an std::string*
and I realised that the deallocator worked fine with an int*
but not with the former.
Does anyone know why that happens and more importantly: How to detect these programmatically at runtime?
Thanks in advance.
CodePudding user response:
You are looking at an implementation detail of how your compiler treats new[]
and delete[]
, so there isn't a definitive answer for the extra space being allocated since the answer will be specific to an implementation -- though I can provide a likely reason below.
Since this is implementation-defined, you cannot reliably detect this at runtime. More importantly, there shouldn't be any real reason to do this. Especially if you're new to C , this fact is more of an interesting/esoteric thing to know of, but there should be no real benefit detecting this at runtime.
It's important to also be aware that this only happens with array-allocations, and not with object allocations. For example, the following will print expected numbers:
struct A {
~A(){}
};
struct B {
};
auto operator new(std::size_t n) -> void* {
std::cout << "Allocated: " << n << std::endl;
return std::malloc(n);
}
auto operator delete(void* p, std::size_t n) -> void {
std::free(p);
}
auto main() -> int {
auto* a = new A{};
delete a;
auto* b = new B{};
delete b;
}
Output:
Allocated: 1
Allocated: 1
The extra storage only gets allocated for types with non-trivial destructors:
auto* a = new A[10];
delete[] a;
auto* b = new B[10];
delete[] b;
Outputs:
Allocated: 18
Allocated: 10
The most likely reason why this happens is that extra bookkeeping of a single size_t
is being kept at the beginning of allocated arrays containing non-trivial destructors. This would be done so that when delete
is called, the language can know how many objects require their destructors invoked. For non-trivial destructors, its able to rely on the underlying delete
mechanics of their deallocation functions.
This hypothesis is also supported by the fact that for the GNU ABI, the extra storage is sizeof(size_t)
bytes. Building for x86_64
yields 18
for an allocation of A[10]
(8
bytes for size_t
). Building for x86
yields 14
for that same allocation (4
bytes for size_t
).
Edit
I don't recommend doing this in practice, but you can actually view this extra data from arrays. The allocated pointer from new[]
gets adjusted before being returned to the caller (which you can test by printing the address from the new[]
operator).
If you read this data into a std::size_t
, you can see that this data -- at least for the GNU ABI -- contains the exact count for the number of objects allocated.
Again, I do not recommend doing this in practice since this exploits implementation-defined behavior. But just for fun:
auto* a = new A[10];
const auto* bytes = reinterpret_cast<const std::byte*>(a);
std::size_t count;
std::memcpy(&count, bytes - sizeof(std::size_t), sizeof(std::size_t));
std::cout << "Count " << count << std::endl;
delete[] a;
The output:
Count 10