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


129

我发现一些代码使用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的内部结构,其他实现实际上会破坏此代码吗?


2
您期望发生什么?
Lightness Races in Orbit

1
那里没有强制转换-这是从shared_ptr <test>到shared_ptr <void>的转换。
艾伦·斯托克斯

仅供参考:这是MSDN中有关std :: shared_ptr的文章的链接:msdn.microsoft.com/en-us/library/bb982026.aspx,这是来自GCC的文档:gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser 2011年

Answers:


98

诀窍是std::shared_ptr执行类型擦除。基本上,shared_ptr创建新deleter函数时,它将在内部存储一个函数(可以将其作为构造函数的参数提供,但如果不存在,则默认为call 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从另一个复制(或默认构造)一个a时,删除器会被传递,因此当您shared_ptr<T>从构造一个a时shared_ptr<U>,关于要调用的析构函数的信息也会被传递deleter


似乎有误印:my_shared。我会解决此问题,但还没有编辑权限。
阿列克谢·库卡诺夫

@Alexey Kukanov,@ Dennis Zickefoose:感谢您的修改,我当时不在,看不到它。
大卫·罗德里格斯(DavidRodríguez)-dribeas 2011年

2
@ user102008不需要'std :: function',但是它有点灵活(在这里可能一点都不重要),但是如果将'delete_deleter <T>'存储为函数指针'void(void *)'在那里执行类型擦除:T已从存储的指针类型中删除。
DavidRodríguez-dribeas

1
这种行为由C ++标准保证,对吗?我需要在一个类中进行类型擦除,并且std::shared_ptr<void>让我避免声明一个无用的包装器类,以便可以从某个基类继承它。
紫罗兰色长颈鹿2014年

1
@AngelusMortis:确切的删除器不是的类型的一部分my_unique_ptr。当在main模板中实例化时,double选择了正确的删除器,但这不是该类型的一部分,my_unique_ptr因此无法从对象中检索。当一个函数接收到一个(例如,通过右值引用)函数时,该删除器的类型就会从对象中删除my_unique_ptr,该函数不需要并且不需要知道删除器是什么。
大卫·罗德里格斯(DavidRodríguez)-dribeas

35

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

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

您的删除器功能shared_ptr<Test>给你构建的方式,是正常的一个Test,该指针转换为Test*delete这样。

当您将推shared_ptr<Test>入的向量时,尽管第一个转换为shared_ptr<void>这两个都将被复制void*

因此,当向量元素在使用最后一个引用被破坏时,它将指针传递给正确删除它的删除器。

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

[*]从逻辑上讲,它可以访问它们-它们可能不是shared_ptr本身的成员,而是它指向的某些管理节点。


2
+1表示删除功能/功能复制到其他shared_ptr实例中-其他答案中缺少一条信息。
阿列克谢·库卡诺夫

这是否意味着在使用shared_ptrs时不需要虚拟基础析构函数?
罗纳格2011年

@ronag是的。但是,我仍然建议将析构函数虚拟化,至少在您有任何其他虚拟成员的情况下。(一次忘记忘记的痛苦超过了可能带来的好处。)
艾伦·斯托克斯

是的,我同意。很有意思。我知道类型擦除只是没有考虑它的“功能”。
罗纳格2011年

2
@ronag:如果shared_ptr直接使用适当的类型创建或使用,则不需要虚拟析构函数make_shared。但是,仍然是一个好主意,因为指针的类型可以从构造更改,直到将其存储在shared_ptrbase *p = new derived; shared_ptr<base> sp(p);中为止(shared_ptr就对象base不是而言)derived,因此您需要一个虚拟析构函数。例如,此模式可能与工厂模式相同。
大卫·罗德里格斯(DavidRodríguez)-dribeas,2011年

10

之所以有效,是因为它使用类型擦除。

基本上,当您构建时shared_ptr,它会传递一个额外的参数(您可以根据需要实际提供),即删除器函子。

此默认函子接受您在中使用的类型的指针作为参数shared_ptr,因此void在此处,将其适当地转换为您test在此处使用的静态类型,并在此对象上调用析构函数。

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


5

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

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

的确,规范要求接收shared_ptr<T>对象接受shared_ptr<U>对象必须为true并且U*必须隐式转换为a T*,这确实是这种情况,T=void因为任何指针都可以void*隐式转换为。没有任何关于删除程序的说明,因为删除程序将是无效的,因此确实规范要求它可以正常工作。

从技术上讲,IIRC a shared_ptr<T>包含指向包含参考计数器的隐藏对象的指针和指向实际对象的指针;通过将删除器存储在此隐藏结构中,可以使此看似魔术的功能正常工作,同时仍保持shared_ptr<T>与常规指针一样大的大小(但是,取消引用指针需要双重定向)

shared_ptr -> hidden_refcounted_object -> real_object

3

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


你能解释更多吗?我刚才也发布了类似的问题,如果您能提供帮助,那就太好了!
布鲁斯

3

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

首先,我将介绍一些辅助类,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
};

现在,我将创建两个名为make_sp_counted_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>() );
}

好的,这两个函数对于通过模板函数创建shared_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为空并且U是您的“测试”类,那么上面发生的事情。它将使用指向U的指针而不是指向T的指针来调用make_sp_counted_impl()。销毁的管理全部通过此处完成。shared_ptr_base类管理有关复制和赋值等的引用计数。shared_ptr类本身管理操作符重载(->,*等)的类型安全使用。

因此,尽管您有一个shared_ptr要作废,但是在下面,您正在管理传递给new的类型的指针。请注意,如果在将指针转换为void *并将其放入shared_ptr之前,它将无法在checked_delete上进行编译,因此实际上也很安全。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.