Meneruskan petunjuk bersama sebagai argumen

89

Jika saya mendeklarasikan objek yang dibungkus dengan pointer bersama:

std::shared_ptr<myClass> myClassObject(new myClass());

maka saya ingin menyampaikannya sebagai argumen untuk suatu metode:

DoSomething(myClassObject);

//the called method
void DoSomething(std::shared_ptr<myClass> arg1)
{
   arg1->someField = 4;
}

Apakah hal di atas hanya menambah jumlah referensi shared_pt dan semuanya keren? Atau apakah itu meninggalkan penunjuk yang menggantung?

Apakah Anda masih harus melakukan ini ?:

DoSomething(myClassObject.Get());

void DoSomething(std::shared_ptr<myClass>* arg1)
{
   (*arg1)->someField = 4;
}

Saya pikir cara ke-2 mungkin lebih efisien karena hanya harus menyalin 1 alamat (sebagai lawan dari seluruh penunjuk cerdas), tetapi cara pertama tampaknya lebih mudah dibaca dan saya tidak mengantisipasi mendorong batas kinerja. Saya hanya ingin memastikan tidak ada yang berbahaya tentang itu.

Terima kasih.

Steve H.
sumber
14
const std::shared_ptr<myClass>& arg1
Captain Obvlious
3
Cara kedua dipatahkan, yang pertama idiomatik jika Anda benar-benar membutuhkan fungsi Anda untuk berbagi kepemilikan. Tapi, DoSomethingbenarkah perlu berbagi kepemilikan? Sepertinya ini hanya mengambil referensi saja ...
ildjarn
8
@SteveH: Tidak, tapi mengapa fungsi Anda harus memaksa semantik kepemilikan objek khusus pada pemanggilnya jika sebenarnya tidak membutuhkannya? Fungsi Anda tidak melakukan apa pun yang tidak lebih baik dari void DoSomething(myClass& arg1).
ildjarn
2
@SteveH: Tujuan keseluruhan dari smart pointer adalah untuk menangani masalah kepemilikan objek yang tidak biasa - jika Anda tidak memilikinya, Anda tidak boleh menggunakan smart pointer sejak awal. ; -] shared_ptr<>Khususnya, Anda harus meneruskan nilai untuk benar-benar berbagi kepemilikan.
ildjarn
4
Tidak terkait: secara umum, std::make_sharedtidak hanya lebih efisien, tetapi juga lebih aman daripada std::shared_ptrkonstruktor.
R. Martinho Fernandes

Jawaban:

171

Saya ingin meneruskan penunjuk bersama ke suatu fungsi. Bisakah Anda membantu saya dengan itu?

Tentu, saya bisa bantu soal itu. Saya berasumsi Anda memiliki pemahaman tentang semantik kepemilikan di C ++. Benarkah itu?

Ya, saya cukup nyaman dengan subjek ini.

Baik.

Oke, saya hanya bisa memikirkan dua alasan untuk shared_ptrberargumen:

  1. Fungsi ingin berbagi kepemilikan objek;
  2. Fungsi ini melakukan beberapa operasi yang bekerja secara spesifik pada shared_ptrs.

Yang mana yang Anda minati?

Saya mencari jawaban umum, jadi saya sebenarnya tertarik pada keduanya. Saya ingin tahu tentang apa yang Anda maksud dalam kasus # 2.

Contoh dari fungsi tersebut termasuk std::static_pointer_cast, pembanding kustom, atau predikat. Misalnya, jika Anda perlu menemukan semua shared_ptr unik dari vektor, Anda memerlukan predikat seperti itu.

Ah, ketika fungsinya benar-benar perlu memanipulasi smart pointer itu sendiri.

Persis.

Dalam hal ini, saya pikir kita harus melewati referensi.

Iya. Dan jika itu tidak mengubah penunjuk, Anda ingin melewati referensi const. Tidak perlu menyalin karena Anda tidak perlu berbagi kepemilikan. Itu skenario lainnya.

OK mengerti. Mari kita bicara tentang skenario lainnya.

Di mana Anda berbagi kepemilikan? Baik. Bagaimana Anda berbagi kepemilikan dengan shared_ptr?

