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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/5913396/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 19:09:14  来源:igfitidea点击:

Why do std::shared_ptr<void> work

c++c++11shared-ptr

提问by LiKao

I found some code using std::shared_ptr to perform arbitrary cleanup at shutdown. At first I thought this code could not possibly work, but then I tried the following:

我发现一些代码使用 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;
}

This program gives the output:

这个程序给出了输出:

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

I have some ideas on why this might work, that have to do with the internals of std::shared_ptrs as implemented for G++. Since these objects wrap the internal pointer together with the counter the cast from std::shared_ptr<test>to std::shared_ptr<void>is probably not hindering the call of the destructor. Is this assumption correct?

我有一些关于为什么这可能起作用的想法,这与为 G++ 实现的 std::shared_ptrs 的内部结构有关。由于这些对象将内部指针与计数器包装在一起,因此从std::shared_ptr<test>to 的std::shared_ptr<void>强制转换可能不会妨碍析构函数的调用。这个假设正确吗?

And of course the much more important question: Is this guaranteed to work by the standard, or might further changes to the internals of std::shared_ptr, other implementations actually break this code?

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

采纳答案by David Rodríguez - dribeas

The trick is that std::shared_ptrperforms type erasure. Basically, when a new shared_ptris created it will store internally a deleterfunction (which can be given as argument to the constructor but if not present defaults to calling delete). When the shared_ptris destroyed, it calls that stored function and that will call the deleter.

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

A simple sketch of the type erasure that is going on simplified with std::function, and avoiding all reference counting and other issues can be seen here:

使用 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)

When a shared_ptris copied (or default constructed) from another the deleter is passed around, so that when you construct a shared_ptr<T>from a shared_ptr<U>the information on what destructor to call is also passed around in the deleter.

当 ashared_ptr从另一个复制(或默认构造)时,删除器会被传递,因此当您shared_ptr<T>从 a构造 a时shared_ptr<U>,有关要调用的析构函数的信息也会在deleter.

回答by Steve Jessop

shared_ptr<T>logically[*] has (at least) two relevant data members:

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

  • a pointer to the object being managed
  • a pointer to the deleter function that will be used to destroy it.
  • 指向被管理对象的指针
  • 指向将用于销毁它的删除器函数的指针。

The deleter function of your shared_ptr<Test>, given the way you constructed it, is the normal one for Test, which converts the pointer to Test*and deletes it.

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

When you push your shared_ptr<Test>into the vector of shared_ptr<void>, bothof those are copied, although the first one is converted to void*.

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

So, when the vector element is destroyed taking the last reference with it, it passes the pointer to a deleter that destroys it correctly.

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

It's actually a little more complicated than this, because shared_ptrcan take a deleter functorrather than just a function, so there might even be per-object data to be stored rather than just a function pointer. But for this case there is no such extra data, it would be sufficient just to store a pointer to an instantiation of a template function, with a template parameter that captures the type through which the pointer must be deleted.

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

[*] logically in the sense that it has access to them - they may not be members of the shared_ptr itself but instead of some management node that it points to.

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

回答by Matthieu M.

It works because it uses type erasure.

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

Basically, when you build a shared_ptr, it passes one extra argument (that you can actually provide if you wish), which is the deleter functor.

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

This default functor accepts as argument a pointer to type you use in the shared_ptr, thus voidhere, casts it appropriately to the static type you used testhere, and calls the destructor on this object.

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

Any sufficiently advanced science feels like magic, isn't it ?

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

回答by 6502

The constructor shared_ptr<T>(Y *p)indeed seems to be calling shared_ptr<T>(Y *p, D d)where dis an automatically generated deleter for the object.

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

When this happens the type of the object Yis known, so the deleter for this shared_ptrobject knows which destructor to call and this information is not lost when the pointer is the stored in a vector of shared_ptr<void>.

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

Indeed the specs require that for a receving shared_ptr<T>object to accept a shared_ptr<U>object it must be true that and U*must be implicitly convertible to a T*and this is certainly the case with T=voidbecause any pointer can be converted to a void*implicitly. Nothing is said about the deleter that will be invalid so indeed the specs are mandating that this will work correctly.

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

Technically IIRC a shared_ptr<T>holds a pointer to an hidden object that contains the reference counter and a pointer to the actual object; by storing the deleter in this hidden structure it's possible to make this apparently magic feature working while still keeping shared_ptr<T>as big as a regular pointer (however dereferencing the pointer requires a double indirection

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

shared_ptr -> hidden_refcounted_object -> real_object

回答by CashCow

I am going to answer this question (2 years later) using a very simplistic implementation of shared_ptr that the user will understand.

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

Firstly I am going to a few side classes, shared_ptr_base, sp_counted_base sp_counted_impl, and checked_deleter the last of which is a template.

首先,我将介绍几个副类,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
};

Now I am going to create two "free" function called make_sp_counted_impl which will return a pointer to a newly created one.

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

Ok, these two functions are essential as to what will happen next when you create a shared_ptr through a templated function.

好的,当您通过模板化函数创建 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->
};

Note what happens above if T is void and U is your "test" class. It will call make_sp_counted_impl() with a pointer to U, not a pointer to T. The management of the destruction is all done through here. The shared_ptr_base class manages the reference counting with regards to copying and assignment etc. The shared_ptr class itself manages the typesafe use of operator overloads (->, * etc).

请注意如果 T 为空并且 U 是您的“测试”类,上面会发生什么。它将调用make_sp_counted_impl() 并带有指向U 的指针,而不是指向T 的指针。销毁的管理全部通过这里完成。shared_ptr_base 类管理与复制和赋值等相关的引用计数。 shared_ptr 类本身管理运算符重载(->、* 等)的类型安全使用。

Thus although you have a shared_ptr to void, underneath you are managing a pointer of the type you passed into new. Note that if you convert your pointer to a void* before putting it into the shared_ptr, it will fail to compile on the checked_delete so you are actually safe there too.

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

回答by Puppy

Test*is implicitly convertible to void*, therefore shared_ptr<Test>is implicitly convertible to shared_ptr<void>, from memory. This works because shared_ptris designed to control destruction at run-time, not compile-time, they will internally use inheritance to call the appropriate destructor as it was at allocation time.

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