Mengapa std :: shared_ptr <void> berfungsi

129

Saya menemukan beberapa kode menggunakan std :: shared_ptr untuk melakukan pembersihan sembarang saat shutdown. Awalnya saya pikir kode ini tidak mungkin bekerja, tetapi kemudian saya mencoba yang berikut:

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

Program ini memberikan output:

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

Saya punya beberapa ide mengapa ini bisa berhasil, yang ada hubungannya dengan internal std :: shared_ptrs seperti yang diterapkan untuk G ++. Karena benda-benda ini membungkus pointer internal bersama-sama dengan counter gips dari std::shared_ptr<test>ke std::shared_ptr<void>mungkin tidak menghalangi panggilan destruktor. Apakah asumsi ini benar?

Dan tentu saja pertanyaan yang jauh lebih penting: Apakah ini dijamin bekerja dengan standar, atau mungkin perubahan lebih lanjut ke internal std :: shared_ptr, implementasi lain benar-benar memecahkan kode ini?

LiKao
sumber
2
Apa yang Anda harapkan terjadi?
Lightness Races in Orbit
1
Tidak ada pemeran di sana - ini adalah konversi dari shared_ptr <test> ke shared_ptr <void>.
Alan Stokes
FYI: inilah tautan ke artikel tentang std :: shared_ptr di MSDN: msdn.microsoft.com/en-us/library/bb982026.aspx dan ini adalah dokumentasi dari GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

Jawaban:

98

Kuncinya adalah std::shared_ptrmelakukan penghapusan tipe. Pada dasarnya, ketika baru shared_ptrdibuat itu akan menyimpan deleterfungsi internal (yang dapat diberikan sebagai argumen untuk konstruktor tetapi jika tidak ada default untuk panggilan delete). Ketika shared_ptrdihancurkan, itu memanggil fungsi yang disimpan dan yang akan memanggil deleter.

Sebuah sketsa sederhana dari penghapusan tipe yang terjadi disederhanakan dengan fungsi std ::, dan menghindari semua penghitungan referensi dan masalah lain dapat dilihat di sini:

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)

Ketika a shared_ptrdisalin (atau dibangun secara default) dari yang lain, deleter dilewatkan, sehingga ketika Anda membangun shared_ptr<T>dari shared_ptr<U>informasi tentang apa yang memanggil destruktor juga dilewatkan di deleter.

David Rodríguez - dribeas
sumber
Sepertinya ada salah cetak: my_shared. Saya akan memperbaikinya tetapi belum memiliki hak untuk mengedit.
Alexey Kukanov
@Alexey Kukanov, @Dennis Zickefoose: Terima kasih atas hasil editnya saya pergi dan tidak melihatnya.
David Rodríguez - dribeas
2
@ user102008 Anda tidak perlu 'std :: function' tetapi ini sedikit lebih fleksibel (mungkin tidak masalah sama sekali di sini), tetapi itu tidak mengubah cara kerja tipe penghapusan, jika Anda menyimpan 'delete_deleter <T>' sebagai pointer fungsi 'void (void *)' Anda melakukan penghapusan jenis di sana: T hilang dari jenis pointer yang disimpan.
David Rodríguez - dribeas
1
Perilaku ini dijamin oleh standar C ++, kan? Saya perlu mengetik penghapusan di salah satu kelas saya, dan std::shared_ptr<void>memungkinkan saya menghindari mendeklarasikan kelas pembungkus yang tidak berguna hanya supaya saya bisa mewarisinya dari kelas dasar tertentu.
Violet Giraffe
1
@AngelusMortis: Deleter yang tepat bukan bagian dari tipe my_unique_ptr. Ketika dalam maintemplate dipakai dengan doubledeleter kanan dipilih tapi ini bukan bagian dari jenis my_unique_ptrdan tidak dapat diambil dari objek. Jenis deleter dihapus dari objek, ketika suatu fungsi menerima my_unique_ptr(katakan dengan rvalue-reference), fungsi itu tidak dan tidak perlu tahu apa itu deleter.
David Rodríguez - dribeas
35

shared_ptr<T> secara logis [*] memiliki (setidaknya) dua anggota data yang relevan:

  • pointer ke objek yang sedang dikelola
  • pointer ke fungsi deleter yang akan digunakan untuk menghancurkannya.

Fungsi deleter dari Anda shared_ptr<Test>, mengingat cara Anda membuatnya, adalah fungsi normal untuk Test, yang mengubah pointer menjadi Test*dan deletemenggunakannya.

Ketika Anda mendorong Anda shared_ptr<Test>ke dalam vektor shared_ptr<void>, keduanya disalin, meskipun yang pertama dikonversi menjadi void*.

Jadi, ketika elemen vektor dihancurkan dengan mengambil referensi terakhir dengannya, ia melewatkan pointer ke deleter yang menghancurkannya dengan benar.

Ini sebenarnya sedikit lebih rumit dari ini, karena shared_ptrdapat mengambil Deleter functor bukan hanya fungsi, sehingga ada bahkan mungkin data per-objek yang akan disimpan bukan hanya fungsi pointer. Tetapi untuk kasus ini tidak ada data tambahan seperti itu, itu akan cukup hanya untuk menyimpan pointer ke instantiation dari fungsi template, dengan parameter template yang menangkap tipe di mana pointer harus dihapus.

[*] secara logis dalam arti bahwa ia memiliki akses ke mereka - mereka mungkin bukan anggota shared_ptr itu sendiri melainkan beberapa simpul manajemen yang ditunjuknya.

