我正在尝试自己实现shared_ptr.我有问题make_shared.它的主要特征是std::make_shared在连续的内存块中分配计数器块和对象.我怎么能这样做?
我尝试过这样的事情:
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;
}
Run Code Online (Sandbox Code Playgroud)
但是当我删除对象和计数器块时,会发生无效的堆块异常.
NB _T是保留名称,您不得将其用于您自己的类型/变量/参数等的名称.
问题出在这里:
void _delete_ptr()
{
delete _ref_counter;
delete _obj_ptr;
}
Run Code Online (Sandbox Code Playgroud)
这种情况是错误的,make_shared因为您没有分配两个单独的对象.
采取这种方法make_shared在Boost的和GCC的shared_ptr是使用一种新的派生类型的控制块,它包括在基类的引用计数,并增加了存储空间中派生类型被管理目标的.如果您_ref_cntr负责通过虚函数删除对象,那么派生类型可以覆盖该虚函数以执行不同的操作(例如,只使用显式析构函数调用来销毁对象而不释放存储空间).
如果你给_ref_cntr一个虚拟析构函数,那么delete _ref_counter将正确地销毁派生类型,所以它应该变成如下:
void _delete_ptr()
{
_ref_counter->dispose();
delete _ref_counter;
}
Run Code Online (Sandbox Code Playgroud)
虽然如果你不打算添加weak_ptr支持,那么就不需要分离托管对象和控制块的破坏,你可以让控制块的析构函数同时执行:
void _delete_ptr()
{
delete _ref_counter;
}
Run Code Online (Sandbox Code Playgroud)
您当前的设计无法支持重要属性shared_ptr,即 template<class Y> explicit shared_ptr(Y* ptr)构造函数必须记住原始类型ptr并在其上调用delete,而不是on _obj_ptr(已转换为T*).请参阅文档中的相应构造函数的注释boost::shared_ptr.为了使这项工作的_ref_cntr使用需求类型擦除存储原始指针,从单独_obj_ptr的shared_ptr对象,这样_ref_cntr::dispose()可以删除正确的值.设计中的这种变化也需要支持别名构造函数.
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 */ }
};
Run Code Online (Sandbox Code Playgroud)
通过这种设计,make_shared变为:
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;
}
Run Code Online (Sandbox Code Playgroud)
所以_ref_counter指向分配的控制块,当你这样做delete _ref_counter意味着你有一个正确匹配new/ delete对分配和释放相同的对象,而不是创建一个对象new然后尝试delete两个不同的对象.
要添加weak_ptr支持,您需要向控制块添加第二个计数,并将调用dispose()移出析构函数,因此当第一个计数变为零(例如,在dec())时调用它,并且在第二个计数进入时仅调用析构函数为零.然后以线程安全的方式完成所有这些操作会增加许多微妙的复杂性,这些复杂性需要比这个答案更长的解释.
此外,这部分实现是错误的并且泄漏了内存:
void _check_delete_ptr()
{
if (_obj_ptr == nullptr)
{
return;
}
Run Code Online (Sandbox Code Playgroud)
构造函数可以shared_ptr使用空指针,例如shared_ptr<int>((int*)nullptr),在这种情况下构造函数将分配一个控制块,但因为_obj_ptr为null ,所以永远不会删除控制块.