【问题标题】:Why do std::shared_ptr<void> work为什么 std::shared_ptr<void> 工作
【发布时间】:2011-08-20 06:45:27
【问题描述】:

我发现一些代码使用 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&lt;test&gt;std::shared_ptr&lt;void&gt; 的强制转换可能不会妨碍析构函数的调用。这个假设正确吗?

当然还有更重要的问题:这是否保证符合标准,或者可能进一步更改 std::shared_ptr 的内部结构,其他实现实际上会破坏此代码?

【问题讨论】:

标签: c++ c++11 shared-ptr


【解决方案1】:

诀窍是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&lt;U&gt; 构造shared_ptr&lt;T&gt; 时,有关要调用的析构函数的信息也会在deleter.

【讨论】:

  • 好像有错字:my_shared。我会解决这个问题,但还没有编辑权限。
  • @Alexey Kukanov、@Dennis Zickefoose:感谢编辑,我不在,没看到。
  • @user102008 你不需要 'std::function' 但它更灵活一点(可能在这里根本不重要),但这不会改变类型擦除的工作方式,如果你store 'delete_deleter' as the function pointer 'void (void*)' 你正在那里执行类型擦除:T 从存储的指针类型中消失了。
  • 这种行为是由 C++ 标准保证的,对吧?我需要在我的一个类中进行类型擦除,std::shared_ptr&lt;void&gt; 让我避免声明一个无用的包装类,以便我可以从某个基类继承它。
  • @AngelusMortis:确切的删除器不是my_unique_ptr 类型的一部分。当在main 中使用double 实例化模板时,选择了正确的删除器但是这不是my_unique_ptr 类型的一部分,并且无法从对象中检索。删除器的类型是从对象中擦除,当函数接收到my_unique_ptr(例如通过右值引用)时,该函数不知道也不需要知道删除器是什么。
【解决方案2】:

shared_ptr&lt;T&gt;logically[*] 有(至少)两个相关数据成员:

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

你的shared_ptr&lt;Test&gt; 的删除函数,考虑到你构造它的方式,是Test 的普通函数,它将指针转换为Test*deletes。

当您将shared_ptr&lt;Test&gt; 推入shared_ptr&lt;void&gt; 的向量中时,两个都会被复制,尽管第一个会被转换为void*

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

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

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

【讨论】:

  • +1 提到删除函数/函子被复制到其他 shared_ptr 实例中 - 其他答案中遗漏了一条信息。
  • 这是否意味着在使用 shared_ptrs 时不需要虚拟基析构函数?
  • @ronag 是的。但是,我仍然建议将析构函数设为虚拟,至少如果您有任何其他虚拟成员。 (不小心忘记一次的痛苦超过了任何可能的好处。)
  • 是的,我同意。非常有趣。我知道类型擦除只是没有考虑到它的这个“功能”。
  • @ronag:如果您使用适当的类型直接创建shared_ptr 或使用make_shared,则不需要虚拟析构函数。但是,它仍然是一个好主意,因为指针的类型可以从构造中更改,直到它存储在shared_ptrbase *p = new derived; shared_ptr&lt;base&gt; sp(p); 中,就shared_ptr 而言,对象是base 而不是derived ,所以你需要一个虚拟析构函数。例如,这种模式在工厂模式中很常见。
【解决方案3】:

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

基本上,当您构建shared_ptr 时,它会传递一个额外的参数(如果您愿意,您可以实际提供),即删除函子。

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

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

【讨论】:

    【解决方案4】:

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

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

    确实,规范要求接收shared_ptr&lt;T&gt; 对象接受shared_ptr&lt;U&gt; 对象必须是真的,并且U* 必须隐式转换为T*T=void 肯定是这种情况因为任何指针都可以隐式转换为void*。没有任何关于删除器无效的说明,因此规范确实要求这将正常工作。

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

    shared_ptr -> hidden_refcounted_object -> real_object
    

    【讨论】:

      【解决方案5】:

      Test* 可以隐式转换为void*,因此shared_ptr&lt;Test&gt; 可以从内存中隐式转换为shared_ptr&lt;void&gt;。这是有效的,因为shared_ptr 旨在控制运行时的销毁,而不是编译时,它们将在内部使用继承来调用适当的析构函数,就像在分配时一样。

      【讨论】:

      • 你能解释更多吗?我刚才也发了一个类似的问题,如果你能帮忙就太好了!
      【解决方案6】:

      我将使用用户能够理解的非常简单的 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 是您的“测试”类,请注意上面会发生什么。它将使用指向 U 的指针而不是指向 T 的指针调用 make_sp_counted_impl()。销毁的管理都通过这里完成。 shared_ptr_base 类管理有关复制和赋值等的引用计数。shared_ptr 类本身管理运算符重载(->、* 等)的类型安全使用。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-08-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-06-11
        相关资源
        最近更新 更多