NB _T is a reserved name , and you should not use it for the names of your own types / variables / parameters, etc.
The problem is here:
void _delete_ptr() { delete _ref_counter; delete _obj_ptr; }
This is not true for make_shared , because you did not select two separate objects.
The make_shared approach in Boost and GCC shared_ptr is to use a new derived type of control unit, which includes reference counting in the base class and adds storage space for the managed object in the derived type. If you make _ref_cntr responsible for deleting an object through a virtual function, then the derived type can override this virtual function to do something else (for example, just use an explicit call to the destructor to destroy the object without releasing the storage).
If you give _ref_cntr virtual destructor, then delete _ref_counter will correctly destroy the derived type, so it should become something like:
void _delete_ptr() { _ref_counter->dispose(); delete _ref_counter; }
Although if you do not plan to add support for weak_ptr , then there is no need to separate the destruction of the managed object and the control unit, you can simply create a destructor for the control unit:
void _delete_ptr() { delete _ref_counter; }
Your current project does not support the important property of shared_ptr , which is that the template<class Y> explicit shared_ptr(Y* ptr) constructor template<class Y> explicit shared_ptr(Y* ptr) should remember the original ptr type and delete the call on it, and not on _obj_ptr (which was converted to T* ). See note in the docs for the corresponding boost::shared_ptr constructor. To do this, _ref_cntr needs to use type erasure to store the original pointer, separate from _obj_ptr in the shared_ptr object, so _ref_cntr::dispose() can remove the correct value. This design change is also necessary to support the alias constructor .
class _ref_cntr { private: long counter; public: _ref_cntr() : counter(1) { } virtual ~_ref_cntr() { dispose(); } void inc() { ++counter; } void dec() { if (counter == 0) { throw std::logic_error("already zero"); } --counter; } long use_count() const { return counter; } virtual void dispose() = 0; }; template<class Y> struct _ptr_and_block : _ref_cntr { Y* _ptr; explicit _ptr_and_block(Y* p) : _ptr(p) { } virtual void dispose() { delete _ptr; } }; template<class Y> struct _object_and_block : _ref_cntr { Y object; template<class ... Args> _object_and_block(Args && ...args) : object(args...) { } virtual void dispose() { } };
With this construct, make_shared becomes:
template<class T, class ... Args> shared_ptr<T> make_shared(Args && ... args) { shared_ptr<T> ptr; auto tmp_object = new shared_ptr<T>::_object_and_block<T>(args...); ptr._obj_ptr = &tmp_object->object; ptr._ref_counter = tmp_object; return ptr; }
So _ref_counter points to a dedicated control unit, and when you execute delete _ref_counter , it means that you have a correctly matched new / delete pair that selects and deallocates the same object instead of creating a single object with new , then try delete two different objects.
To add weak_ptr support, you need to add a second counter to the control unit and transfer the call to dispose() from the destructor, so it is called when the first counter goes to zero (for example, in dec() ) and only call the destructor when the second counter reaches zero. Then doing all this in a thread-safe manner adds a lot of subtle complexity that would take a lot more time to explain than this answer.
Also, this part of your implementation is incorrect and a memory leak:
void _check_delete_ptr() { if (_obj_ptr == nullptr) { return; }
You can build a shared_ptr with a null pointer, for example. shared_ptr<int>((int*)nullptr) , in which case the constructor will allocate the control block, but since _obj_ptr is null, you will never delete the control block.