Steve Jessop
sumber
2
+1 untuk menyebutkan bahwa fungsi / functor deleter disalin ke instance shared_ptr lainnya - sepotong info terlewatkan dalam jawaban lain.
Alexey Kukanov
Apakah ini berarti destruktor basis virtual tidak diperlukan saat menggunakan shared_ptrs?
ronag
@ronag Ya. Namun, saya masih merekomendasikan untuk membuat destructor virtual, setidaknya jika Anda memiliki anggota virtual lainnya. (Rasa sakit karena lupa secara tidak sengaja melebihi manfaat yang mungkin ada.)
Alan Stokes
Ya, saya setuju. Menarik bukan-kurang. Saya tahu tentang jenis penghapusan hanya tidak mempertimbangkan "fitur" itu.
ronag
2
@ronag: penghancur virtual tidak diperlukan jika Anda membuat shared_ptrlangsung dengan tipe yang sesuai atau jika Anda gunakan make_shared. Tapi, tetap saja ide yang baik karena jenis pointer dapat berubah dari konstruksi sampai disimpan dalam shared_ptr: base *p = new derived; shared_ptr<base> sp(p);, sejauh shared_ptryang bersangkutan objek tersebut basetidak derived, sehingga Anda perlu destructor virtual. Pola ini bisa sama dengan pola pabrik, misalnya.
David Rodríguez - dribeas
10

Ini bekerja karena menggunakan tipe erasure.

Pada dasarnya, ketika Anda membangun shared_ptr, ia melewati satu argumen tambahan (yang sebenarnya bisa Anda berikan jika Anda mau), yang merupakan functor deleter.

Functor default ini menerima sebagai argumen penunjuk untuk mengetik Anda gunakan dalam shared_ptr, jadi di voidsini, melemparkannya dengan tepat ke tipe statis yang Anda gunakan di testsini, dan memanggil destruktor pada objek ini.

Adakah ilmu yang cukup maju terasa seperti sihir, bukan?

Matthieu M.
sumber
5

Konstruktor shared_ptr<T>(Y *p)memang tampaknya memanggil di shared_ptr<T>(Y *p, D d)mana ddeleter yang dihasilkan secara otomatis untuk objek.

Ketika ini terjadi, jenis objek Ydiketahui, sehingga deleter untuk shared_ptrobjek ini tahu destruktor mana yang harus dihubungi dan informasi ini tidak hilang ketika pointer disimpan dalam vektor shared_ptr<void>.

Memang spesifikasi mengharuskan agar shared_ptr<T>objek yang menerima menerima shared_ptr<U>objek, harus benar bahwa dan U*harus secara implisit dapat dikonversi ke a T*dan ini tentu saja terjadi T=voidkarena pointer apa pun dapat dikonversi menjadi void*implisit. Tidak ada yang dikatakan tentang deleter yang akan tidak valid sehingga memang spesifikasi memerintahkan bahwa ini akan berfungsi dengan benar.

Secara teknis IIRC a shared_ptr<T>memegang pointer ke objek tersembunyi yang berisi counter referensi dan pointer ke objek aktual; dengan menyimpan deleter dalam struktur tersembunyi ini dimungkinkan untuk membuat fitur yang tampaknya ajaib ini bekerja sambil tetap shared_ptr<T>sebesar pointer biasa (namun mendereferensi pointer membutuhkan petunjuk ganda)

shared_ptr -> hidden_refcounted_object -> real_object
6502
sumber
3

Test*secara implisit dapat dikonversi ke void*, oleh karena shared_ptr<Test>itu secara implisit dapat dikonversi ke shared_ptr<void>, dari memori. Ini berfungsi karena shared_ptrdirancang untuk mengontrol kehancuran pada saat run-time, bukan compile-time, mereka akan secara internal menggunakan pewarisan untuk memanggil destructor yang sesuai seperti pada waktu alokasi.

Anak anjing
sumber
Bisakah Anda menjelaskan lebih lanjut? Saya telah memposting pertanyaan yang sama barusan, akan lebih bagus jika Anda bisa membantu!
Bruce
3

Saya akan menjawab pertanyaan ini (2 tahun kemudian) menggunakan implementasi shared_ptr yang sangat sederhana yang akan dimengerti pengguna.

Pertama saya akan ke beberapa kelas sisi, shared_ptr_base, sp_counted_base sp_counted_impl, dan checked_deleter yang terakhir adalah templat.

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

Sekarang saya akan membuat dua fungsi "bebas" bernama make_sp_counted_impl yang akan mengembalikan sebuah pointer ke yang baru dibuat.

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

Oke, kedua fungsi ini penting untuk mengetahui apa yang akan terjadi selanjutnya ketika Anda membuat shared_ptr melalui fungsi templated.

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

Perhatikan apa yang terjadi di atas jika T tidak berlaku dan U adalah kelas "tes" Anda. Ini akan memanggil make_sp_counted_impl () dengan sebuah penunjuk ke U, bukan penunjuk ke T. Manajemen penghancuran semua dilakukan melalui sini. Kelas shared_ptr_base mengelola penghitungan referensi berkenaan dengan penyalinan dan penugasan, dll. Kelas shared_ptr itu sendiri mengelola jenis penggunaan yang lebih aman dari kelebihan operator (->, * dll).

Jadi, meskipun Anda memiliki shared_ptr untuk dibatalkan, di bawahnya Anda mengelola pointer dari jenis yang Anda berikan ke yang baru. Perhatikan bahwa jika Anda mengonversi pointer ke void * sebelum meletakkannya ke shared_ptr, itu akan gagal untuk mengkompilasi pada checked_delete sehingga Anda benar-benar aman di sana juga.

Uang tunai
sumber