为什么std :: shared_ptr <void>工作

我发现一些代码使用std :: shared_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 :: shared_ptrs的内部结构有关。 由于这些对象将内部指针与计数器一起包装,从std::shared_ptr<test>std::shared_ptr<void>可能不会妨碍析构函数的调用。 这个假设是否正确?

当然还有更重要的问题:这是保证按标准工作,还是可能会进一步改变std :: shared_ptr的内部,其他的实现实际上破坏了这个代码?

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

types删除的一个简单的草图,用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被复制(或默认构造)从另一个deleter被传递时,所以当你从shared_ptr<U>构造一个shared_ptr<T> ,关于什么析构函数被调用的信息也被传递给了deleter

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

  • 一个指向正在被pipe理的对象的指针
  • 一个指向删除函数的指针,将被用来销毁它。

您的shared_ptr<Test>的deleter函数,按照您构build的方式,是Test的正常工具,它将指针转换为Test*并将其delete

当你将你的shared_ptr<Test>推入shared_ptr<void>的向量时,虽然第一个被转换为void* ,但是这两个都被复制了。

所以,当vector元素被最后一个引用所销毁时,它将指针传递给一个正确销毁它的删除器。

它实际上比这更复杂一点,因为shared_ptr可以使用删除函子而不仅仅是一个函数,所以甚至可以存储每个对象的数据,而不仅仅是一个函数指针。 但是对于这种情况,不存在这样的额外数据,仅仅存储指向模板函数实例化的指针就足够了,模板参数捕获指针必须被删除的types。

从逻辑上来说,它有权访问它们 – 它们可能不是shared_ptr本身的成员,而不是它指向的某个pipe理节点。

这是因为它使用types擦除。

基本上,当你build立一个shared_ptr ,它会传递一个额外的参数(如果你愿意的话,你可以提供这个参数),这就是删除函数。

这个默认的函子接受一个指向你在shared_ptr使用的types的指针,因此在这里是void ,将它转换为你在这里使用的test的静态types,然后调用这个对象的析构函数。

任何足够先进的科学都感觉像魔术,不是吗?

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

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

事实上,规范要求对于接收shared_ptr<T>对象来接受shared_ptr<U>对象,它必须是真的,并且U*必须隐含地转换为T*T=void是肯定的,因为任何指针可以隐式转换为void* 。 没有关于删除器的说法,这将是无效的,所以确实规范要求,这将正常工作。

技术上来说,IIRC一个shared_ptr<T>包含一个指向隐藏对象的指针,该对象包含引用计数器和一个指向实际对象的指针; 通过将删除器存储在这个隐藏的结构中,可以使这个看起来很神奇的function工作,同时仍然保持与常规指针一样大的shared_ptr<T> (不过,解引用指针需要双重间接

 shared_ptr -> hidden_refcounted_object -> real_object 

我将使用用户将理解的shared_ptr的一个非常简单的实现来回答这个问题(2年后)。

首先我要去几个side类,shared_ptr_base,sp_counted_base,sp_counted_impl和checked_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 }; 

现在我要创build两个名为make_sp_counted_impl的“free”函数,它将返回一个指向新创build的指针的指针。

 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>() ); } 

好的,这两个函数对于通过模板化函数创buildshared_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, eg operator*, operator-> }; 

注意,如果T是无效的,U是你的“testing”类,那么上面会发生什么。 它将使用指向U的指针调用make_sp_counted_impl(),而不是指向T的指针。销毁的pipe理全部通过这里完成。 shared_ptr_base类pipe理着关于复制和赋值等的引用计数。shared_ptr类本身pipe理操作符重载( – >,*等)的types安全使用。

因此,虽然你有一个shared_ptr无效,在你下面pipe理你传入新的types的指针。 请注意,如果在将指针放入shared_ptr之前将指针转换为void *,则它将无法在checked_delete上进行编译,因此您实际上也很安全。

Test*可以隐式转换为void* ,因此shared_ptr<Test>可以从内存隐式转换为shared_ptr<void> 。 这是shared_ptr的,因为shared_ptr被devise为在运行时控制销毁,而不是编译时,它们将在内部使用inheritance来调用合适的析构函数,就像在分配时一样。