Click here to Skip to main content
14,030,721 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

1.5K views
Posted 16 Mar 2019
Licenced MIT

What happens when we “new”

, 16 Mar 2019
Rate this:
Please Sign up or sign in to vote.
A look at what the C++ compiler generates for us when we use the keywords new, new[], delete, and delete[]

Let us take a look at what the C++ compiler generates for us when we use the keywords new, new[], delete, and delete[] (I will skip over std::nothrow versions in this post).

new

When you write this:

T* t1 = new T;

The compiler really generates this:

T* t2 = (T*)operator new(sizeof(T));
try
{
     new (t2) T;
}
catch(...)
{
     operator delete(t2);
     throw;
}

First, using operator new, the memory for T is allocated. If the allocation fails, it will throw std::bad_alloc exception. Next (line 4), the object of type T is being constructed in the memory address t2 (this is called placement new). This however may fail due to T‘s constructor throwing an exception. The language guarantees that if that happens, the allocated memory will be freed. The code in line 6 will catch any exception emitted by T‘s constructor. Line 8 will then free the memory, and finally line 9 will re-throw the exception.

delete

This one-liner:

delete t1;

becomes this:

t2->~T();
operator delete(t2);

In line 1, T‘s destructor is called, and in line 2 the memory is freed using operator delete. All is good until T‘s destructor throws an exception. Noticed the lack of try-catch block? If T’s destructor throws, line 2 will never be executed. This is a memory leak and the reason why you should never throw exceptions from destructors; see Effective C++ by Scott Meyers, Item #8.

new[]

Things are about to get interesting. This innocent looking snippet of code:

#define HOW_MANY 3
T* t3 = new T[HOW_MANY];

becomes (gasp), more or less 😉 , this:

T* t4 = (T*)operator new(sizeof(size_t) + HOW_MANY * sizeof(T));
*((size_t*)t4) = HOW_MANY;
t4 = (T*)(((std::byte*)t4) + sizeof(size_t));
for(size_t i = 0; i < HOW_MANY; ++i)
{
    try
    {
        new (t4 + i) T;
    }
    catch(...)
    {
        for(size_t i2 = 0; i2 < i; ++i2)
            t4[i2].~T();
        t4 = (T*)(((std::byte*)t4) - sizeof(size_t));
        operator delete(t4);
        throw;
    }
}

So what happens when we allocate an array of objects on the heap? Where do we store the number of objects allocated which needs to be referenced later when deleting the array?
The compiler will generate code that allocates enough space for HOW_MANY instances of T, plus sizeof(size_t) bytes to store the number of objects created and will place the object count at offset 0 of the allocated memory block. Moreover, the language guarantees that it will either allocate and construct ALL instances of T, or none. So what happens if halfway through constructing the objects in the array, one of the constructors throws an exception? The compiler will generate proper cleanup code to deal with this scenario. This is what the above code illustrates. Let’s go through it line by line.

  • Line 1 allocates enough space for HOW_MANY objects plus sizeof(size_t) bytes for the number of objects in the array.
  • Line 2 stores the number of objects at offset 0 of the memory block.
  • Line 3 advances the pointer t4 by sizeof(size_t) bytes to where the first T will be created.
  • Line 4 is the loop inside which we’ll be creating the instances of T one by one.
  • Line 8 creates an instance of T at the right memory location inside the array using placement new (same as when constructing a single instance of T).
  • Line 10 catches any possible exception from T‘s constructor and begins the cleanup block of partially constructed array.
  • Line 12 defines the loop that will destroy all instances of T created up to the point of exception being thrown.
  • Line 13 calls the destructor of T.
  • Line 14 moves the pointer t4 back by sizeof(size_t) bytes to the beginning of the memory block allocated for the array.
  • Line 15 frees the previously allocated memory block.
  • Line 16, after having done all the cleanup, re-throws the exception.

delete[]

The following array delete statement:

delete [] t3;

is turned into something like this:

size_t how_many = *(size_t*)(((std::byte*)t4) - sizeof(size_t));
for(size_t i = 0; i < how_many; ++i)
    t4[i].~T();
t4 = (T*)(((std::byte*)t4) - sizeof(size_t));
operator delete(t4);

