Make_shared - own implementation

I am trying to execute my own implementation of shared_ptr . I have problems with make_shared . The main feature of std::make_shared is that it allocates a block block and an object in a contiguous block of memory. How can i do the same?

I tried to do something like this:

 template<class T> class shared_ptr { private: class _ref_cntr { private: long counter; public: _ref_cntr() : counter(1) { } void inc() { ++counter; } void dec() { if (counter == 0) { throw std::logic_error("already zero"); } --counter; } long use_count() const { return counter; } }; template<class _T> struct _object_and_block { _T object; _ref_cntr cntr_block; template<class ... Args> _object_and_block(Args && ...args) : object(args...) { } }; T* _obj_ptr; _ref_cntr* _ref_counter; void _check_delete_ptr() { if (_obj_ptr == nullptr) { return; } _ref_counter->dec(); if (_ref_counter->use_count() == 0) { _delete_ptr(); } _obj_ptr = nullptr; _ref_counter = nullptr; } void _delete_ptr() { delete _ref_counter; delete _obj_ptr; } template<class _T, class ... Args> friend shared_ptr<_T> make_shared(Args && ... args); public: shared_ptr() : _obj_ptr(nullptr), _ref_counter(nullptr) { } template<class _T> explicit shared_ptr(_T* ptr) { _ref_counter = new counter_block(); _obj_ptr = ptr; } template<class _T> shared_ptr(const shared_ptr<_T> & other) { *this = other; } template<class _T> shared_ptr<T> & operator=(const shared_ptr<_T> & other) { _obj_ptr = other._obj_ptr; _ref_counter = other._ref_counter; _ref_counter->inc(); return *this; } ~shared_ptr() { _check_delete_ptr(); } }; 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->cntr_block; return ptr; } 

But when I delete the object and the block counter, an invalid heap block exception is thrown.

+5
source share
1 answer

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() { /* no-op */ } }; 

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.

+9
source

Source: https://habr.com/ru/post/1208654/


All Articles