Dengan menyalinnya.

Maka fungsi tersebut perlu membuat salinan dari a shared_ptr, benar?

Jelas sekali. Jadi saya meneruskannya dengan referensi ke const dan menyalin ke variabel lokal?

Tidak, itu pesimisasi. Jika diteruskan oleh referensi, fungsi tidak punya pilihan selain membuat salinan secara manual. Jika itu dilewatkan oleh nilai kompilator akan memilih pilihan terbaik antara salinan dan pemindahan dan melakukannya secara otomatis. Jadi, lewati nilai.

Poin yang bagus. Saya harus lebih sering mengingat artikel " Want Speed? Pass by Value ".

Tunggu, bagaimana jika fungsi tersebut menyimpan shared_ptrdalam variabel anggota, misalnya? Bukankah itu akan membuat salinan yang berlebihan?

Fungsi tersebut dapat dengan mudah memindahkan shared_ptrargumen ke penyimpanannya. Memindahkan shared_ptrmurah karena tidak mengubah jumlah referensi apa pun.

Ah, ide bagus.

Tetapi saya memikirkan skenario ketiga: bagaimana jika Anda tidak ingin memanipulasi shared_ptr, atau berbagi kepemilikan?

Dalam hal ini, shared_ptrsama sekali tidak relevan dengan fungsinya. Jika Anda ingin memanipulasi orang yang ditunjuk, ambil orang yang ditunjuk, dan biarkan penelepon memilih semantik kepemilikan yang mereka inginkan.

Dan haruskah saya mengambil poin tersebut dengan referensi atau nilai?

Aturan biasa berlaku. Petunjuk cerdas tidak mengubah apa pun.

Lewati nilai jika saya akan menyalin, meneruskan referensi jika saya ingin menghindari salinan.

Baik.

Hmm. Saya pikir Anda lupa skenario lain. Bagaimana jika saya ingin berbagi kepemilikan, tetapi hanya bergantung pada kondisi tertentu?

Ah, kasus tepi yang menarik. Saya tidak berharap itu sering terjadi. Tetapi ketika itu terjadi, Anda dapat meneruskan nilai dan mengabaikan salinannya jika Anda tidak membutuhkannya, atau meneruskan referensi dan membuat salinannya jika Anda membutuhkannya.

Saya mengambil risiko satu salinan yang berlebihan di opsi pertama, dan kehilangan potensi perpindahan di opsi kedua. Tidak bisakah saya makan kue dan memakannya juga?

Jika Anda berada dalam situasi di mana itu benar-benar penting, Anda dapat memberikan dua kelebihan, satu mengambil referensi nilai konstanta, dan yang lainnya mengambil referensi nilai r. Satu salinan, yang lainnya bergerak. Templat fungsi penerusan sempurna adalah opsi lain.

Saya pikir itu mencakup semua kemungkinan skenario. Terima kasih banyak.

R. Martinho Fernandes
sumber
2
@ Jon: Untuk apa? Ini adalah semua tentang saya harus melakukan A atau harus saya lakukan B . Saya pikir kita semua tahu cara melewatkan objek dengan nilai / referensi, bukan?
sbi
9
Karena prosa seperti "Apakah itu berarti saya akan meneruskannya dengan referensi ke const untuk membuat salinan? Tidak, itu pesimisasi" membingungkan seorang pemula. Meneruskan referensi ke const tidak membuat salinan.
Jon
1
@Martinho Saya tidak setuju dengan aturan "Jika Anda ingin memanipulasi orang yang ditunjuk, ambil orang yang ditunjuk,". Seperti yang dinyatakan dalam jawaban ini, tidak mengambil kepemilikan sementara atas suatu objek bisa berbahaya. Menurut pendapat saya, defaultnya adalah meneruskan salinan untuk kepemilikan sementara untuk menjamin validitas objek selama pemanggilan fungsi.
radman
@radman Tidak ada skenario di jawaban yang Anda tautkan untuk menerapkan aturan itu, jadi ini sama sekali tidak relevan. Tolong tulis saya SSCCE di mana Anda melewati referensi dari shared_ptr<T> ptr;(yaitu void f(T&), dipanggil dengan f(*ptr)) dan objek tidak hidup lebih lama dari panggilan tersebut. Atau tulis satu di mana Anda melewati nilai void f(T)dan masalah muncul.
R. Martinho Fernandes
@Martinho lihat kode contoh saya untuk contoh mandiri sederhana yang benar. Contohnya adalah untuk void f(T&)dan melibatkan utas. Saya pikir masalah saya adalah bahwa nada jawaban Anda menghalangi kepemilikan shared_ptr's, yang merupakan cara paling aman, paling intuitif dan paling tidak rumit untuk menggunakannya. Saya akan sangat tidak menyarankan seorang pemula untuk mengekstrak referensi ke data yang dimiliki oleh shared_ptr <> kemungkinan penyalahgunaannya bagus dan manfaatnya minimal.
radman
22

