Std :: shared_ptr <void> neden çalışır?


129

Kapatma sırasında rastgele temizleme gerçekleştirmek için std :: shared_ptr kullanarak bazı kodlar buldum. İlk başta bu kodun işe yaramayacağını düşündüm, ama sonra aşağıdakileri denedim:

#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;
}

Bu program çıktıyı verir:

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

Bunun neden işe yarayabileceğine dair bazı fikirlerim var, bu G ++ için uygulandığı haliyle std :: shared_ptrs'nin içsel öğeleriyle ilgili. Bu nesneler gelen sayaç ile döküm iç işaretçi buluşmanızı sarmak yana std::shared_ptr<test>kadar std::shared_ptr<void>muhtemelen yıkıcı çağrı engel değildir. Bu varsayım doğru mu?

Ve elbette çok daha önemli olan soru: Bunun standart tarafından çalışması garanti ediliyor mu, yoksa std :: shared_ptr'nin iç kısımlarında başka değişiklikler olabilir, diğer uygulamalar bu kodu gerçekten bozabilir mi?


2
Bunun yerine ne olmasını bekliyordunuz?
Orbit'te Hafiflik Yarışları

1
Orada yayınlama yok - bu, shared_ptr <test> 'den shared_ptr <void>' e bir dönüşümdür.
Alan Stokes

Bilginize: İşte MSDN'de std :: shared_ptr ile ilgili bir makalenin bağlantısı: msdn.microsoft.com/en-us/library/bb982026.aspx ve bu GCC'deki
yasouser

Yanıtlar:


99

İşin püf noktası, std::shared_ptrtür silme işlemi gerçekleştirmesidir. Temel olarak, bir yeni shared_ptroluşturulduğunda, dahili olarak bir deleterişlevi depolar (bu, kurucuya argüman olarak verilebilir, ancak mevcut değilse, çağrının varsayılan değerleri delete). Ne zaman shared_ptryok edilir, bu depolanmış işlevini çağırır ve bu arayacak deleter.

Std :: function ile basitleştirilmiş ve tüm referans sayma ve diğer sorunlardan kaçınan basit bir tür silme taslağı burada görülebilir:

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)

Bir zaman shared_ptrbaşka kopyalanan (veya varsayılan inşa) olan deleter bir inşa zaman böylece etrafında geçirilen shared_ptr<T>bir gelen shared_ptr<U>çağrıya yıkıcı da etrafında geçirilen hakkında bilgi deleter.


Bir baskı hatası var gibi görünüyor: my_shared. Bunu düzeltirdim ama henüz düzenleme yetkim yok.
Alexey Kukanov

@Alexey Kukanov, @Dennis Zickefoose: Düzenleme için teşekkürler, uzaktaydım ve görmedim.
David Rodríguez - dribeas

2
@ user102008 'std :: function'a ihtiyacınız yok, ancak biraz daha esnektir (muhtemelen burada hiç önemi yoktur), ancak bu,' delete_deleter <T> 'olarak saklarsanız, tür silme işleminin nasıl çalıştığını 'void (void *)' işlev işaretçisi orada tür silme işlemini gerçekleştiriyorsunuz: T, saklanan işaretçi türünden gitmiştir.
David Rodríguez - dribeas

1
Bu davranış C ++ standardı tarafından garanti edilir, değil mi? Sınıflarımdan birinde tür silme işlemine ihtiyacım var ve std::shared_ptr<void>belirli bir temel sınıftan miras alabilmek için işe yaramaz bir sarmalayıcı sınıf bildirmekten kaçınmama izin veriyor.
Violet Giraffe

1
@AngelusMortis: Tam silme, türünün bir parçası değildir my_unique_ptr. İçinde zaman mainşablonla örneği doubleseçilir sağ deleter ancak bu türü bir parçası değildir my_unique_ptrve nesneden alınamaz. Bir işlev bir (örneğin rvalue-referansı ile) aldığında , silenin türü nesneden silinirmy_unique_ptr , bu işlev silicinin ne olduğunu bilmiyor ve bilmeye ihtiyaç duymuyor.
David Rodríguez - dribeas

35

shared_ptr<T> mantıksal olarak [*], (en az) iki ilgili veri üyesine sahiptir:

  • yönetilen nesneye bir işaretçi
  • onu yok etmek için kullanılacak silme işlevine bir işaretçi.

Aramalarınızdan deleter fonksiyonu shared_ptr<Test>bunu inşa yolu verilen, normal biridir Testişaretçiyi dönüştürür, Test*ve deleteböyle.

Your shared_ptr<Test>vektörüne ittiğinizde shared_ptr<void>, her ikisi de kopyalanır, ancak ilki dönüştürülür void*.

Böylece, vektör elemanı son referans alınarak yok edildiğinde, işaretçiyi doğru bir şekilde yok eden bir siliciye aktarır.

Çünkü aslında biraz daha bundan daha karışık bir shared_ptrbir deleter alabilir functor sadece bir fonksiyonu yerine, bu yüzden bile sadece bir işlev işaretçisi yerine saklanmasına nesne başına veri olabilir. Ancak bu durumda, bu tür fazladan veri yoktur, sadece işaretçinin silinmesi gereken türü yakalayan bir şablon parametresi ile bir şablon işlevinin somutlaştırılmasına bir işaretçi depolamak yeterli olacaktır.

[*] mantıksal olarak onlara erişebilmesi anlamında - paylaşılan_tr'nin kendisinin üyesi değil, işaret ettiği bazı yönetim düğümü yerine olabilirler.


2
Silme işlevinin / functor'un diğer paylaşılan_ptr örneklerine kopyalandığını belirtmek için +1 - diğer yanıtlarda kaçırılan bir bilgi parçası.
Alexey Kukanov

