Haruskah kita meneruskan shared_ptr dengan referensi atau berdasarkan nilai?

270

Saat fungsi mengambil shared_ptr(dari boost atau C ++ 11 STL), apakah Anda mengopernya:

  • dengan referensi const: void foo(const shared_ptr<T>& p)

  • atau berdasarkan nilai void foo(shared_ptr<T> p):?

Saya lebih suka metode pertama karena saya curiga akan lebih cepat. Tetapi apakah ini benar-benar layak atau adakah masalah tambahan?

Bisakah Anda memberikan alasan untuk pilihan Anda atau jika kasusnya, mengapa Anda pikir itu tidak masalah.

Danvil
sumber
14
Masalahnya adalah mereka tidak setara. Versi referensi berteriak "Saya akan alias beberapa shared_ptr, dan saya bisa mengubahnya jika saya mau.", Sementara versi nilai mengatakan "Saya akan menyalin Anda shared_ptr, jadi sementara saya bisa mengubahnya, Anda tidak akan pernah tahu. ) Parameter const-reference adalah solusi nyata, yang mengatakan "Saya akan alias beberapa shared_ptr, dan saya berjanji untuk tidak mengubahnya." (Yang sangat mirip dengan nilai-semantik!)
GManNickG
2
Hei, aku akan tertarik dengan pendapat kalian tentang mengembalikan anggota shared_ptrkelas. Apakah Anda melakukannya dengan const-ref?
Johannes Schaub - litb
Kemungkinan ketiga adalah menggunakan std :: move () dengan C ++ 0x, ini menukar keduanya shared_ptr
Tomaka17
@ Johnannes: Saya akan mengembalikannya dengan referensi-referensi hanya untuk menghindari penyalinan / penghitungan ulang. Kemudian lagi, saya mengembalikan semua anggota dengan referensi-referensi kecuali mereka primitif.
GManNickG
mungkin duplikat dari C ++ - pertanyaan lewat pointer
kennytm

Jawaban:

229

Pertanyaan ini telah didiskusikan dan dijawab oleh Scott, Andrei dan Herb selama sesi Ask Us Anything di C ++ and Beyond 2011 . Tonton mulai 4:34 tentang shared_ptrkinerja dan kebenaran .

Singkatnya, tidak ada alasan untuk memberikan nilai, kecuali tujuannya adalah untuk berbagi kepemilikan atas suatu objek (mis. Antara struktur data yang berbeda, atau antara utas yang berbeda).

Kecuali Anda dapat menggerakkan-mengoptimalkannya seperti yang dijelaskan oleh Scott Meyers dalam video pembicaraan yang ditautkan di atas, tetapi itu terkait dengan versi aktual C ++ yang dapat Anda gunakan.

Pembaruan besar untuk diskusi ini telah terjadi selama Panel Interaktif konferensi GoingNative 2012 : Tanyakan Apa Pun ! yang layak ditonton, terutama dari 22:50 .

mloskot
sumber
5
tetapi seperti yang ditunjukkan di sini lebih murah untuk melewati nilai: stackoverflow.com/a/12002668/128384 seharusnya tidak diperhitungkan juga (setidaknya untuk argumen konstruktor dll di mana shared_ptr akan dibuat menjadi anggota dari kelas)?
stijn
2
@stijn Ya dan tidak. T&J yang Anda maksud tidak lengkap, kecuali jika itu menjelaskan versi standar C ++ yang dimaksud. Sangat mudah untuk menyebarkan aturan umum yang tidak pernah / selalu menyesatkan. Kecuali, pembaca membutuhkan waktu untuk membiasakan diri dengan artikel dan referensi David Abrahams, atau mempertimbangkan tanggal posting vs standar C ++ saat ini. Jadi, kedua jawaban, milik saya dan yang Anda tunjuk, sudah benar mengingat waktu posting.
mloskot
1
" kecuali ada multi-threading " tidak, MT sama sekali tidak istimewa.
curiousguy
3
Saya sangat terlambat ke pesta, tapi alasan saya ingin memberikan shared_ptr berdasarkan nilainya adalah membuat kode lebih pendek dan lebih cantik. Serius. Value*pendek dan mudah dibaca, tetapi buruk, jadi sekarang kode saya penuh const shared_ptr<Value>&dan secara signifikan lebih mudah dibaca dan hanya ... kurang rapi. Apa yang dulu void Function(Value* v1, Value* v2, Value* v3)sekarang void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3), dan orang-orang baik-baik saja dengan ini?
Alex
7
@Alex Praktek umum adalah membuat alias (typedef) tepat setelah kelas. Sebagai contoh Anda: class Value {...}; using ValuePtr = std::shared_ptr<Value>;Maka fungsi Anda menjadi lebih sederhana: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)dan Anda mendapatkan kinerja maksimal. Itu sebabnya Anda menggunakan C ++, bukan? :)
4LegsDrivenCat
92