Saya pikir orang tidak perlu takut menggunakan pointer mentah sebagai parameter fungsi. Jika fungsi tidak akan menyimpan penunjuk atau mempengaruhi masa pakainya, penunjuk mentah bekerja dengan baik dan mewakili penyebut terkecil yang sama. Pertimbangkan misalnya bagaimana Anda akan meneruskan a unique_ptrke dalam fungsi yang mengambil a shared_ptrsebagai parameter, baik dengan nilai atau referensi const?

void DoSomething(myClass * p);

DoSomething(myClass_shared_ptr.get());
DoSomething(myClass_unique_ptr.get());

Pointer mentah sebagai parameter fungsi tidak mencegah Anda menggunakan smart pointer dalam kode panggilan, yang sangat penting.

Mark Ransom
sumber
8
Jika Anda menggunakan pointer, mengapa tidak menggunakan referensi saja? DoSomething(*a_smart_ptr)
Xeo
5
@ Xeo, Anda benar - itu akan menjadi lebih baik. Terkadang Anda perlu mengizinkan kemungkinan penunjuk NULL.
Mark Ransom
1
Jika Anda memiliki konvensi untuk menggunakan pointer mentah sebagai parameter output, lebih mudah untuk menemukan kode panggilan bahwa variabel dapat berubah, misalnya: bandingkan fun(&x)dengan fun(x). Dalam contoh yang terakhir ini xbisa baik dilewatkan dengan nilai atau sebagai ref const. Ini akan seperti yang dinyatakan di atas juga memungkinkan Anda untuk lulus nullptrjika Anda tidak tertarik dengan output ...
Andreas Magnusson
2
@AndreasMagnusson: Konvensi pointer-as-output-parameter dapat memberikan rasa aman yang salah saat menangani pointer yang diteruskan dari sumber lain. Karena dalam kasus ini panggilannya menyenangkan (x) dan * x dimodifikasi dan sumber tidak memiliki & x untuk memperingatkan Anda.
Zan Lynx
bagaimana jika pemanggilan fungsi melebihi cakupan pemanggil? Ada kemungkinan bahwa destruktor ptr bersama telah dipanggil, dan sekarang fungsinya akan mencoba mengakses memori yang dihapus, mengakibatkan UB
Sridhar Thiagarajan
4

Ya, keseluruhan ide tentang shared_ptr <> adalah bahwa beberapa instance dapat menampung pointer mentah yang sama dan memori yang mendasarinya hanya akan dibebaskan ketika instance terakhir shared_ptr <> dihancurkan.

Saya akan menghindari pointer ke shared_ptr <> karena itu mengalahkan tujuan karena Anda sekarang berurusan dengan raw_pointers lagi.

R Samuel Klatchko
sumber
2

Nilai lewat dalam contoh pertama Anda aman, tetapi ada idiom yang lebih baik. Lewati referensi const bila memungkinkan - saya akan mengatakan ya bahkan ketika berhadapan dengan petunjuk cerdas. Contoh kedua Anda tidak benar-benar rusak tetapi sangat !???. Konyol, tidak mencapai apa-apa dan mengalahkan sebagian dari inti dari petunjuk cerdas, dan akan meninggalkan Anda dalam dunia kesakitan saat Anda mencoba untuk membedakan dan memodifikasi sesuatu.

