为什么 std: : share_ptr < void > 起作用

我发现一些代码使用 std: : share _ ptr 在关机时执行任意清理。起初我认为这个代码不可能工作,但后来我尝试了以下方法:

#include <memory>
#include <iostream>
#include <vector>


class test {
public:
test() {
std::cout << "Test created" << std::endl;
}
~test() {
std::cout << "Test destroyed" << std::endl;
}
};


int main() {
std::cout << "At begin of main.\ncreating std::vector<std::shared_ptr<void>>"
<< std::endl;
std::vector<std::shared_ptr<void>> v;
{
std::cout << "Creating test" << std::endl;
v.push_back( std::shared_ptr<test>( new test() ) );
std::cout << "Leaving scope" << std::endl;
}
std::cout << "Leaving main" << std::endl;
return 0;
}

这个程序给出了输出:

At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed

我有一些关于为什么这样做的想法,这与在 G + + 中实现的 std: : share _ ptrs 的内部原理有关。由于这些对象将内部指针与计数器包装在一起,所以从 std::shared_ptr<test>std::shared_ptr<void>的转换可能不会阻碍析构函数的调用。这个假设正确吗?

当然,还有一个更重要的问题: 这是否能够保证按照标准工作,或者对 std: : share _ ptr 的内部进一步更改,其他实现实际上会破坏这段代码?

37530 次浏览

它能工作是因为它使用了类型擦除。

基本上,在构建 shared_ptr时,它会传递一个额外的参数(如果愿意,实际上可以提供这个参数) ,这个参数就是 deleter 函数。

这个默认函数接受一个指针作为参数,指向您在 shared_ptr中使用的类型,因此这里是 void,将它适当地强制转换为您在这里使用的 test的静态类型,并调用这个对象上的析构函数。

任何足够先进的科学都像魔法一样,不是吗?

Test*是从内存隐式转换为 void*的,因此 shared_ptr<Test>是从内存隐式转换为 shared_ptr<void>的。这是因为 shared_ptr被设计用来在运行时而非编译时控制销毁,它们将在内部使用继承来调用适当的销毁函数,就像在分配时一样。

诀窍是 std::shared_ptr执行类型擦除。基本上,当创建一个新的 shared_ptr时,它将在内部存储一个 deleter函数(可以将其作为构造函数的参数给出,但是如果不提供调用 delete的默认值)。当 shared_ptr被销毁时,它将调用这个存储函数,这个函数将调用 deleter

这里可以看到一个简单的类型擦除示意图,它使用 std: : function 进行简化,避免了所有的引用计数和其他问题:

template <typename T>
void delete_deleter( void * p ) {
delete static_cast<T*>(p);
}


template <typename T>
class my_unique_ptr {
std::function< void (void*) > deleter;
T * p;
template <typename U>
my_unique_ptr( U * p, std::function< void(void*) > deleter = &delete_deleter<U> )
: p(p), deleter(deleter)
{}
~my_unique_ptr() {
deleter( p );
}
};


int main() {
my_unique_ptr<void> p( new double ); // deleter == &delete_deleter<double>
}
// ~my_unique_ptr calls delete_deleter<double>(p)

当从另一个 shared_ptr中复制(或默认构造)一个 shared_ptr时,会传递删除器,这样当您从 shared_ptr<U>中构造一个 shared_ptr<T>时,关于要调用哪个析构器的信息也会在 deleter中传递。

shared_ptr<T>逻辑上[ * ]至少有两个相关数据成员:

  • 指向被管理对象的指针
  • 指向用于销毁它的删除函数的指针。

考虑到构造 shared_ptr<Test>的方式,shared_ptr<Test>的删除函数是 Test的正常函数,它将指针转换为 Test*delete将其转换为 delete

当你把你的 shared_ptr<Test>推入到 shared_ptr<void>的矢量中时,其中的 都有会被复制,尽管第一个会被转换成 void*

因此,当带有最后一个引用的向量元素被销毁时,它将指针传递给一个正确销毁它的删除器。

它实际上比这个要复杂一些,因为 shared_ptr可以使用一个删除器 函子,而不仅仅是一个函数,所以甚至可能存储每个对象的数据,而不仅仅是一个函数指针。但是在这种情况下,没有这样的额外数据,只需存储一个指向模板函数实例化的指针,并使用一个模板参数来捕获必须删除指针的类型就足够了。

[ * ]逻辑上是指它可以访问它们——它们可能不是 share _ ptr 本身的成员,而是它所指向的某个管理节点。

构造函数 shared_ptr<T>(Y *p)似乎确实在调用 shared_ptr<T>(Y *p, D d),其中 d是为对象自动生成的删除器。

当这种情况发生时,对象 Y的类型是已知的,因此这个 shared_ptr对象的删除器知道要调用哪个析构函数,当指针存储在 shared_ptr<void>的向量中时,这些信息不会丢失。

实际上,规范要求接收 shared_ptr<T>对象接受 shared_ptr<U>对象必须是真的,而且 U*必须隐式地转换为 T*,而 T=void当然就是这种情况,因为任何指针都可以隐式地转换为 void*。没有说删除将是无效的,所以确实规范要求这将正常工作。

从技术上讲,一个 shared_ptr<T>持有一个指向隐藏对象的指针,该对象包含引用计数器和一个指向实际对象的指针; 通过将删除器存储在这个隐藏结构中,它可以使这个看似神奇的特性工作,同时仍然保持 shared_ptr<T>与常规指针一样大(然而,取消引用指针需要一个双重间接

shared_ptr -> hidden_refcounted_object -> real_object

我将用一个用户可以理解的、非常简单的 share _ ptr 实现来回答这个问题(两年后)。

首先,我将访问一些副类: share _ ptr _ base、 sp _ count _ base sp _ count _ impl 和 check _ deleter,其中最后一个是模板。

class sp_counted_base
{
public:
sp_counted_base() : refCount( 1 )
{
}


virtual ~sp_deleter_base() {};
virtual void destruct() = 0;


void incref(); // increases reference count
void decref(); // decreases refCount atomically and calls destruct if it hits zero


private:
long refCount; // in a real implementation use an atomic int
};


template< typename T > class sp_counted_impl : public sp_counted_base
{
public:
typedef function< void( T* ) > func_type;
void destruct()
{
func(ptr); // or is it (*func)(ptr); ?
delete this; // self-destructs after destroying its pointer
}
template< typename F >
sp_counted_impl( T* t, F f ) :
ptr( t ), func( f )


private:


T* ptr;
func_type func;
};


template< typename T > struct checked_deleter
{
public:
template< typename T > operator()( T* t )
{
size_t z = sizeof( T );
delete t;
}
};


class shared_ptr_base
{
private:
sp_counted_base * counter;


protected:
shared_ptr_base() : counter( 0 ) {}


explicit shared_ptr_base( sp_counter_base * c ) : counter( c ) {}


~shared_ptr_base()
{
if( counter )
counter->decref();
}


shared_ptr_base( shared_ptr_base const& other )
: counter( other.counter )
{
if( counter )
counter->addref();
}


shared_ptr_base& operator=( shared_ptr_base& const other )
{
shared_ptr_base temp( other );
std::swap( counter, temp.counter );
}


// other methods such as reset
};

现在我将创建两个“空闲”函数 make _ sp _ count _ impl,它将返回一个指向新创建的函数的指针。

template< typename T, typename F >
sp_counted_impl<T> * make_sp_counted_impl( T* ptr, F func )
{
try
{
return new sp_counted_impl( ptr, func );
}
catch( ... ) // in case the new above fails
{
func( ptr ); // we have to clean up the pointer now and rethrow
throw;
}
}


template< typename T >
sp_counted_impl<T> * make_sp_counted_impl( T* ptr )
{
return make_sp_counted_impl( ptr, checked_deleter<T>() );
}

好的,这两个函数对于通过模板化函数创建 share _ ptr 时接下来会发生什么非常重要。

template< typename T >
class shared_ptr : public shared_ptr_base
{


public:
template < typename U >
explicit shared_ptr( U * ptr ) :
shared_ptr_base( make_sp_counted_impl( ptr ) )
{
}


// implement the rest of shared_ptr, e.g. operator*, operator->
};

注意如果 T 为 void 并且 U 是您的“ test”类会发生什么。它将使用指向 U 的指针而不是指向 T 的指针调用 make _ sp _ count _ impl ()。破坏的管理都是通过这里完成的。Share _ ptr _ base 类管理与复制和赋值等相关的引用计数。Share _ ptr 类本身管理运算符重载(- > ,* etc)的类型安全使用。

因此,虽然您有一个 share _ ptr 要 void,但是在下面您管理的是您传递到 new 的类型的指针。请注意,如果在将指针放入 share _ ptr 之前将其转换为 void * ,那么它将无法在 check _ delete 上编译,因此您在那里实际上也是安全的。