Why Arrays have to be deleted via Delete[] in c++

This note is for beginner programmers in C++ who are wondering why everyone keeps telling them to use delete[] for arrays. But, instead of a clear explanation, senior developers just keep hiding behind the magical “undefined behavior” term. A little bit of code, some pictures and a glimpse into the nuts and bolts of the compiler – if interested, you’re welcome to read on.

introduction

You may not have noticed, or even noticed, but when you write code to free the memory space occupied by arrays, you don’t need to enter the number of items to be deleted. Is. And it all works great, though.

int *p = new SomeClass[42];  // Specify the quantity
delete[] p;                  // Don't specify the quantity

What is this, magic? In part, yes. And compiler developers have different approaches to describe and implement it.

There are two main approaches to the way compilers remember the number of elements in an array:

  • Recording the number of elements in an allocated array (“over-allocation”)
  • Storing number of elements in a separate associative array (“Associative Array”)

over-allocation

The first strategy, as the name implies, is done by simply inserting the number of elements before this The first element of an array. Note that in this case, the pointer you get after executing operator new Will point to the first element of the array, not its actual start.

operator new will point to the first element of the array

This pointer should not be passed normally under any circumstances delete operator, Most likely, this will only remove the first element of the array and leave the others intact. Note that I wrote “most likely” for a reason because one cannot predict every possible outcome and the way the program will behave. It all depends on what objects were in the array and whether their destructors did anything significant. As a result, we get the traditional undefined behavior. This is not what you would expect when trying to delete an array.

Fun facts: In most implementations of the standard library, delete operator just calls free Works from within. If we pass a pointer to an array, we get another undefined behavior. This is because it is expecting a pointer from the function calloc, malloc either realloc tasks, And as we found out above, it fails because the variable is hidden at the beginning of the array and the pointer is shifted to the beginning of the array.

what’s different about delete[] operator? It just counts the number of elements in an array, calls a destructor for each object, and then deallocates the memory (along with the hidden variables).

In fact, it’s basically pseudocode that delete[] p; Turns out when using this strategy:

// Get the number of elements in an array
size_t n = * (size_t*) ((char*)p - sizeof(size_t));

// Call the destructor for each of them
while (n-- != 0)
{
  p[n].~SomeClass();
}

// And finally cleaning up the memory
operator delete[] ((char*)p - sizeof(size_t));

MSVC, GCC and Clang compilers use this strategy. You can easily verify this by looking at the memory management code in the respective repositories (GCC and Clang) or by using the Compiler Explorer service.

compiler's assembler output

In the picture above (the upper part is the code, and the lower part is the compiler’s assembler output), I have sketched a simple code segment in which a structure and a function are defined to create an array of these structures.

Comment: The empty destructor of the struct isn’t extra code in any way. In fact, according to the Itanium CXX ABI, the compiler should use a different approach to memory management with arrays consisting of objects of trivially destructible types. In fact, there are a few more conditions, and you can find them all in section 2.7 “Array Operator New Cookies” in the Itanium CXX ABI. It also lists the requirements for where and how information about the number of elements in an array should occur.

So, in a nutshell what happens in the context of assembler:

  • Line N3: Store the required amount of memory (20 bytes for 5 objects + 8 bytes for array size) in the register;
  • Line N4: Call operator new To allocate memory;
  • Line N5: Store the number of elements at the beginning of the allocated memory;
  • Line N6: Shift pointer to beginning of array sizeof(size_t)The result is the return value.

The advantages of this method are its easy implementation and performance, but the disadvantage is the fatality of errors with the wrong choice. delete operator, At best, the program will crash with a “heap corrupt” error, and at worst you will have to search long and hard to find the cause of the program’s strange behavior.

associative array

The second strategy involves the presence of a hidden global container that stores pointers to arrays and the number of elements they contain. In this case, there is no hidden data in front of the arrays, and delete[] P; The call is implemented as follows:

// Getting the size of an array from the hidden global storage
size_t n = arrayLengthAssociation.lookup(p);

// Calling destructors for each element
while (n-- != 0)
{
  p[n].~SomeClass();
}

// Cleaning up the memory
operator delete[] (p);

Well, it doesn’t look “magical” like the previous way. Are there any other differences? Yes.

In addition to the previously mentioned lack of data hidden in the front of the array, the need to search for data in global storage causes a slight slowdown. But we balance this with the fact that the program may be more tolerant of the wrong choice. delete operator,

This approach has been used in the Cfront compiler. We won’t go into its implementation, but if you want to learn more about one of the first C++ compilers, you can check it out on GitHub.

a short epilogue

All of the above are the nuts and bolts of the compiler, and you shouldn’t rely on just one particular behavior. This is especially true when the program is planned to be ported to different platforms. Fortunately, there are several options for avoiding this type of error:

  • Use std::make_* function template. For example: std::make_unique, std::make_shared,
  • Use static analysis tools for early detection of errors.

Leave a Comment