The compiler now has to generate code to first destroy all the instances of T stored in the array before deallocating the memory. After allocating the array of T‘s the t4 pointed at the first object in the array, not at the beginning of the memory block. That’s why…

  • Line 1 subtracts sizeof(size_t) bytes from t4 and retrieves the number of objects in the array.
  • Line 2 starts the loop that will call every T‘s destructor.
  • Line 3 destroys each instance of T.
  • Line 4 moves the t4 pointer back by sizeof(size_t) bytes to point at the beginning of the memory block.
  • Line 5, after all T‘s have been destroyed, frees the memory block.

Due to a lack of try-catch block, the same principle of not throwing exceptions from destructors applies here as well.

Complete Listing

#include <iostream>
#include <iomanip>
#include <new>

struct T
{
    T() { std::cout << "T() @ " << std::hex << std::showbase << this << std::endl; }
    ~T() { std::cout << "~T() @ " << std::hex << std::showbase << this << std::endl; }
};

int main(int argc, char** argv)
{
    // new
    // this:
    T* t1 = new T;
    // becomes this:
    T* t2 = (T*)operator new(sizeof(T));
    try
    {
        new (t2) T;
    }
    catch(...)
    {
        operator delete(t2);
        throw;
    }

    // delete
    // this:
    delete t1;
    // becomes this:
    t2->~T();
    operator delete(t2);

    // new []
    // this:
    #define HOW_MANY 3
    T* t3 = new T[HOW_MANY];
    // becomes this:
    T* t4 = (T*)operator new(sizeof(size_t) + HOW_MANY * sizeof(T));
    *((size_t*)t4) = HOW_MANY;
    t4 = (T*)(((std::byte*)t4) + sizeof(size_t));
    for(size_t i = 0; i < HOW_MANY; ++i)
    {
        try
        {
            new (t4 + i) T;
        }
        catch(...)
        {
            for(size_t i2 = 0; i2 < i; ++i2)
                t4[i2].~T();
            t4 = (T*)(((std::byte*)t4) - sizeof(size_t));
            operator delete(t4);
            throw;
        }
    }

    // delete []
    // this:
    delete [] t3;
    // becomes:
    size_t how_many = *(size_t*)(((std::byte*)t4) - sizeof(size_t));
    for(size_t i = 0; i < how_many; ++i)
        t4[i].~T();
    t4 = (T*)(((std::byte*)t4) - sizeof(size_t));
    operator delete(t4);

    return 1;
}

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

Martin Vorbrodt
Software Developer (Senior)
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
News+/- sizeof(size_t) for new []/delete [] may vary Pin
Chad3F19-Mar-19 14:52
memberChad3F19-Mar-19 14:52 
Just to note, on some platforms where the CPU has hard alignment requirements, more space than sizeof(size_t) may be required internally.

For example, new uint64_t[COUNT] on a 32-bit system will have sizeof(size_t) == 4, but uint64_t typically has an alignment of 8. Offsetting the allocation by only 4 could cause a bus error when accessing the data elements on such systems.

It might also be expanded in terms of an internal structure, e.g. new T[HOW_MANY]:
struct T_arr_intern
{
   size_t   count;
   T        elems[HOW_MANY];
};

struct T_arr_intern * t4_intern = new(sizeof(struct T_arr_intern));
t4_intern->count = HOW_MANY;

T* t4 = t4_intern->elems;
for(size_t i = 0; i < HOW_MANY; ++i)
{
...

In which case, the compiler would apply normal alignment/padding requirements between count and elems.

The reverse is syntactically a little messier (without a helper macro or something, which may exist). e.g., delete [] t3:
struct T_arr_intern
{
   size_t   count;
   T        elems[1]; // Just a placeholder size
};

struct T_arr_intern * t3_intern = (struct T_arr_intern *) ((std::byte*)t3) - offsetof(struct T_arr_intern, elems);
size_t how_many = t3_intern->count;
...
operator delete(t3_intern);

BugMistake/typo in delete/delete[] expansions Pin
Chad3F19-Mar-19 14:09
memberChad3F19-Mar-19 14:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web05 | 2.8.190419.4 | Last Updated 16 Mar 2019
Article Copyright 2019 by Martin Vorbrodt
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid