Jika saya memiliki fungsi yang perlu bekerja dengan a shared_ptr
, bukankah akan lebih efisien untuk memberikan referensi padanya (jadi untuk menghindari penyalinan shared_ptr
objek)? Apa saja kemungkinan efek samping yang buruk? Saya membayangkan dua kemungkinan kasus:
1) di dalam fungsi salinan dibuat dari argumen, seperti di
ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)
{
...
m_sp_member=sp; //This will copy the object, incrementing refcount
...
}
2) di dalam fungsi argumen hanya digunakan, seperti di
Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here
{
...
sp->do_something();
...
}
Saya tidak dapat melihat dalam kedua kasus alasan yang baik untuk meneruskan nilai boost::shared_ptr<foo>
by alih-alih referensi. Meneruskan nilai hanya akan menambah jumlah referensi "sementara" karena penyalinan, dan kemudian menurunkannya saat keluar dari cakupan fungsi. Apakah saya mengabaikan sesuatu?
Hanya untuk memperjelas, setelah membaca beberapa jawaban: Saya sangat setuju dengan masalah pengoptimalan prematur, dan saya selalu mencoba untuk membuat profil-pertama-kemudian-bekerja-di-hotspot. Pertanyaan saya lebih dari sudut pandang kode teknis murni, jika Anda tahu apa yang saya maksud.
sumber
Jawaban:
Inti dari
shared_ptr
contoh yang berbeda adalah untuk menjamin (sejauh mungkin) bahwa selama inishared_ptr
dalam ruang lingkup, objek yang ditunjuknya akan tetap ada, karena jumlah referensinya paling sedikit 1.Jadi dengan menggunakan referensi ke a
shared_ptr
, Anda menonaktifkan jaminan itu. Jadi dalam kasus kedua Anda:Bagaimana Anda tahu bahwa
sp->do_something()
tidak akan meledak karena pointer nol?Itu semua tergantung apa yang ada di bagian '...' dari kode itu. Bagaimana jika Anda memanggil sesuatu selama '...' pertama yang memiliki efek samping (di suatu tempat di bagian lain dari kode) untuk membersihkan objek
shared_ptr
yang sama? Dan bagaimana jika itu kebetulan satu-satunya yang tersisashared_ptr
dari objek itu? Bye bye object, tepat di mana Anda akan mencoba dan menggunakannya.Jadi ada dua cara untuk menjawab pertanyaan itu:
Periksa sumber seluruh program Anda dengan sangat hati-hati sampai Anda yakin objek tidak akan mati selama badan fungsi.
Ubah parameter kembali menjadi objek berbeda, bukan referensi.
Sedikit saran umum yang berlaku di sini: jangan repot-repot membuat perubahan berisiko pada kode Anda demi kinerja sampai Anda mengatur waktu produk Anda dalam situasi yang realistis di profiler dan secara meyakinkan mengukur bahwa perubahan yang ingin Anda buat akan membuat perbedaan yang signifikan terhadap kinerja.
Pembaruan untuk pemberi komentar JQ
Inilah contoh yang dibuat-buat. Ini sengaja dibuat sederhana, jadi kesalahannya akan terlihat jelas. Dalam contoh nyata, kesalahan tidak begitu jelas karena tersembunyi dalam lapisan detail nyata.
Kami memiliki fungsi yang akan mengirim pesan ke suatu tempat. Ini mungkin pesan besar jadi daripada menggunakan
std::string
yang kemungkinan akan disalin saat diteruskan ke beberapa tempat, kami menggunakanshared_ptr
ke string:(Kami hanya "mengirim" ke konsol untuk contoh ini).
Sekarang kami ingin menambahkan fasilitas untuk mengingat pesan sebelumnya. Kami menginginkan perilaku berikut: variabel harus ada yang berisi pesan terkirim terbaru, tetapi saat pesan sedang dikirim maka tidak boleh ada pesan sebelumnya (variabel harus disetel ulang sebelum dikirim). Jadi kami mendeklarasikan variabel baru:
Kemudian kami mengubah fungsi kami sesuai dengan aturan yang kami tentukan:
Jadi, sebelum kita mulai mengirim kita membuang pesan sebelumnya yang sekarang, dan kemudian setelah pengiriman selesai kita bisa menyimpan pesan baru sebelumnya. Semuanya bagus. Berikut beberapa kode tes:
Dan seperti yang diharapkan, ini mencetak
Hi!
dua kali.Sekarang datanglah Tuan Pemelihara, yang melihat kode dan berpikir: Hei, parameter itu
send_message
adalah ashared_ptr
:Jelas itu bisa diubah menjadi:
Pikirkan peningkatan kinerja yang akan dihasilkannya! (Jangan pedulikan bahwa kami akan mengirim pesan yang biasanya berukuran besar melalui beberapa saluran, sehingga peningkatan kinerja akan menjadi sangat kecil sehingga tidak dapat diukur).
Tetapi masalah sebenarnya adalah bahwa sekarang kode pengujian akan menunjukkan perilaku tidak terdefinisi (dalam Visual C ++ 2010 debug build, ia lumpuh).
Tn. Maintainer terkejut dengan hal ini, tetapi menambahkan pemeriksaan defensif
send_message
untuk menghentikan terjadinya masalah:Tapi tentu saja itu masih berjalan dan macet, karena
msg
tidak pernah nol saatsend_message
dipanggil.Seperti yang saya katakan, dengan semua kode yang begitu berdekatan dalam contoh yang sepele, mudah untuk menemukan kesalahannya. Tetapi dalam program nyata, dengan hubungan yang lebih kompleks antara objek yang bisa berubah yang menyimpan pointer satu sama lain, mudah untuk membuat kesalahan, dan sulit untuk membangun kasus uji yang diperlukan untuk mendeteksi kesalahan.
Solusi mudah, di mana Anda ingin sebuah fungsi dapat mengandalkan
shared_ptr
terus menjadi bukan nol secara keseluruhan, adalah fungsi mengalokasikan true-nya sendirishared_ptr
, daripada mengandalkan referensi ke yang sudah adashared_ptr
.Sisi negatifnya adalah bahwa menyalin a
shared_ptr
tidak gratis: bahkan implementasi "tanpa kunci" harus menggunakan operasi yang saling bertautan untuk menghormati jaminan threading. Jadi, mungkin ada situasi di mana program dapat dipercepat secara signifikan dengan mengubah ashared_ptr
menjadishared_ptr &
. Tetapi ini bukanlah perubahan yang dapat dilakukan dengan aman untuk semua program. Ini mengubah arti logis dari program.Perhatikan bahwa bug serupa akan terjadi jika kita menggunakan
std::string
seluruh alih-alihstd::shared_ptr<std::string>
, dan alih-alih:untuk menghapus pesan tersebut, kami berkata:
Kemudian gejalanya adalah pengiriman pesan kosong yang tidak disengaja, bukan perilaku yang tidak ditentukan. Biaya salinan tambahan dari string yang sangat besar mungkin jauh lebih signifikan daripada biaya menyalin a
shared_ptr
, jadi trade-offnya mungkin berbeda.sumber
Saya mendapati diri saya tidak setuju dengan jawaban yang dipilih paling banyak, jadi saya mencari pendapat ahli dan ini dia. Dari http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything
Herb Sutter: "jika Anda meneruskan shared_ptrs, salinan menjadi mahal"
Scott Meyers: "Tidak ada yang istimewa tentang shared_ptr dalam hal apakah Anda meneruskannya dengan nilai, atau meneruskannya dengan referensi. Gunakan analisis yang sama persis dengan yang Anda gunakan untuk jenis yang ditentukan pengguna lainnya. Orang-orang tampaknya memiliki persepsi ini bahwa shared_ptr entah bagaimana memecahkannya semua masalah manajemen, dan karena itu kecil, maka murah untuk melewatkan nilai. Itu harus disalin, dan ada biaya yang terkait dengan itu ... mahal untuk menyebarkannya berdasarkan nilai, jadi jika saya bisa lolos dengan semantik yang tepat dalam program saya, saya akan meneruskannya dengan merujuk ke const atau referensi sebagai gantinya "
Herb Sutter: "selalu meneruskannya dengan referensi ke const, dan terkadang mungkin karena Anda tahu apa yang Anda panggil dapat mengubah hal yang Anda dapatkan referensi, mungkin Anda akan melewatkan nilai ... jika Anda menyalinnya sebagai parameter, oh Ya ampun, Anda hampir tidak perlu melebihi jumlah referensi itu karena toh disimpan hidup-hidup, dan Anda harus meneruskannya dengan referensi, jadi tolong lakukan itu "
Pembaruan: Herb telah memperluas ini di sini: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , meskipun moral dari ceritanya adalah Anda tidak boleh lewat shared_ptrs sama sekali "kecuali jika Anda ingin menggunakan atau memanipulasi smart pointer itu sendiri, seperti untuk membagikan atau mentransfer kepemilikan."
sumber
shared_ptr
, tidak ada kesepakatan tentang masalah pass-by-value-vs.-reference.const &
akan memengaruhi semantik hanya mudah dilakukan program sederhana.Saya akan menyarankan agar praktik ini tidak dilakukan kecuali Anda dan programmer lain yang bekerja dengan Anda benar-benar, benar tahu apa yang Anda lakukan.
Pertama, Anda tidak tahu bagaimana antarmuka ke kelas Anda dapat berkembang dan Anda ingin mencegah programmer lain melakukan hal-hal buruk. Meneruskan shared_ptr dengan referensi bukanlah sesuatu yang diharapkan oleh programmer, karena itu tidak idiomatis, dan itu membuatnya mudah untuk digunakan secara tidak benar. Program defensif: membuat antarmuka sulit digunakan secara tidak benar. Melewati referensi hanya akan mengundang masalah di kemudian hari.
Kedua, jangan optimalkan sampai Anda tahu kelas khusus ini akan menjadi masalah. Profil dulu, dan kemudian jika program Anda benar-benar membutuhkan dorongan yang diberikan melalui referensi, maka mungkin. Jika tidak, jangan khawatirkan hal-hal kecil (yaitu instruksi N tambahan yang diperlukan untuk meneruskan nilai) alih-alih khawatir tentang desain, struktur data, algoritme, dan pemeliharaan jangka panjang.
sumber
Ya, mengambil referensi tidak masalah di sana. Anda tidak bermaksud memberikan metode kepemilikan bersama; ia hanya ingin bekerja dengannya. Anda juga dapat mengambil referensi untuk kasus pertama, karena Anda tetap menyalinnya. Tapi untuk kasus pertama, ini membutuhkan kepemilikan. Ada trik untuk tetap menyalinnya hanya sekali:
Anda juga harus menyalin saat mengembalikannya (yaitu tidak mengembalikan referensi). Karena kelas Anda tidak tahu apa yang dilakukan klien dengannya (kelas dapat menyimpan penunjuk ke sana dan kemudian terjadi ledakan besar). Jika nanti ternyata itu bottleneck (profil pertama!), Maka Anda masih bisa mengembalikan referensi.
Sunting : Tentu saja, seperti yang ditunjukkan orang lain, ini hanya berlaku jika Anda tahu kode Anda dan tahu bahwa Anda tidak mengatur ulang penunjuk bersama yang diberikan dengan cara tertentu. Jika ragu, berikan nilai saja.
sumber
Hal ini masuk akal untuk lulus
shared_ptr
s olehconst&
. Ini sepertinya tidak akan menimbulkan masalah (kecuali dalam kasus yang tidak biasa bahwa referensishared_ptr
dihapus selama pemanggilan fungsi, seperti yang dijelaskan oleh Earwicker) dan kemungkinan akan lebih cepat jika Anda melewatkan banyak hal ini. Ingat; defaultnyaboost::shared_ptr
adalah thread safe, jadi menyalinnya akan menyertakan penambahan thread safe.Cobalah untuk menggunakan
const&
daripada hanya&
, karena objek sementara mungkin tidak diteruskan oleh referensi non-const. (Meskipun ekstensi bahasa di MSVC memungkinkan Anda untuk tetap melakukannya)sumber
Dalam kasus kedua, melakukan ini lebih sederhana:
Anda bisa menyebutnya sebagai
sumber
Saya akan menghindari referensi "biasa" kecuali fungsi secara eksplisit dapat mengubah pointer.
A
const &
mungkin merupakan pengoptimalan mikro yang masuk akal saat memanggil fungsi kecil - misalnya untuk mengaktifkan pengoptimalan lebih lanjut, seperti meniadakan beberapa kondisi. Selain itu, increment / decrement - karena threadnya aman - adalah titik sinkronisasi. Namun, saya tidak berharap ini membuat perbedaan besar di sebagian besar skenario.Umumnya, Anda harus menggunakan gaya yang lebih sederhana kecuali Anda punya alasan untuk tidak melakukannya. Kemudian, gunakan
const &
secara konsisten, atau tambahkan komentar tentang mengapa jika Anda menggunakannya hanya di beberapa tempat.sumber
Saya akan menganjurkan melewatkan pointer bersama dengan referensi const - semantik bahwa fungsi yang diteruskan dengan pointer TIDAK memiliki pointer, yang merupakan idiom bersih untuk pengembang.
Satu-satunya kesalahan adalah dalam beberapa program utas, objek yang diarahkan oleh penunjuk bersama akan dihancurkan di utas lain. Jadi aman untuk mengatakan menggunakan referensi const dari pointer bersama aman dalam program utas tunggal.
Meneruskan penunjuk bersama oleh referensi non-const terkadang berbahaya - alasannya adalah fungsi swap dan reset yang mungkin dipanggil oleh fungsi di dalam untuk menghancurkan objek yang masih dianggap valid setelah fungsi tersebut kembali.
Ini bukan tentang pengoptimalan prematur, saya kira - ini tentang menghindari pemborosan siklus CPU yang tidak perlu ketika Anda jelas apa yang ingin Anda lakukan dan idiom pengkodean telah diadopsi dengan kuat oleh sesama pengembang Anda.
Hanya 2 sen saya :-)
sumber
Tampaknya semua pro dan kontra di sini sebenarnya dapat digeneralisasikan ke jenis APA PUN yang diteruskan dengan referensi bukan hanya shared_ptr. Menurut pendapat saya, Anda harus tahu semantik lewat referensi, referensi konstanta dan nilai dan menggunakannya dengan benar. Tetapi sama sekali tidak ada yang salah dengan meneruskan shared_ptr dengan referensi, kecuali Anda berpikir bahwa semua referensi itu buruk ...
Untuk kembali ke contoh:
Bagaimana Anda tahu bahwa
sp.do_something()
itu tidak akan meledak karena penunjuk yang menjuntai?Yang benar adalah, shared_ptr atau tidak, const atau tidak, ini bisa terjadi jika Anda memiliki cacat desain, seperti berbagi kepemilikan secara langsung atau tidak langsung di
sp
antara utas, melewatkan penggunaan objek yang melakukannyadelete this
, Anda memiliki kepemilikan melingkar atau kesalahan kepemilikan lainnya.sumber
Satu hal yang belum saya lihat disebutkan adalah bahwa ketika Anda melewati pointer bersama dengan referensi, Anda kehilangan konversi implisit yang Anda dapatkan jika Anda ingin meneruskan pointer bersama kelas turunan melalui referensi ke pointer bersama kelas dasar.
Misalnya, kode ini akan menghasilkan kesalahan, tetapi akan berfungsi jika Anda mengubah
test()
sehingga penunjuk bersama tidak diteruskan oleh referensi.sumber
Saya akan berasumsi bahwa Anda sudah familiar dengan pengoptimalan prematur dan menanyakan ini baik untuk tujuan akademis atau karena Anda telah mengisolasi beberapa kode yang sudah ada sebelumnya yang berkinerja buruk.
Melewati referensi tidak apa-apa
Melewati referensi const lebih baik, dan biasanya dapat digunakan, karena tidak memaksa konstanta pada objek yang dituju.
Anda tidak berisiko kehilangan penunjuk karena menggunakan referensi. Referensi tersebut adalah bukti bahwa Anda memiliki salinan penunjuk cerdas sebelumnya dalam tumpukan dan hanya satu utas yang memiliki tumpukan panggilan, sehingga salinan yang sudah ada tidak akan hilang.
Menggunakan referensi seringkali lebih efisien untuk alasan yang Anda sebutkan, tetapi tidak dijamin . Ingatlah bahwa dereferensi suatu objek juga membutuhkan pekerjaan. Skenario penggunaan referensi ideal Anda adalah jika gaya pengkodean Anda melibatkan banyak fungsi kecil, di mana penunjuk akan diteruskan dari fungsi ke fungsi ke fungsi sebelum digunakan.
Anda harus selalu menghindari menyimpan penunjuk cerdas Anda sebagai referensi.
Class::take_copy_of_sp(&sp)
Contoh Anda menunjukkan penggunaan yang benar untuk itu.sumber
Dengan asumsi kita tidak peduli dengan kebenaran const (atau lebih, Anda bermaksud mengizinkan fungsi untuk dapat mengubah atau berbagi kepemilikan data yang sedang diteruskan), meneruskan boost :: shared_ptr by value lebih aman daripada meneruskannya dengan referensi sebagai kami mengizinkan boost :: shared_ptr asli untuk mengontrol masa pakainya sendiri. Perhatikan hasil dari kode berikut ...
Dari contoh di atas, kita melihat bahwa meneruskan sharedA dengan referensi memungkinkan FooTakesReference menyetel ulang penunjuk asli, yang mengurangi jumlah penggunaannya menjadi 0, menghancurkan datanya. FooTakesValue , bagaimanapun, tidak dapat mengatur ulang penunjuk asli, menjamin data sharedB masih dapat digunakan. Ketika pengembang lain datang dan mencoba untuk mendukung keberadaan sharedA yang rapuh, kekacauan pun terjadi. Pengembang sharedB yang beruntung , bagaimanapun, pulang lebih awal karena semuanya baik-baik saja di dunianya.
Keamanan kode, dalam hal ini, jauh melebihi peningkatan kecepatan yang diciptakan penyalinan. Pada saat yang sama, boost :: shared_ptr dimaksudkan untuk meningkatkan keamanan kode. Akan jauh lebih mudah untuk beralih dari salinan ke referensi, jika sesuatu memerlukan pengoptimalan niche semacam ini.
sumber
Sandy menulis: "Tampaknya semua pro dan kontra di sini sebenarnya dapat digeneralisasikan ke jenis APA PUN yang diteruskan dengan referensi bukan hanya shared_ptr."
Benar sampai batas tertentu, tetapi tujuan menggunakan shared_ptr adalah untuk menghilangkan kekhawatiran tentang masa pakai objek dan membiarkan compiler menanganinya untuk Anda. Jika Anda akan meneruskan penunjuk bersama dengan referensi dan mengizinkan klien metode non-const panggilan referensi-objek-dihitung yang mungkin membebaskan data objek, maka penggunaan penunjuk bersama hampir tidak ada gunanya.
Saya menulis "hampir" dalam kalimat sebelumnya karena kinerja dapat menjadi perhatian, dan itu 'mungkin' dibenarkan dalam kasus yang jarang terjadi, tetapi saya juga akan menghindari skenario ini sendiri dan mencari sendiri semua kemungkinan solusi pengoptimalan lainnya, seperti untuk melihat dengan serius dalam menambahkan tingkat tipuan lain, evaluasi malas, dll.
Kode yang ada setelah penulisnya, atau bahkan memposting memori penulisnya, yang memerlukan asumsi implisit tentang perilaku, khususnya perilaku tentang masa pakai objek, memerlukan dokumentasi yang jelas, ringkas, dapat dibaca, dan kemudian banyak klien tidak akan membacanya! Kesederhanaan hampir selalu mengalahkan efisiensi, dan hampir selalu ada cara lain untuk menjadi efisien. Jika Anda benar-benar perlu meneruskan nilai dengan referensi untuk menghindari penyalinan mendalam oleh konstruktor salinan objek yang dihitung-referensi Anda (dan operator yang sama), maka mungkin Anda harus mempertimbangkan cara untuk membuat data yang disalin dalam menjadi referensi terhitung petunjuk yang dapat digunakan. disalin dengan cepat. (Tentu saja, itu hanya satu skenario desain yang mungkin tidak berlaku untuk situasi Anda).
sumber
Saya dulu bekerja dalam sebuah proyek yang prinsipnya sangat kuat tentang menyampaikan petunjuk cerdas dengan nilai. Ketika saya diminta untuk melakukan beberapa analisis kinerja - saya menemukan bahwa untuk kenaikan dan penurunan penghitung referensi dari pointer pintar, aplikasi menghabiskan antara 4-6% dari waktu prosesor yang digunakan.
Jika Anda ingin meneruskan petunjuk cerdas dengan nilai hanya untuk menghindari masalah dalam kasus aneh seperti yang dijelaskan dari Daniel Earwicker, pastikan Anda memahami harga yang Anda bayar untuk itu.
Jika Anda memutuskan untuk menggunakan referensi, alasan utama untuk menggunakan referensi const adalah untuk memungkinkan adanya upcasting implisit saat Anda perlu meneruskan pointer bersama ke objek dari kelas yang mewarisi kelas yang Anda gunakan dalam antarmuka.
sumber
Sebagai tambahan dari apa yang litb katakan, saya ingin menunjukkan bahwa itu mungkin melewati referensi const pada contoh kedua, dengan cara itu Anda yakin Anda tidak sengaja memodifikasinya.
sumber
melewati nilai:
melewati referensi:
melewati penunjuk:
sumber
Setiap bagian kode harus memiliki arti. Jika Anda memberikan penunjuk bersama berdasarkan nilai di mana pun dalam aplikasi, ini berarti "Saya tidak yakin tentang apa yang terjadi di tempat lain, oleh karena itu saya lebih menyukai keamanan mentah ". Ini bukanlah apa yang saya sebut sebagai tanda kepercayaan diri yang baik bagi pemrogram lain yang dapat melihat kode tersebut.
Bagaimanapun, meskipun suatu fungsi mendapatkan referensi konstanta dan Anda "tidak yakin", Anda masih dapat membuat salinan penunjuk bersama di bagian atas fungsi, untuk menambahkan referensi yang kuat ke penunjuk. Ini juga bisa dilihat sebagai petunjuk tentang desain ("penunjuk dapat dimodifikasi di tempat lain").
Jadi ya, IMO, defaultnya harus " pass by const reference ".
sumber