Are new and delete still useful in C++14?

5.7k views Asked by At

Given availability of make_unique and make_shared, as well as automatic deletion by unique_ptr and shared_ptr destructors, what are the situations (apart from supporting legacy code) for using new and delete in C++14?

4

There are 4 answers

11
Barry On BEST ANSWER

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.

1
Mark B On

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.

0
TheCppZoo On

I would say the only reason for new and delete is to implement other kinds of smart pointers.

For example, the library still does not have intrusive pointers as boost::intrusive_ptr, which is a pity since they are superior for performance reasons to shared pointers, as Andrei Alexandrescu points out.

9
Yakk - Adam Nevraumont On

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_ptrs 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.