Ini ramuan Herb Sutter

Pedoman: Jangan melewatkan pointer pintar sebagai parameter fungsi kecuali jika Anda ingin menggunakan atau memanipulasi pointer pintar itu sendiri, seperti untuk berbagi atau mentransfer kepemilikan.

Guideline: Mengungkapkan bahwa suatu fungsi akan menyimpan dan berbagi kepemilikan dari objek heap menggunakan parameter shared_ptr dengan-nilai.

Pedoman: Gunakan non-const shared_ptr & parameter hanya untuk memodifikasi shared_ptr. Gunakan const shared_ptr & sebagai parameter hanya jika Anda tidak yakin apakah Anda akan mengambil salinan dan berbagi kepemilikan; jika tidak gunakan widget * sebagai gantinya (atau jika tidak dapat dibatalkan, widget &).

acel
sumber
3
Terima kasih untuk tautannya ke Sutter. Ini adalah artikel yang sangat bagus. Saya tidak setuju dengannya di widget *, lebih memilih <widget &> opsional jika C ++ 14 tersedia. widget * terlalu ambigu dari kode lama.
Eponim
3
+1 untuk menyertakan widget * dan widget & sebagai kemungkinan. Hanya untuk menjelaskan, melewati widget * atau widget & mungkin merupakan opsi terbaik ketika fungsi tidak memeriksa / memodifikasi objek pointer itu sendiri. Antarmuka lebih umum, karena tidak memerlukan jenis pointer tertentu, dan masalah kinerja jumlah referensi shared_ptr dihindar.
tgnottingham
4
Saya pikir ini harus menjadi jawaban yang diterima hari ini, karena pedoman kedua. Itu jelas membatalkan jawaban yang diterima saat ini, yang mengatakan: tidak ada alasan untuk memberikan nilai.
mbrt
62

Secara pribadi saya akan menggunakan constreferensi. Tidak perlu menambah jumlah referensi hanya untuk mengurangi lagi demi pemanggilan fungsi.

Evan Teran
sumber
1
Saya tidak memberi suara rendah pada jawaban Anda, tetapi sebelum ini adalah masalah pilihan, ada pro dan kontra untuk masing-masing dari dua kemungkinan untuk dipertimbangkan. Dan akan lebih baik untuk mengetahui dan mendiskusikan pro dan kontra tesis ini. Setelah itu semua orang dapat membuat keputusan untuk dirinya sendiri.
Danvil
@Danvil: dengan mempertimbangkan cara shared_ptrkerjanya, satu-satunya downside yang mungkin terjadi jika tidak melewati referensi adalah sedikit penurunan kinerja. Ada dua penyebab di sini. a) fitur pointer aliasing berarti pointer bernilai data plus penghitung (mungkin 2 untuk referensi lemah) disalin, sehingga sedikit lebih mahal untuk menyalin ronde data. b) penghitungan referensi atom sedikit lebih lambat dari kode kenaikan / penurunan tua biasa, tetapi diperlukan agar benang aman. Di luar itu, kedua metode itu sama untuk sebagian besar maksud dan tujuan.
Evan Teran
37

Lulus dengan constreferensi, lebih cepat. Jika Anda perlu menyimpannya, katakan dalam beberapa wadah, ref. hitungan akan bertambah secara ajaib oleh operasi penyalinan.

Nikolai Fetissov
sumber
4
Downvote menjadi pendapatnya tanpa nomor untuk mendukungnya.
kwesolowski
22

Aku berlari kode di bawah ini, sekali dengan foomengambil shared_ptroleh const&dan lagi dengan foomengambil shared_ptrberdasarkan nilai.

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

Menggunakan VS2015, build rilis x86, pada prosesor intel core 2 quad (2.4GHz) saya

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

Salinan menurut versi nilai adalah urutan besarnya lebih lambat.
Jika Anda memanggil fungsi secara sinkron dari utas saat ini, lebih suka const&versinya.

tcb
sumber
1
Bisakah Anda mengatakan pengaturan kompiler, platform, dan optimisasi apa yang Anda gunakan?
Carlton
Saya menggunakan debug build vs2015, memperbarui jawaban untuk menggunakan build rilis sekarang.
tcb
1
Saya ingin tahu jika ketika optimasi dihidupkan, Anda mendapatkan hasil yang sama dengan keduanya
Elliot Woods
2
Optimalisasi tidak banyak membantu. masalahnya adalah pertikaian kunci pada jumlah referensi pada salinan.
Alex
1
Itu bukan intinya. foo()Fungsi seperti itu seharusnya bahkan tidak menerima pointer bersama di tempat pertama karena itu tidak menggunakan objek ini: itu harus menerima int&dan lakukan p = ++x;, memanggil foo(*p);dari main(). Fungsi menerima objek penunjuk pintar ketika perlu melakukan sesuatu dengannya, dan sebagian besar waktu, yang perlu Anda lakukan adalah memindahkannya ( std::move()) ke tempat lain, sehingga parameter nilai-per-satuan tidak memerlukan biaya.
eepp
15

Karena C ++ 11 Anda harus mengambilnya dengan nilai lebih dari const & lebih sering daripada yang Anda pikirkan.

Jika Anda menggunakan std :: shared_ptr (daripada tipe T yang mendasarinya), maka Anda melakukannya karena Anda ingin melakukan sesuatu dengannya.

Jika Anda ingin menyalinnya suatu tempat, lebih masuk akal untuk mengambilnya dengan menyalin, dan std :: memindahkannya secara internal, daripada mengambilnya dengan const & dan kemudian menyalinnya. Ini karena Anda membiarkan opsi penelepon pada gilirannya std :: memindahkan shared_ptr saat memanggil fungsi Anda, sehingga menghemat sendiri satu set operasi kenaikan dan penurunan. Atau tidak. Artinya, pemanggil fungsi dapat memutuskan apakah dia memerlukan std :: shared_ptr di sekitar setelah memanggil fungsi, dan tergantung pada apakah atau tidak bergerak atau tidak. Ini tidak dapat dicapai jika Anda melewati const &, dan karena itu lebih disukai untuk mengambilnya dengan nilai.

Tentu saja, jika pemanggil keduanya membutuhkan shared_ptr-nya lebih lama (sehingga tidak dapat std :: memindahkannya) dan Anda tidak ingin membuat salinan polos dalam fungsi (misalkan Anda ingin penunjuk yang lemah, atau Anda hanya kadang-kadang ingin untuk menyalinnya, tergantung pada beberapa kondisi), maka konst & mungkin masih lebih disukai.

Misalnya, Anda harus melakukannya

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

lebih

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

Karena dalam hal ini Anda selalu membuat salinan secara internal

Kue kering
sumber
1

Tidak tahu biaya waktu operasi copy shared_copy di mana peningkatan dan penurunan atom, saya menderita masalah penggunaan CPU yang jauh lebih tinggi. Saya tidak pernah menyangka peningkatan dan penurunan atom mungkin memakan banyak biaya.

Setelah hasil pengujian saya, kenaikan dan penurunan atom int32 membutuhkan 2 atau 40 kali lipat daripada kenaikan dan penurunan non-atom. Saya mendapatkannya di 3GHz Core i7 dengan Windows 8.1. Hasil pertama muncul ketika tidak ada pertengkaran yang terjadi, yang terakhir ketika kemungkinan pertengkaran tinggi terjadi. Saya ingat bahwa operasi atom pada kunci berbasis perangkat keras yang terakhir. Kunci adalah kunci. Buruk terhadap kinerja saat pertikaian terjadi.

Mengalami ini, saya selalu menggunakan byref (const shared_ptr &) daripada byval (shared_ptr).

Hyunjik Bae
sumber
1

Ada posting blog baru-baru ini: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

