Why 'std::make_shared' is always using the global memory allocation even with class overloaded new/delete operators?
Asked Answered
J

3

10

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.

Jaenicke answered 13/9, 2019 at 19:8 Comment(1)
They added std::allocate_shared for thisPreselector
M
13

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
Mortality answered 13/9, 2019 at 19:16 Comment(6)
Sometimes C++ just makes you groan with intense pain.Ultimate
@Ultimate 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).Mortality
It's painfull for people to need to be aware of the distinction between make_shared and allocate_shared...Ultimate
The alternative with make_unique is no better than just using new to initialize shared_ptr.Pallette
@Pallette 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 :-)Mortality
I can understand this. However, new will still hit you when you use it for placement new. Life is not fair.Pallette
P
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.

Pallette answered 13/9, 2019 at 19:14 Comment(2)
Do you happen to have a link to some discussion about this feature?Ultimate
@Ultimate you mean overloading new/delete for a class? I do not have a link, this is my personal opinion.Pallette
P
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.
Postgraduate answered 13/9, 2019 at 19:22 Comment(4)
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 :-)Ultimate
@Ultimate 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.Postgraduate
I understand, but other people will likely miss your answer if it's under a "Why" question.Ultimate
@Ultimate I understand and thanks for the tip, but I just like sharing when I feel like it.Postgraduate

© 2022 - 2024 — McMap. All rights reserved.