How C++ delete Differentiates Single Objects and Arrays

In the C++ programming language, the runtime does not automatically deduce whether a pointer refers to a single object or a contiguous array of objects. Instead, the language enforces this distinction at compile time through two distinct operators: delete and delete[]. While single-object delete simply invokes a single destructor and frees the memory, array destruction requires tracking the number of allocated elements, which compilers implement using low-level mechanisms such as "array cookies" or allocator lookup tables.

The Programmer's Responsibility

C++ does not store dynamic type metadata within raw pointers. A pointer of type T* looks identical to the compiler whether it points to a single instance of T allocated via new T or an array of instances allocated via new T[N].

Because raw pointers lack this context, the language requires the programmer to explicitly differentiate between the two operations:

  • delete ptr; is reserved for pointers allocated with new.
  • delete[] ptr; is reserved for pointers allocated with new[].

Mismatching these operators—such as calling delete on an array or delete[] on a single object—results in undefined behavior.

How Single-Object Destruction Works (delete)

When delete ptr is executed, the compiler generates instructions to perform two specific tasks:

  1. Destruction: It invokes the destructor for the single object: ptr->~T(). If the static type of ptr has a virtual destructor, the compiler uses the object's virtual method table (vtable) to invoke the correct derived destructor.
  2. Deallocation: It releases the underlying memory by calling the deallocation function: operator delete(ptr).

Because only one object exists, the compiler does not need to look up an element count.

How Array Destruction Works (delete[])

When delete[] ptr is executed, the compiler must destroy every element in the array before freeing the memory block. To accomplish this, the runtime must determine:

  1. The number of elements in the array.
  2. The true starting address of the allocated block.

The compiler then generates a loop that calls the destructor ~T() on each element in reverse order of construction. Finally, it invokes operator delete[](raw_pointer) to release the entire memory block.

The primary mechanism defined by major C++ Application Binary Interfaces (ABIs), such as the Itanium C++ ABI and the MSVC ABI, to track the number of elements is known as an array cookie (or overhead allocation).

When dynamic memory is allocated using new T[N] for a type T that has a non-trivial destructor:

  1. Over-allocation: The runtime requests more bytes than N * sizeof(T). It adds padding equivalent to the size of a size-tracking integer (typically sizeof(std::size_t)).
  2. Storing the Count: The runtime writes the value N into the initial bytes of the allocated memory block.
  3. Pointer Adjustment: The pointer returned to the user is offset forward by the size of the cookie, pointing directly to the first actual element of type T.

When delete[] ptr is subsequently invoked:

  1. Reading the Count: The compiler generates code that offsets backward from ptr to read the stored value N.
  2. Destruction Loop: The compiler executes a loop calling ~T() exactly N times.
  3. Pointer Readjustment: The base pointer (including the cookie) is passed to operator delete[] to reclaim the memory.

If a type has a trivial destructor (such as int or a plain-old-data struct), compilers often optimize this process by omitting the array cookie entirely. Because no destructors need to be executed, the underlying memory allocator only needs the original pointer to release the raw memory block.

Why Mismatching Causes Undefined Behavior

Because of the architectural differences between delete and delete[], mixing them leads to severe runtime failures:

  • Calling delete on an array: The compiler will only invoke the destructor for the first element (ptr[0]). If an array cookie was stored, the base address passed to the memory allocator will be offset by the size of the cookie rather than the true start of the allocation, leading to heap corruption.
  • Calling delete[] on a single object: The compiler will attempt to read memory immediately preceding the single object, misinterpreting whatever bytes reside there as an element count. It will then attempt to execute destructors on unallocated or unrelated memory, leading to segmentation faults or memory corruption.