8

When using std::make_shared<C> the class overloaded new/delete operators are not called.

When using std::shared_ptr<C>, std::unique_ptr<C> & std::make_unique<C> the class overloaded new/delete operators are used.

When looking at the documentation it's perfectly correct and well documented.

cppreference explains the behavior:

std::make_shared uses ::new, so if any special behavior has been set up using a class-specific operator new, it will differ from std::shared_ptr<T>(new T(args...)).

Below is some pseudo-code to better highlight the behavior:

#include <memory>

class C {
 public:
  void* operator new(size_t size) {
    void* p = ::operator new(size);
    std::cout << "C::new() -> " << p << "\n";
    return p;
  }

  void operator delete(void* p) {
    std::cout << "C::delete() -> " << p << "\n";
    ::operator delete(p);
  }
};

std::shared_ptr<C> ptr = std::make_shared<C>();

From an external point of view, it seems inconsistent and error prone. Overloading class new/delete operators should always be used.

So, what is the rationale of the behavior?

And, where is the C++ specification detailing the std::make_shared behavior?

Thanks for your help.

David G
  • 94,763
  • 41
  • 167
  • 253
  • 3
    They added [`std::allocate_shared`](https://en.cppreference.com/w/cpp/memory/shared_ptr/allocate_shared) for this – Cory Kramer Sep 13 '19 at 19:10

3 Answers3

12

So, what is the rational of the behavior?

The reason this is done is because make_shared doesn't just allocate your object, it also allocates the control block of the shared_ptr. To make this as efficient as possible, it calls new once and allocates enough storage for the control block and the object in one go. Otherwise it would have to call new twice which doubles the allocation overhead.

If you want to use a custom allocator then you need to use std::allocate_shared and it will use your custom allocator to do a single memory acquisition to create the shared_ptr.


Another option is to use std::make_unique to create a unique_ptr, and then use that to initialize the shared_ptr. This works because unique_ptr does not have a control block so std::make_unique allocates in the form of

unique_ptr<T>(new T(std::forward<Args>(args)...))

That would give you

std::shared_ptr<C> ptr = std::make_unique<C>();

which outputs

C::new() -> 0xf34c20
C::delete() -> 0xf34c20
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 2
    Sometimes C++ just makes you groan with intense pain. – einpoklum Sep 13 '19 at 19:29
  • 2
    @einpoklum For the most part it is painless. This is just another reason to not overload new and delete for your type. If they really want to they can leverage `unique_ptr` (adding that to the answer in a minute). – NathanOliver Sep 13 '19 at 19:35
  • It's painfull for people to need to be aware of the distinction between `make_shared` and `allocate_shared`... – einpoklum Sep 13 '19 at 19:37
  • The alternative with `make_unique` is no better than just using `new` to initialize shared_ptr. – SergeyA Sep 13 '19 at 19:41
  • @SergeyA It has one important advantage, I didn't have to type `new` to get it to do what the OP wanted. I like not having to type `new` :-) – NathanOliver Sep 13 '19 at 19:43
  • I can understand this. However, `new` will still hit you when you use it for placement new. Life is not fair. – SergeyA Sep 13 '19 at 19:45
5

The immediate reason for this behavior is the fact that std::make_shared performs a single allocation to allocate both control block and the object. It has no other option but to use global operator new for this.

Also, I would like to mention that I personally consider ability to overload the new/delete operators for a class one of the most ill-conceived features of C++. A class should not prescribe methods of it's memory allocation. Rather, class allocation should be delegated to particular task at hand, and a well-designed class should behave equally well be it allocated in dynamic storage, automatic storage, in memory mapped file or on a flash card.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
1

The other answers explain why std::make_shared doesn't use your class specific operator new and operator delete. This is just a side note.

If you'd like to use referenced counter pointers with your object and use your class specific allocation/deallocation another option is to use boost::intrusive_ptr.

#include <iostream>
#include <boost/intrusive_ptr.hpp>
#include <boost/smart_ptr/intrusive_ref_counter.hpp>

class C : public boost::intrusive_ref_counter<C, boost::thread_safe_counter> {
public:
    static void* operator new(size_t size) {
        void* p = ::operator new(size);
        std::cout << "C::new() -> " << p << "\n";
        return p;
    }

    static void operator delete(void* p) {
        std::cout << "C::delete() -> " << p << "\n";
        ::operator delete(p);
    }
};

int main() {
    boost::intrusive_ptr<C> c(new C);
}

The drawbacks of boost::intrusive_ptr are:

  • It is intrusive. The class must have the reference counter as one of its members or derive from boost::intrusive_ref_counter. You don't necessarily need to change the class definition, a wrapper class with a counter that derives from your class can be used (but that loses class specific allocation/deallocations functions again).
  • No weak pointers.

The benefits are:

  • sizeof(boost::intrusive_ptr<T>) == sizeof(T*), whereas sizeof(std::shared_ptr<T>) == 2 * sizeof(T*).
  • You have a choice of thread safe or unsafe reference counter. For objects that never cross thread boundaries using a thread safe counter is wasteful: atomic increments and decrements are most expensive (as all of atomic read-modify-write operations). std::shared_ptr<T> always uses a 2 (two) thread-safe counters in multi-threaded applications. In well designed applications only objects of a few message classes ever cross thread boundaries.
  • The reference counter is stored inside your object which is the best case in terms of locality of reference / most cache friendly. std::make_shared does that for you as well, but you lose your class specific allocation/deallocation functions.
  • Can be assigned to and swapped in wait-free atomic fashion.
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • This is a nice answer, but not to OP's question, which is "Why XYZ".... Consider asking a follow-up question about overcoming this restriction, posting your answer there and a comment here pointing people over there. I promise an upvote :-) – einpoklum Sep 13 '19 at 19:31
  • @einpoklum This is a speculative answer with _The other answers explain why... / If you'd like to use referenced pointers..._ caveat. I could post a question and answer it myself, but that is not my style. – Maxim Egorushkin Sep 13 '19 at 19:32
  • I understand, but other people will likely miss your answer if it's under a "Why" question. – einpoklum Sep 13 '19 at 19:35
  • @einpoklum I understand and thanks for the tip, but I just like sharing when I feel like it. – Maxim Egorushkin Sep 13 '19 at 19:37