Insights into new and C++
published at 03.10.2014 15:37 by Jens Weller
Every now and then, I've been thinking about this. So this blogpost is also a summary of my thoughts on this topic, dynamic memory allocation and C++. Since I wrote the blog entries on smart pointers, and C++14 giving us make_unique, raw new and delete seem to disappear from C++ in our future code. Only some frameworks like Qt may impose using new in our code on to us, as they have their interfaces designed in such an old fashioned way.
But new does not disappear, it is just hidden. In modern C++ the stack is king, its used to guard all kind of things, and with smart pointers its also ensuring that our allocated memory will be freed. So in the dawning age of modern C++, users will not see or use new anywhere in the ideal world.
But still, invisible to the untrained eye, and behind the scenes new will be everywhere. Dynamic memory allocation will (IMHO) become in this coming age more and not less important.
In the beginning there was new
For C++, new is one of the key concepts which has been around since the beginning of (ISO) C++. A short example how new is used:
T* p = new T; ... production code ... delete p;
The first line allocates an object on the heap, p points to this dynamically allocated object. One of the advantages is, that this object will outlive the local context (aka stack), where p lives. The last line destroys the allocated object and frees the memory. You'll need one delete for every possible execution path, so a new is usually having many deletes in code. But if in your production code an exception is thrown and not caught, p will never be freed, the last line never executed. This is why most of C++ switched to smart pointers long before they were in the C++11 standard. Even without exceptions (CppCon showed that many folks seem to prefer using C++ without exceptions), smart pointers keep their value: you don't have to take care of delete, its just going to be done by the smart pointer when the time comes.
Also there is a version of new for arrays, which requires you to call delete instead of delete. But maybe you just want to use an std::vector then. Also unique_ptr and shared_ptr can handle this today.
New and modern C++
As I already said, new is hidden away in modern C++, and future generations using >= C++14 will have no need to use it directly in their application code. Some library writers might have to still care about the inner semantics of memory allocation such as in allocators, pools or container implementations. But this will be hidden from the 'normal' C++ programmer writing the applications running our world in a few years.
I already also mentioned that the stack is king in C++, so that normally a lot of the data will be allocated/held there rather then on the heap. But often this can also only be a handle like a std::vector/unique_ptr, that internally uses again new. One of the big additions to C++17 will be a better multithreading support, and maybe even the first support for task based approaches. Also already a lot of libraries exist in this domain (PPL, TBB), as modern machines have for quite a while more then one core. In this domain dynamic memory allocation is a key player, as it enables data to outlive the stack. But this domain brings a lot of new things, such as transporting exceptions across boundaries with exception_ptr and new challenges in debugging. This years closing keynote might offer new insights into this.
Also the common use cases for new are still around in modern C++, polymorphism at runtime is not always replaceable with compile-time polymorphism. Some objects are too large to fit on the stack, and some need to be stored in a way that the stack is not an option.
new and bad_alloc
While I'm at exceptions, I should also mention that new can throw bad_alloc. You might be writing C++ code for years without ever seeing such an exception, but on embedded systems like Arduino/Rasperry Pi or mobile devices this could be different. Dealing with bad_alloc depends on your context, abording/terminating is the most common I think.
But what if exceptions are turned off? The raw usage of new then will return a nullptr, but as new is in modern C++ now hidden and you mostly only will get a handle (e.g. smart pointer) , which you can check. But not always is this an option, for example std::vector::push_back will not give you a hint. For most users of C++, exceptions belong into modern C++, but as CppCon has shown, there is a large subset of C++ users which have exceptions turned off. One of them is google, with a huge C++ code base. I've had a very interesting conversation about this with Chandler Carruth before C++Now this year, and his argument for turning off exceptions was: its faster, because the optimizer can do a better job and the generated code is better. So at least for some of the heavy performance users in C++, the combination of modern C++ and turned off exceptions makes perfect sense.
So when looking at the no-exception support of unique_ptr and shared_ptr, I find that there is an allocator version for shared_ptr: allocate_shared where you could use a custom allocator. C++14 offers a make_unique function, but no allocator version, so no-exception users will have to implement this for their code.
Refactoring and new
There is still a lot of code that needs to be refactored into using smart pointers. It is a simple task to replace pointers with smart pointers and search & replace //delete. But is it the right thing to do so? And which smart pointer is the right one? shared_ptr or unique_ptr? Maybe boost instead of std? There is no easy answer to this, as it also relies heavily on your own code base. For arrays you usually can make use of std::vector or std::array.
First thing you should always consider, is new actually needed in this context? Maybe you could refactor things out to not use new, have polymorphism at compile time or use a different idiom. Also in some code bases new is way to often used in a java like styled C++, then its often a good thing to consider the option if the variable could not easily be on the stack. Not an option? Then std::unique_ptr/boost::scoped_ptr are the next possible solution. Both guard the allocation, and free it at the end of their scope, unique_ptr is able to be moved out of a scope. You can store unique_ptr in a vector this way for example. Both are not copyable. They guarantee a single point of ownership.
The last option is to use shared_ptr, both boost and C++11 have their own version. You should be always very careful on how and when to use shared_ptr. Its name already hints that your intent is to share a certain variable, in a reference counted way. So every copy of a shared_ptr will increment its use count at construction, and decrement at destruction. This also applies to temporary objects of shared_ptr, for example if its a non reference parameter to a function. You should try to achieve that the shared object in shared_ptr is constant, as it is also very similar to a global variable. A useful feature of shared_ptr is the weak_ptr, an option to hold a connection to an object that might or might not exist, which then is turned into a shared_ptr for local use. Also, shared_ptr will never release its allocation back to you, to store it into a unique_ptr for example. While it is possible create a shared_ptr from a unique_ptr, this will not allow you to use make_shared, which aligns the two variables for counting with the actual object.