Are new and delete still useful in C++14?
While smart pointers are preferable to raw pointers in many cases, there are still lots of use-cases for new
/delete
in C++14.
If you need to write anything that requires in-place construction, for example:
- a memory pool
- an allocator
- a tagged variant
- binary messages to a buffer
you will need to use placement new
and, possibly, delete
. No way around that.
For some containers that you want to write, you may want to use raw pointers for storage.
Even for the standard smart pointers, you will still need new
if you want to use custom deleters since make_unique
and make_shared
don't allow for that.
It is a relatively common choice to use make_unique
and make_shared
rather than raw calls to new
. It is not, however, mandatory. Assuming you choose to follow that convention, there are a few places to use new
.
First, non-custom placement new
(I'll neglect the "non-custom" part, and just call it placement new
) is a completely different card game than standard (non-placement) new
. It is logically paired with manually calling a destructor. Standard new
both acquires a resource from the free store, and constructs an object in it. It is paired with delete
, which destroys the object and recycles the storage to the free store. In a sense, standard new
calls placement new
internally, and standard delete
calls the destructor internally.
Placement new
is the way you directly call a constructor on some storage, and is required for advanced lifetime management code. If you are implementing optional
, a type safe union
on aligned storage, or a smart pointer (with unified storage and non-unified lifetime, like make_shared
), you will be using placement new
. Then at the end of a particular object's lifetime, you directly call its destructor. Like non-placement new
and delete
, placement new
and manual destructor calls come in pairs.
Custom placement new
is another reason to use new
. Custom placement new
can be used to allocate resources from a non-global pool -- scoped allocation, or allocation into a cross-process shared memory page, allocation into video card shared memory, etc -- and other purposes. If you want to write make_unique_from_custom
that allocates its memory using custom placement new, you'd have to use the new
keyword. Custom placement new
could act like placement new (in that it doesn't actually acquire resources, but rather the resource is somehow passed in), or it could act like standard new
(in that it acquires resources, maybe using the arguments passed in).
Custom placement delete
is called if a custom placement new
throws, so you might need to write that. In C++ you don't call custom placement delete
, it (C++) calls you(r overload).
Finally, make_shared
and make_unique
are incomplete functions in that they don't support custom deleters.
If you are writing make_unique_with_deleter
, you can still use make_unique
to allocate the data, and .release()
it into your unique-with-deleter's care. If your deleter wants to stuff its state into the pointed-to buffer instead of into the unique_ptr
or into a separate allocation, you'll need to use placement new
here.
For make_shared
, client code doesn't have access to the "reference counting stub" creation code. As far as I can tell you cannot easily both have the "combined allocation of object and reference counting block" and a custom deleter.
In addition, make_shared
causes the resource allocation (the storage) for the object itself to persist as long as weak_ptr
s to it persist: in some cases this may not be desirable, so you'd want to do a shared_ptr<T>(new T(...))
to avoid that.
In the few cases where you want to call non-placement new
, you can call make_unique
, then .release()
the pointer if you want to manage separately from that unique_ptr
. This increases your RAII coverage of resources, and means that if there are exceptions or other logic errors, you are less likely to leak.
I noted above I didn't know how to use a custom deleter with a shared pointer that uses a single allocation block easily. Here is a sketch of how to do it trickily:
template<class T, class D>
struct custom_delete {
std::tuple<
std::aligned_storage< sizeof(T), alignof(T) >,
D,
bool
> data;
bool bCreated() const { return std::get<2>(data); }
void markAsCreated() { std::get<2>()=true; }
D&& d()&& { return std::get<1>(std::move(data)); }
void* buff() { return &std::get<0>(data); }
T* t() { return static_cast<T*>(static_cast<void*>(buff())); }
template<class...Ts>
explicit custom_delete(Ts...&&ts):data(
{},D(std::forward<Ts>(ts)...),false
){}
custom_delete(custom_delete&&)=default;
~custom_delete() {
if (bCreated())
std::move(*this).d()(t());
}
};
template<class T, class D, class...Ts, class dD=std::decay_t<D>>
std::shared_ptr<T> make_shared_with_deleter(
D&& d,
Ts&&... ts
) {
auto internal = std::make_shared<custom_delete<T, dD>>(std::forward<D>(d));
if (!internal) return {};
T* r = new(internal->data.buff()) T(std::forward<Ts>(ts...));
internal->markAsCreated();
return { internal, r };
}
I think that should do it. I made an attempt to allow stateless deleters to use no up space by using a tuple
, but I may have screwed up.
In a library-quality solution, if T::T(Ts...)
is noexcept
, I could remove the bCreated
overhead, as there would be no opportunity for a custom_delete
to have to be destroyed before the T
is constructed.
The only reason I can think of is that occasionally you may wish to use a custom deleter with your unique_ptr
or shared_ptr
. To use a custom deleter you need to create the smart pointer directly, passing in the result of new
. Even this isn't frequent but it does come up in practice.
Other than that it seems that make_shared
/make_unique
should cover pretty much all uses.