Bu, shared_ptrs kullanılırken sanal temel yıkıcıların gerekli olmadığı anlamına mı geliyor?
52'de ronag

@ronag Evet. Bununla birlikte, en azından başka sanal üyeleriniz varsa, yine de yıkıcıyı sanal hale getirmenizi tavsiye ederim. (Yanlışlıkla unutmanın
Alan Stokes

Evet, katılıyorum. Az olmayan ilginç. Yazı silmesinin bu "özelliği" ni dikkate almadığını biliyordum.
ronag

2
@ronag: Sanal yıkıcılar, shared_ptrdoğrudan uygun türle oluşturursanız veya kullanırsanız gerekli değildir make_shared. Ancak yine de, işaretçinin türü yapımdan şurada saklanana kadar değişebildiği için iyi bir fikirdir shared_ptr: söz konusu base *p = new derived; shared_ptr<base> sp(p);olduğu sürece shared_ptrnesne basedeğildir derived, bu nedenle sanal bir yıkıcıya ihtiyacınız vardır. Bu model, örneğin fabrika modellerinde ortak olabilir.
David Rodríguez - dribeas

10

Tür silmeyi kullandığı için çalışır.

Temel olarak, bir a shared_ptroluşturduğunuzda, silme işlevi olan fazladan bir argüman (isterseniz gerçekten sağlayabilirsiniz) iletir.

Bu varsayılan işlev, içinde kullandığınız yazım için bir işaretçi bağımsız değişken olarak kabul eder shared_ptr, dolayısıyla voidburada, onu testburada kullandığınız statik türe uygun şekilde çevirir ve bu nesnede yıkıcıyı çağırır.

Yeterince gelişmiş bir bilim sihir gibi hissediyor, değil mi?


5

Yapıcı shared_ptr<T>(Y *p)gerçekten de nesne için otomatik olarak oluşturulan bir silici shared_ptr<T>(Y *p, D d)nerede dolduğunu çağırıyor gibi görünüyor .

Bu gerçekleştiğinde nesnenin Ytürü bilinir, bu nedenle bu shared_ptrnesnenin silici, hangi yıkıcıyı çağıracağını bilir ve işaretçi bir vektöründe depolandığında bu bilgi kaybolmaz shared_ptr<void>.

Nitekim, spesifikasyonlar, alıcı shared_ptr<T>nesnenin bir shared_ptr<U>nesneyi kabul etmesi için doğru olması ve U*dolaylı olarak a'ya dönüştürülebilir olması gerektiğini gerektirir T*ve bu kesinlikle durumdur, T=voidçünkü herhangi bir işaretçi void*örtük olarak dönüştürülebilir . Silici hakkında geçersiz olacak hiçbir şey söylenmez, bu yüzden gerçekten de spesifikasyonlar bunun doğru çalışacağını zorunlu kılar.

Teknik olarak IIRC a shared_ptr<T>, referans sayacını içeren gizli bir nesneye bir işaretçi ve gerçek nesneye bir işaretçi tutar; Siliciyi bu gizli yapıda depolayarak, bu görünüşte sihirli özelliği shared_ptr<T>normal bir işaretçi kadar büyük tutarken çalışır hale getirmek mümkündür (bununla birlikte, işaretçinin referansını kaldırmak için çift yöneltme gerekir.

shared_ptr -> hidden_refcounted_object -> real_object

3

Test*örtük olarak dönüştürülebilir olduğunu void*, bu nedenle, shared_ptr<Test>örtük olarak dönüştürülebilir olan shared_ptr<void>bellekten,. Bu işe shared_ptryarar, çünkü derleme zamanında değil çalışma zamanında yıkımı kontrol etmek için tasarlanmıştır, dahili olarak mirası ayırma zamanında olduğu gibi uygun yıkıcıyı çağırmak için kullanır.


Biraz daha fazla açıklayabilir misin? Az önce benzer bir soru yayınladım, yardım edebilirseniz çok iyi olur!
Bruce

3

Bu soruyu (2 yıl sonra) kullanıcının anlayacağı çok basit bir shared_ptr uygulaması kullanarak cevaplayacağım.

İlk olarak, bir kaç yan sınıfa gidiyorum, shared_ptr_base, sp_counted_base sp_counted_impl ve sonuncusu bir şablon olan check_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
};

Şimdi make_sp_counted_impl adında yeni oluşturulmuş bir göstericiye bir işaretçi döndürecek iki "özgür" işlev oluşturacağım.

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

Tamam, bu iki işlev, şablonlu bir işlev aracılığıyla bir shared_ptr oluşturduğunuzda daha sonra ne olacağı konusunda çok önemlidir.

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 geçersizse ve U sizin "test" sınıfınızsa yukarıda ne olduğuna dikkat edin. Make_sp_counted_impl () 'yi T'ye değil, U'ya bir gösterici ile çağıracaktır. İmha yönetiminin tamamı buradan yapılır. Shared_ptr_base sınıfı, kopyalama ve atama vb. İle ilgili olarak referans sayımını yönetir. Shared_ptr sınıfının kendisi, operatör aşırı yüklerinin (->, * vb.) Tip güvenli kullanımını yönetir.

Böylece, geçersiz kılmak için bir shared_ptr'ye sahip olmanıza rağmen, altınızda yeniye aktardığınız türden bir işaretçiyi yönetiyorsunuz. İşaretçinizi shared_ptr'ye koymadan önce void * 'e dönüştürürseniz, check in_delete'de derlenemeyeceğini ve böylece gerçekten güvende olacağınızı unutmayın.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.