为什么 std::shared_ptr<void> 能够工作?

138

我在stackoverflow上找到一些使用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>。 - Alan Stokes
FYI:这是有关 MSDN 中 std::shared_ptr 的文章链接:http://msdn.microsoft.com/en-us/library/bb982026.aspx,以及来自 GCC 的文档:http://gcc.gnu.org/onlinedocs/libstdc++/latest-doxygen/a00267.html。 - yasouser
6个回答

113
这个技巧在于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从另一个对象复制(或默认构造)时,deleter也会被传递,因此当您从shared_ptr<U>构造shared_ptr<T>时,有关要调用哪个析构函数的信息也会在deleter中传递。


2
@user102008,您不需要‘std::function’,但它可能会更灵活一些(这里可能完全无所谓),但这并不会改变类型擦除的工作方式,如果您将‘delete_deleter<T>’存储为函数指针‘void( void* )’,则在此处执行了类型擦除:T已经从存储指针类型中消失了。 - David Rodríguez - dribeas
@DavidRodríguez-dribeas 我刚刚发布了一个类似的问题,如果您能帮忙就太好了! - Bruce
1
这种行为是由C++标准保证的,对吧?我需要在我的一个类中进行类型抹除,而std::shared_ptr<void>让我避免了声明一个无用的包装类,以便我可以从某个基类继承它。 - Violet Giraffe
先生@DavidRodríguez-dribeas,我对类型抹除不太熟悉,您可以告诉我在您的代码中确切发生了什么类型抹除吗?谢谢。 - Angelus Mortis
1
@AngelusMortis:确切的删除器不是 my_unique_ptr 类型的一部分。当在 main 中使用 double 实例化模板时,选择了正确的删除器,但这并不是 my_unique_ptr 类型的一部分,并且无法从对象中检索到。当函数接收到 my_unique_ptr(例如右值引用)时,删除器的类型从该对象中被 擦除,该函数不需要知道并且也不需要知道删除器是什么。 - David Rodríguez - dribeas
显示剩余3条评论

38

shared_ptr<T> 中有(至少)两个相关数据成员:

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

对于你构造的shared_ptr<Test>,它的删除器函数是正常的Test 删除器函数,该函数会将指针转换为 Test* 并进行delete 操作。

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

因此,当向量元素被销毁并且最后一份引用也随之消失时,它会传递指向正确销毁指针的删除器。

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

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


4
提到 deleter 函数/函数对象会被复制到其他 shared_ptr 实例中得到了加分,这是其他答案中遗漏的一条信息。 - Alexey Kukanov
这是否意味着在使用shared_ptrs时不需要虚基类析构函数? - ronag
@ronag 是的。但是,我仍然建议将析构函数设置为虚函数,至少如果你有任何其他虚成员的话。(一次意外遗忘所带来的困扰,超过了任何可能的好处。) - Alan Stokes
是的,我同意。尽管如此,还是很有趣的。我知道类型擦除,只是没有考虑到它的这个“特性”。 - ronag
3
如果您直接使用适当类型创建 shared_ptr 或使用 make_shared,则不需要虚析构函数。但是,仍然建议使用虚析构函数,因为指针类型可能会从构造到存储在 shared_ptr 之间发生变化。例如,在工厂模式中,您可能会使用以下模式:base *p = new derived; shared_ptr<base> sp(p);,在 shared_ptr 的视角中,对象是“base”而不是“derived”,因此需要虚拟析构函数。 - David Rodríguez - dribeas

11
它之所以有效,是因为它使用了类型擦除。
基本上,在构建一个shared_ptr时,它会传递一个额外的参数(如果需要,您实际上可以提供这个参数),它是deleter函数对象。
这个默认函数对象接受你在shared_ptr中使用的指针类型作为参数,这里是void,然后适当地将其转换为你在这里使用的静态类型test,并在该对象上调用析构函数。
任何足够先进的科学都感觉像魔法,不是吗?

5

我将使用一种非常简单的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 是 void 而 U 是你的“test”类,上面的内容会发生什么。它将使用指向 U 而不是指向 T 的指针调用 make_sp_counted_impl()。所有的销毁管理都通过此处完成。shared_ptr_base 类管理与复制和赋值等相关的引用计数。shared_ptr 类本身管理操作符重载(->、* 等)的类型安全使用。
因此,尽管您拥有一个指向 void 的 shared_ptr,在底层,您正在管理传递到 new 中的类型指针。请注意,如果在将指针放入 shared_ptr 之前将其转换为 void*,它将无法在 checked_delete 上编译,所以你实际上也是安全的。

5
构造函数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

4

Test*可以隐式转换为void*,因此shared_ptr<Test>可以隐式转换为shared_ptr<void>,从而实现内存共享。这是因为shared_ptr的设计是在运行时控制销毁,而不是编译时,在分配时它们将内部使用继承来调用适当的析构函数。


你能解释得更详细一些吗?我刚刚发布了一个类似的问题,如果你能帮忙就太好了! - Bruce

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接