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?
sumber
Jawaban:
Kuncinya adalah
std::shared_ptr
melakukan penghapusan tipe. Pada dasarnya, ketika barushared_ptr
dibuat itu akan menyimpandeleter
fungsi internal (yang dapat diberikan sebagai argumen untuk konstruktor tetapi jika tidak ada default untuk panggilandelete
). Ketikashared_ptr
dihancurkan, itu memanggil fungsi yang disimpan dan yang akan memanggildeleter
.Sebuah sketsa sederhana dari penghapusan tipe yang terjadi disederhanakan dengan fungsi std ::, dan menghindari semua penghitungan referensi dan masalah lain dapat dilihat di sini:
Ketika a
shared_ptr
disalin (atau dibangun secara default) dari yang lain, deleter dilewatkan, sehingga ketika Anda membangunshared_ptr<T>
darishared_ptr<U>
informasi tentang apa yang memanggil destruktor juga dilewatkan dideleter
.sumber
my_shared
. Saya akan memperbaikinya tetapi belum memiliki hak untuk mengedit.std::shared_ptr<void>
memungkinkan saya menghindari mendeklarasikan kelas pembungkus yang tidak berguna hanya supaya saya bisa mewarisinya dari kelas dasar tertentu.my_unique_ptr
. Ketika dalammain
template dipakai dengandouble
deleter kanan dipilih tapi ini bukan bagian dari jenismy_unique_ptr
dan tidak dapat diambil dari objek. Jenis deleter dihapus dari objek, ketika suatu fungsi menerimamy_unique_ptr
(katakan dengan rvalue-reference), fungsi itu tidak dan tidak perlu tahu apa itu deleter.shared_ptr<T>
secara logis [*] memiliki (setidaknya) dua anggota data yang relevan:Fungsi deleter dari Anda
shared_ptr<Test>
, mengingat cara Anda membuatnya, adalah fungsi normal untukTest
, yang mengubah pointer menjadiTest*
dandelete
menggunakannya.Ketika Anda mendorong Anda
shared_ptr<Test>
ke dalam vektorshared_ptr<void>
, keduanya disalin, meskipun yang pertama dikonversi menjadivoid*
.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_ptr
dapat 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.
sumber
shared_ptr
langsung dengan tipe yang sesuai atau jika Anda gunakanmake_shared
. Tapi, tetap saja ide yang baik karena jenis pointer dapat berubah dari konstruksi sampai disimpan dalamshared_ptr
:base *p = new derived; shared_ptr<base> sp(p);
, sejauhshared_ptr
yang bersangkutan objek tersebutbase
tidakderived
, sehingga Anda perlu destructor virtual. Pola ini bisa sama dengan pola pabrik, misalnya.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 divoid
sini, melemparkannya dengan tepat ke tipe statis yang Anda gunakan ditest
sini, dan memanggil destruktor pada objek ini.Adakah ilmu yang cukup maju terasa seperti sihir, bukan?
sumber
Konstruktor
shared_ptr<T>(Y *p)
memang tampaknya memanggil dishared_ptr<T>(Y *p, D d)
manad
deleter yang dihasilkan secara otomatis untuk objek.Ketika ini terjadi, jenis objek
Y
diketahui, sehingga deleter untukshared_ptr
objek ini tahu destruktor mana yang harus dihubungi dan informasi ini tidak hilang ketika pointer disimpan dalam vektorshared_ptr<void>
.Memang spesifikasi mengharuskan agar
shared_ptr<T>
objek yang menerima menerimashared_ptr<U>
objek, harus benar bahwa danU*
harus secara implisit dapat dikonversi ke aT*
dan ini tentu saja terjadiT=void
karena pointer apa pun dapat dikonversi menjadivoid*
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 tetapshared_ptr<T>
sebesar pointer biasa (namun mendereferensi pointer membutuhkan petunjuk ganda)sumber
Test*
secara implisit dapat dikonversi kevoid*
, oleh karenashared_ptr<Test>
itu secara implisit dapat dikonversi keshared_ptr<void>
, dari memori. Ini berfungsi karenashared_ptr
dirancang 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.sumber
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.
Sekarang saya akan membuat dua fungsi "bebas" bernama make_sp_counted_impl yang akan mengembalikan sebuah pointer ke yang baru dibuat.
Oke, kedua fungsi ini penting untuk mengetahui apa yang akan terjadi selanjutnya ketika Anda membuat shared_ptr melalui fungsi templated.
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.
sumber