Saya hanya menonton stream "Going Native 2012" dan saya memperhatikan diskusi tentang std::shared_ptr
. Saya agak terkejut mendengar pandangan Bjarne yang agak negatif std::shared_ptr
dan komentarnya bahwa itu harus digunakan sebagai "upaya terakhir" ketika waktu hidup suatu objek tidak pasti (yang saya percaya, menurutnya, jarang terjadi).
Adakah yang mau menjelaskan ini sedikit lebih dalam? Bagaimana kita bisa memprogram tanpa std::shared_ptr
dan masih mengelola objek seumur hidup dengan cara yang aman ?
c++
c++11
smart-pointer
ronag
sumber
sumber
Jawaban:
Jika Anda dapat menghindari kepemilikan bersama maka aplikasi Anda akan lebih sederhana dan lebih mudah dipahami dan karenanya kurang rentan terhadap bug yang diperkenalkan selama pemeliharaan. Model kepemilikan yang kompleks atau tidak jelas cenderung mengarah pada sulit untuk mengikuti kopling dari berbagai bagian aplikasi melalui status bersama yang mungkin tidak mudah dilacak.
Mengingat hal ini, lebih disukai untuk menggunakan objek dengan durasi penyimpanan otomatis dan memiliki sub-objek "nilai". Kegagalan ini,
unique_ptr
mungkin merupakan alternatif yang baik denganshared_ptr
menjadi - jika bukan pilihan terakhir - beberapa cara di bawah daftar alat yang diinginkan.sumber
Dunia tempat Bjarne tinggal sangat ... akademis, karena menginginkan masa jabatan yang lebih baik. Jika kode Anda dapat dirancang dan disusun sedemikian rupa sehingga objek memiliki hierarki relasional yang disengaja, sehingga hubungan kepemilikan menjadi kaku dan pantang menyerah, kode mengalir dalam satu arah (dari level tinggi ke level rendah), dan objek hanya berbicara dengan yang lebih rendah di hierarki, maka Anda tidak akan menemukan banyak kebutuhan
shared_ptr
. Ini adalah sesuatu yang Anda gunakan pada kesempatan langka di mana seseorang harus melanggar aturan. Tetapi jika tidak, Anda bisa memasukkan semuanya ke dalamvector
atau struktur data lain yang menggunakan semantik nilai, danunique_ptr
untuk hal-hal yang harus Anda alokasikan sendiri.Meskipun itu adalah dunia yang hebat untuk ditinggali, itu bukan apa yang bisa Anda lakukan sepanjang waktu. Jika Anda tidak dapat mengatur kode Anda dengan cara itu, karena desain sistem yang Anda coba buat berarti tidak mungkin (atau hanya sangat tidak menyenangkan), maka Anda akan menemukan diri Anda membutuhkan kepemilikan bersama atas benda-benda yang semakin banyak. .
Dalam sistem seperti itu, memegang pointer telanjang adalah ... tidak persis berbahaya, tetapi itu menimbulkan pertanyaan. Hal yang hebat tentang itu
shared_ptr
adalah memberikan jaminan sintaksis yang wajar tentang masa hidup objek. Bisakah itu rusak? Tentu saja. Tetapi orang juga dapat melakukan banyakconst_cast
hal; perawatan dasar dan pemberian makanshared_ptr
harus memberikan kualitas hidup yang wajar untuk objek yang dialokasikan yang kepemilikannya harus dibagi.Lalu, ada
weak_ptr
s, yang tidak dapat digunakan tanpa adanya ashared_ptr
. Jika sistem Anda terstruktur dengan kaku, maka Anda dapat menyimpan pointer telanjang ke beberapa objek, aman karena mengetahui bahwa struktur aplikasi memastikan bahwa objek yang ditunjuk akan hidup lebih lama dari Anda. Anda dapat memanggil fungsi yang mengembalikan pointer ke beberapa nilai internal atau eksternal (misalnya, cari objek bernama X). Dalam kode yang terstruktur dengan baik, fungsi itu hanya akan tersedia untuk Anda jika umur objek dijamin melebihi milik Anda; dengan demikian, menyimpan pointer telanjang di objek Anda baik-baik saja.Karena kekakuan itu tidak selalu mungkin dicapai dalam sistem nyata, Anda perlu beberapa cara untuk memastikan masa pakai yang wajar . Terkadang, Anda tidak perlu kepemilikan penuh; Terkadang, Anda hanya perlu tahu kapan pointer buruk atau bagus. Di situlah
weak_ptr
masuk. Ada kasus di mana saya bisa menggunakanunique_ptr
atauboost::scoped_ptr
, tapi saya harus menggunakanshared_ptr
karena saya secara khusus perlu memberi seseorang pointer "volatile". Pointer yang seumur hidupnya tidak pasti, dan mereka dapat menanyakan kapan pointer itu dihancurkan.Cara aman untuk bertahan hidup ketika keadaan dunia tak tentu.
Mungkinkah itu dilakukan oleh beberapa pemanggilan fungsi untuk mendapatkan pointer, alih-alih via
weak_ptr
? Ya, tapi itu bisa lebih mudah dipatahkan. Fungsi yang mengembalikan pointer telanjang tidak memiliki cara yang menunjukkan secara sintaksis bahwa pengguna tidak melakukan sesuatu seperti menyimpan pointer itu dalam jangka panjang. Mengembalikanshared_ptr
juga membuatnya menjadi sangat mudah bagi seseorang untuk menyimpannya dan berpotensi memperpanjang masa hidup suatu objek. Mengembalikan yangweak_ptr
sangat menyarankan bahwa menyimpanshared_ptr
Anda dapatkan darilock
adalah ... ide yang meragukan. Ini tidak akan menghentikan Anda untuk melakukannya, tetapi tidak ada dalam C ++ yang menghentikan Anda dari memecahkan kode.weak_ptr
memberikan sedikit perlawanan dari melakukan hal yang alami.Sekarang, itu bukan untuk mengatakan bahwa
shared_ptr
tidak bisa digunakan secara berlebihan ; itu pasti bisa. Terutama praunique_ptr
, ada banyak kasus di mana saya hanya menggunakanboost::shared_ptr
karena saya perlu melewati pointer RAII atau memasukkannya ke dalam daftar. Tanpa memindahkan semantik danunique_ptr
,boost::shared_ptr
adalah satu-satunya solusi nyata.Dan Anda dapat menggunakannya di tempat-tempat yang sebenarnya tidak perlu. Seperti yang dinyatakan di atas, struktur kode yang tepat dapat menghilangkan kebutuhan untuk beberapa penggunaan
shared_ptr
. Tetapi jika sistem Anda tidak dapat terstruktur seperti itu dan masih melakukan apa yang perlu dilakukan,shared_ptr
akan sangat bermanfaat.sumber
shared_ptr
sangat bagus untuk sistem di mana c ++ terintegrasi dengan bahasa scripting seperti python. Menggunakanboost::python
, penghitungan referensi pada sisi c ++ dan python sangat bekerja sama; objek apa pun dari c ++ masih bisa disimpan dalam python dan tidak akan mati.shared_ptr
. Keduanya menggunakan implementasi mereka sendiriintrusive_ptr
. Saya hanyashared_ptr
berlaku sama untukintrusive_ptr
: dia keberatan dengan seluruh konsep kepemilikan bersama, bukan pada ejaan spesifik dari konsep tersebut. Jadi untuk tujuan pertanyaan ini, mereka adalah dua contoh-contoh nyata dari aplikasi besar yang tidak menggunakanshared_ptr
. (Dan, lebih dari itu, mereka menunjukkan bahwashared_ptr
itu berguna bahkan ketika itu tidak memungkinkanweak_ptr
.)Saya tidak percaya saya pernah menggunakannya
std::shared_ptr
.Sebagian besar waktu, suatu objek dikaitkan dengan beberapa koleksi, yang menjadi miliknya selama seumur hidup. Dalam hal ini Anda hanya dapat menggunakan
whatever_collection<o_type>
atauwhatever_collection<std::unique_ptr<o_type>>
, koleksi itu menjadi anggota suatu objek atau variabel otomatis. Tentu saja, jika Anda tidak membutuhkan jumlah objek yang dinamis, Anda bisa menggunakan array otomatis ukuran tetap.Baik iterasi melalui koleksi atau operasi lainnya pada objek tidak memerlukan fungsi pembantu untuk berbagi kepemilikan ... ia menggunakan objek, lalu kembali, dan penelepon menjamin bahwa objek tetap hidup untuk seluruh panggilan . Sejauh ini, ini adalah kontrak yang paling sering digunakan antara penelepon dan callee.
Nicol Bolas berkomentar bahwa "Jika suatu benda memegang penunjuk telanjang dan benda itu mati ... oops." dan "Objek perlu memastikan bahwa objek hidup melalui kehidupan objek itu. Hanya
shared_ptr
bisa melakukan itu."Saya tidak membeli argumen itu. Setidaknya bukan itu yang
shared_ptr
memecahkan masalah ini. Bagaimana dengan:Seperti pengumpulan sampah, penggunaan default
shared_ptr
mendorong programmer untuk tidak memikirkan kontrak antara objek, atau antara fungsi dan pemanggil. Memikirkan prasyarat dan prasyarat yang benar diperlukan, dan objek seumur hidup hanyalah sepotong kecil dari pai yang lebih besar.Objek tidak "mati", beberapa kode menghancurkannya. Dan melemparkan
shared_ptr
pada masalah alih-alih mencari tahu kontrak panggilan adalah keamanan yang salah.sumber
shared_ptr
danweak_ptr
dirancang untuk dihindari. Bjarne mencoba untuk hidup di dunia jika semuanya memiliki kehidupan yang baik, eksplisit, dan semuanya dibangun di sekitarnya. Dan jika Anda dapat membangun dunia itu, bagus. Tapi bukan itu yang terjadi di dunia nyata. Objek perlu memastikan bahwa objek hidup melalui kehidupan objek itu. Hanyashared_ptr
bisa melakukan itu.shared_ptr
hanya mengurangi satu modifikasi luar tertentu, dan bahkan tidak yang paling umum. Dan itu bukan tanggung jawab objek untuk memastikan masa pakainya benar, jika fungsi panggilan kontrak menentukan sebaliknya.unique_ptr
, yang menyatakan bahwa hanya satu pointer ke objek yang ada dan memiliki kepemilikan.shared_ptr
, itu masih harus mengembalikan aunique_ptr
. Konversi dariunique_ptr
keshared_ptr
mudah, tetapi sebaliknya secara logis tidak mungkin.Saya lebih suka tidak berpikir secara absolut (seperti "pilihan terakhir") tetapi relatif terhadap domain masalah.
C ++ dapat menawarkan sejumlah cara berbeda untuk mengatur masa pakai. Beberapa dari mereka mencoba untuk mengembalikan objek dengan cara yang digerakkan oleh tumpukan. Beberapa yang lain mencoba melepaskan diri dari batasan ini. Beberapa dari mereka adalah "literal", beberapa lainnya adalah perkiraan.
Sebenarnya Anda bisa:
Person
memiliki yang samaname
adalah sama orang (lebih baik: dua representasi dari sama orang ). Seumur hidup diberikan oleh tumpukan mesin, pada akhirnya - tidak masalah bagi program (karena seseorang adalah nama , tidak peduli apaPerson
yang membawanya)std::unique_ptr
apa yang dilakukan (Anda dapat menganggapnya sebagai vektor dengan ukuran 1). Sekali lagi, Anda mengakui objek mulai ada (dan mengakhiri keberadaan mereka) sebelum (setelah) struktur data yang mereka rujuk.Kelemahan dari mehtods ini adalah bahwa jenis dan jumlah objek tidak dapat bervariasi selama pelaksanaan panggilan level stack yang lebih dalam sehubungan dengan di mana mereka dibuat. Semua teknik ini "gagal" kekuatan mereka dalam semua situasi di mana penciptaan dan penghapusan objek adalah konsekuensi dari aktivitas pengguna, sehingga runtime-jenis objek tidak dikompilasi-waktu diketahui dan mungkin ada over-struktur yang merujuk pada objek yang pengguna diminta untuk menghapus dari panggilan fungsi tingkat-stack yang lebih dalam. Dalam hal ini, Anda harus:
C ++ isteslf tidak memiliki mekanisme asli untuk memonitor acara itu (
while(are_they_needed)
), maka Anda harus memperkirakan dengan:Pergi ke solusi pertama ke yang terakhir, jumlah struktur data tambahan yang dibutuhkan untuk mengelola objek seumur hidup meningkat, karena waktu yang dihabiskan untuk mengatur dan memeliharanya.
Pengumpul sampah memiliki biaya, shared_ptr memiliki lebih sedikit, unique_ptr bahkan lebih sedikit, dan tumpukan objek yang dikelola memiliki sangat sedikit.
Apakah
shared_ptr
"jalan terakhir"? Tidak, tidak: jalan terakhir adalah pemulung.shared_ptr
sebenarnya adalah upayastd::
terakhir yang diusulkan. Tetapi mungkin solusi yang tepat, jika Anda berada dalam situasi yang saya jelaskan.sumber
Satu hal yang disebutkan oleh Herb Sutter di sesi berikutnya adalah bahwa setiap kali Anda menyalin
shared_ptr<>
ada kenaikan / penurunan yang saling terkait yang harus terjadi. Pada kode multi-utas pada sistem multi-inti, sinkronisasi memori tidak signifikan. Dengan pilihan itu, lebih baik menggunakan nilai tumpukan, atau aunique_ptr<>
dan meneruskan referensi atau petunjuk mentah.sumber
shared_ptr
referensi lvalue atau rvalue ...shared_ptr
peluru perak yang akan menyelesaikan semua masalah kebocoran memori Anda hanya karena sudah sesuai standar. Ini adalah jebakan yang menggoda, tetapi masih penting untuk menyadari kepemilikan sumber daya, dan kecuali kepemilikan itu dibagikan, ashared_ptr<>
bukanlah pilihan terbaik.Saya tidak ingat apakah "jalan" terakhir adalah kata yang tepat ia gunakan, tetapi saya percaya bahwa arti sebenarnya dari apa yang ia katakan adalah "pilihan" terakhir: diberikan syarat kepemilikan yang jelas; unique_ptr, kelemahan_ptr, shared_ptr dan bahkan pointer telanjang memiliki tempat mereka.
Satu hal yang mereka semua sepakati adalah bahwa kita (pengembang, penulis buku, dll) semuanya dalam "fase pembelajaran" C ++ 11 dan pola dan gaya didefinisikan.
Sebagai contoh, Herb menjelaskan kita harus mengharapkan edisi baru dari beberapa buku C ++ seminal, seperti C ++ Efektif (Meyers) dan C ++ Standar Pengkodean (Sutter & Alexandrescu), beberapa tahun lagi sementara pengalaman industri dan praktik terbaik dengan C ++ 11 panci keluar.
sumber
Saya pikir apa yang ia maksudkan adalah menjadi hal biasa bagi semua orang untuk menulis shared_ptr setiap kali mereka mungkin menulis pointer standar (seperti semacam penggantian global), dan itu digunakan sebagai cop-out alih-alih benar-benar mendesain atau setidaknya perencanaan untuk pembuatan dan penghapusan objek.
Hal lain yang orang lupa (selain bottleneck mengunci / memperbarui / membuka kunci yang disebutkan dalam materi di atas), adalah shared_ptr saja tidak menyelesaikan masalah siklus. Anda masih dapat membocorkan sumber daya dengan shared_ptr:
Objek A, berisi pointer bersama ke Objek A lainnya. Objek B membuat A a1 dan A a2, dan menetapkan a1.otherA = a2; dan a2.otherA = a1; Sekarang, pointer bersama objek B yang digunakan untuk membuat a1, a2 keluar dari ruang lingkup (katakanlah pada akhir fungsi). Sekarang Anda memiliki kebocoran - tidak ada orang lain yang mengacu pada a1 dan a2, tetapi mereka merujuk satu sama lain sehingga jumlah referensi mereka selalu 1, dan Anda telah bocor.
Itu contoh sederhana, ketika ini terjadi dalam kode nyata itu terjadi biasanya dengan cara yang rumit. Ada solusi dengan lemah_ptr, tetapi begitu banyak orang sekarang hanya melakukan shared_ptr di mana-mana dan bahkan tidak tahu masalah kebocoran atau bahkan soal lemah_ptr.
Untuk menyelesaikannya: Saya pikir komentar yang dirujuk oleh OP bermuara pada ini:
Tidak peduli bahasa apa yang Anda gunakan (dikelola, tidak dikelola, atau sesuatu di antaranya dengan jumlah referensi seperti shared_ptr), Anda perlu memahami dan secara sengaja memutuskan pembuatan objek, masa hidup, dan penghancuran.
sunting: bahkan jika itu berarti "tidak diketahui, saya perlu menggunakan shared_ptr," Anda masih memikirkannya dan melakukannya dengan sengaja.
sumber
Saya akan menjawab dari pengalaman saya dengan Objective-C, bahasa di mana semua objek referensi dihitung dan dialokasikan di heap. Karena memiliki satu cara untuk memperlakukan objek, banyak hal lebih mudah bagi programmer. Itu telah memungkinkan aturan standar untuk didefinisikan bahwa, ketika dipatuhi, menjamin kekokohan kode dan tidak ada kebocoran memori. Itu juga memungkinkan untuk optimisasi kompiler pintar untuk muncul seperti ARC baru-baru ini (penghitungan referensi otomatis).
Maksud saya adalah shared_ptr harus menjadi pilihan pertama Anda daripada pilihan terakhir. Gunakan penghitungan referensi secara default dan opsi lain hanya jika Anda yakin dengan apa yang Anda lakukan. Anda akan lebih produktif dan kode Anda akan lebih kuat.
sumber
Saya akan mencoba menjawab pertanyaan:
C ++ memiliki sejumlah besar cara berbeda untuk melakukan memori, misalnya:
struct A { MyStruct s1,s2; };
alih-alih shared_ptr dalam lingkup kelas. Ini hanya untuk pemrogram tingkat lanjut karena mengharuskan Anda memahami cara kerja dependensi, dan membutuhkan kemampuan untuk mengontrol dependensi yang cukup untuk membatasi mereka ke pohon. Urutan kelas dalam file header adalah aspek penting dari ini. Tampaknya penggunaan ini sudah umum dengan tipe bawaan bawaan ++, tetapi penggunaannya dengan kelas yang ditentukan pemrogram tampaknya kurang digunakan karena masalah ketergantungan dan urutan kelas ini. Solusi ini juga memiliki masalah dengan sizeof. Programmer melihat masalah dalam hal ini sebagai persyaratan untuk menggunakan deklarasi maju atau #includes yang tidak perlu dan dengan demikian banyak programmer akan kembali ke solusi inferior dari pointer dan kemudian ke shared_ptr.MyClass &find_obj(int i);
+ clone () sebagai gantishared_ptr<MyClass> create_obj(int i);
. Banyak programmer ingin membuat pabrik untuk membuat objek baru. shared_ptr sangat cocok untuk penggunaan seperti ini. Masalahnya adalah bahwa itu sudah mengasumsikan solusi manajemen memori yang kompleks menggunakan heap / alokasi toko bebas, bukan tumpukan sederhana atau solusi berbasis objek. Hirarki kelas C ++ yang baik mendukung semua skema manajemen memori, bukan hanya satu saja. Solusi berbasis referensi dapat bekerja jika objek yang dikembalikan disimpan di dalam objek yang mengandung, alih-alih menggunakan variabel lingkup fungsi lokal. Melewati kepemilikan dari pabrik ke kode pengguna harus dihindari. Menyalin objek setelah menggunakan find_obj () adalah cara yang baik untuk menanganinya - copy-constructor normal dan constructor normal (dari kelas yang berbeda) dengan parameter refrerence atau clone () agar objek polimorfik dapat menanganinya.sumber
unique_ptr
paling cocok untuk pabrik. Anda dapat mengubah aunique_ptr
menjadishared_ptr
, tetapi secara logis tidak mungkin untuk pergi ke arah lain.