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