Jadi jawabannya adalah: Jangan (hampir) tidak pernah lewat const shared_ptr<T>&.
Cukup lulus kelas yang mendasarinya saja.

Pada dasarnya satu-satunya jenis parameter yang masuk akal adalah:

  • shared_ptr<T> - Ubah dan ambil kepemilikan
  • shared_ptr<const T> - Jangan modifikasi, ambil kepemilikan
  • T& - Ubah, tidak ada kepemilikan
  • const T& - Jangan modifikasi, tidak ada kepemilikan
  • T - Jangan modifikasi, tanpa kepemilikan, Murah untuk menyalin

Seperti @accel tunjukkan dalam https://stackoverflow.com/a/26197326/1930508 saran dari Herb Sutter adalah:

Gunakan const shared_ptr & sebagai parameter hanya jika Anda tidak yakin apakah Anda akan mengambil salinan dan berbagi kepemilikan

Tetapi dalam berapa kasus Anda tidak yakin? Jadi ini adalah situasi yang jarang terjadi

Api
sumber
0

Sudah diketahui masalah bahwa meneruskan shared_ptr menurut nilai memiliki biaya dan harus dihindari jika mungkin.

Biaya melewati dengan shared_ptr

Sebagian besar waktu lewat shared_ptr dengan referensi, dan bahkan lebih baik dengan referensi const, akan dilakukan.

Pedoman inti cpp memiliki aturan khusus untuk meneruskan shared_ptr

R.34: Ambil parameter shared_ptr untuk menyatakan bahwa suatu fungsi adalah pemilik bagian

void share(shared_ptr<widget>);            // share -- "will" retain refcount

Contoh kapan lewat shared_ptr menurut nilai sangat diperlukan adalah ketika pemanggil meneruskan objek yang dibagi ke callee asinkron - yaitu pemanggil keluar dari ruang lingkup sebelum callee menyelesaikan tugasnya. Callee harus "memperpanjang" masa pakai objek yang dibagikan dengan mengambil nilai share_ptr. Dalam hal ini, meneruskan referensi ke shared_ptr tidak akan berhasil.

Hal yang sama berlaku untuk melewatkan objek bersama ke utas kerja.

artm
sumber
-4

shared_ptr tidak cukup besar, konstruktor \ destruktornya juga tidak cukup bekerja agar ada overhead yang cukup dari salinan untuk peduli dengan referensi demi referensi vs demi kinerja copy.

stonemetal
sumber
15
Sudahkah Anda mengukurnya?
curiousguy
2
@stonemetal: Bagaimana dengan instruksi atom selama membuat shared_ptr baru?
Quarra
Ini adalah tipe non-POD, jadi di sebagian besar ABI bahkan melewatinya "berdasarkan nilai" sebenarnya melewati sebuah pointer. Bukan salinan sebenarnya byte yang merupakan masalah sama sekali. Seperti yang dapat Anda lihat dalam output asm, melewatkan shared_ptr<int>nilai berdasarkan membutuhkan lebih dari 100 instruksi x86 (termasuk lockinstruksi ed mahal untuk secara atom memasukkan / memutuskan jumlah referensi). Melewati oleh ref konstan sama dengan melewatkan pointer ke apa saja (dan dalam contoh ini pada explorer compiler Godbolt, optimasi tail-call mengubahnya menjadi jmp sederhana alih-alih panggilan: godbolt.org/g/TazMBU ).
Peter Cordes
TL: DR: Ini adalah C ++ di mana copy-constructor dapat melakukan lebih banyak pekerjaan daripada hanya menyalin byte. Jawaban ini total sampah.
Peter Cordes
2
stackoverflow.com/questions/3628081/share-ptr-horrible-speed Sebagai contoh, pointer yang dibagikan dilewatkan oleh nilai vs lulus dengan referensi, dia melihat perbedaan run time sekitar 33%. Jika Anda bekerja pada kode kritis kinerja maka pointer telanjang memberi Anda peningkatan kinerja yang lebih besar. Jadi, pastikan untuk melewati const ref jika Anda ingat tetapi itu bukan masalah besar jika Anda tidak melakukannya. Jauh lebih penting untuk tidak menggunakan shared_ptr jika Anda tidak membutuhkannya.
stonemetal