djechlin.dll
sumber
3
Tidak ada yang menganjurkan lewat pointer, jika yang Anda maksud pointer mentah. Melewati referensi jika tidak ada perselisihan kepemilikan tentu idiomatik. Intinya, terkait shared_ptr<>secara khusus, Anda harus membuat salinan untuk benar-benar berbagi kepemilikan; jika Anda memiliki referensi atau penunjuk ke a shared_ptr<>maka Anda tidak berbagi apa pun, dan tunduk pada masalah seumur hidup yang sama seperti referensi atau penunjuk normal.
ildjarn
2
@ildjarn: Meneruskan referensi konstan diperbolehkan dalam kasus ini. Asli shared_ptrpemanggil dijamin hidup lebih lama dr panggilan fungsi, sehingga fungsi tersebut aman dalam menggunakan shared_ptr<> const &. Jika penunjuk perlu disimpan untuk lain waktu, penunjuk perlu disalin (pada titik ini salinan harus dibuat, bukan memegang referensi), tetapi tidak perlu mengeluarkan biaya penyalinan kecuali Anda perlu melakukannya Itu. Saya akan pergi untuk melewati referensi konstan dalam kasus ini ....
David Rodríguez - dribeas
3
@ David: " Meneruskan referensi konstan tidak masalah dalam kasus ini. " Tidak dalam API apa pun yang waras - mengapa API mengamanatkan jenis penunjuk cerdas yang bahkan tidak memanfaatkannya daripada hanya mengambil referensi konstanta biasa? Itu hampir sama buruknya dengan kurangnya ketelitian. Jika maksud Anda secara teknis tidak merugikan apa pun, maka saya pasti setuju, tetapi menurut saya itu bukan hal yang benar untuk dilakukan.
ildjarn
2
... jika di sisi lain, fungsi tersebut tidak perlu memperpanjang umur objek, maka Anda dapat menghapusnya shared_ptrdari antarmuka dan meneruskan constreferensi plain ( ) ke objek yang dituju.
David Rodríguez - dribeas
3
@David: Bagaimana jika konsumen API Anda tidak menggunakan shared_ptr<>untuk memulai? Sekarang API Anda benar-benar hanya merepotkan, karena memaksa pemanggil untuk mengubah semantik seumur hidup objek mereka tanpa tujuan hanya untuk menggunakannya. Saya tidak melihat sesuatu yang berharga untuk diadvokasi, selain mengatakan "secara teknis tidak merugikan apa pun." Saya tidak melihat sesuatu yang kontroversial tentang 'jika Anda tidak membutuhkan kepemilikan bersama, jangan gunakan shared_ptr<>'.
ildjarn
0

dalam fungsi DoSomething Anda, Anda mengubah anggota data dari sebuah instance kelas myClass jadi apa yang Anda ubah adalah objek yang dikelola (pointer mentah) bukan objek (shared_ptr). Yang berarti pada titik balik fungsi ini semua pointer yang dibagikan ke pointer mentah terkelola akan melihat anggota datanya: myClass::someFielddiubah ke nilai yang berbeda.

dalam hal ini, Anda meneruskan objek ke fungsi dengan jaminan bahwa Anda tidak mengubahnya (berbicara tentang shared_ptr, bukan objek yang dimiliki).

Ungkapan untuk mengungkapkan ini adalah melalui: ref const, seperti itu

void DoSomething(const std::shared_ptr<myClass>& arg)

Demikian pula Anda memastikan pengguna fungsi Anda, bahwa Anda tidak menambahkan pemilik lain ke daftar pemilik penunjuk mentah. Namun, Anda tetap memiliki kemungkinan untuk mengubah objek pokok yang ditunjukkan oleh penunjuk mentah.

CAVEAT: Artinya, jika seseorang memanggil shared_ptr::resetsebelum Anda memanggil fungsi Anda, dan pada saat itu adalah shared_ptr terakhir yang memiliki raw_ptr, maka objek Anda akan dimusnahkan, dan fungsi Anda akan memanipulasi Pointer yang menggantung ke objek yang dihancurkan. SANGAT BERBAHAYA!!!

wanita organik
sumber