Bagaimana cara mengembalikan smart pointers (shared_ptr), dengan referensi atau nilai?

95

Katakanlah saya memiliki kelas dengan metode yang mengembalikan a shared_ptr.

Apa keuntungan dan kerugian yang mungkin didapat dari mengembalikannya berdasarkan referensi atau nilai?

Dua petunjuk yang mungkin:

  • Penghancuran objek awal. Jika saya mengembalikan shared_ptrreferensi by (const), penghitung referensi tidak bertambah, jadi saya menanggung risiko objek dihapus ketika keluar dari ruang lingkup dalam konteks lain (misalnya utas lain). Apakah ini benar? Bagaimana jika lingkungannya berulir tunggal, dapatkah situasi ini terjadi juga?
  • Biaya. Pass-by-value tentunya tidak gratis. Apakah layak menghindarinya jika memungkinkan?

Terimakasih semuanya.

Vincenzo Pii
sumber

Jawaban:

115

Kembalikan petunjuk cerdas dengan nilai.

Seperti yang Anda katakan, jika Anda mengembalikannya dengan referensi, Anda tidak akan menaikkan jumlah referensi dengan benar, yang membuka risiko menghapus sesuatu pada waktu yang tidak tepat. Itu saja sudah cukup menjadi alasan untuk tidak kembali dengan referensi. Antarmuka harus kuat.

Masalah biaya saat ini diperdebatkan berkat pengoptimalan nilai pengembalian (RVO), jadi Anda tidak akan menimbulkan urutan kenaikan-kenaikan-penurunan atau sesuatu seperti itu di kompiler modern. Jadi cara terbaik untuk mengembalikan a shared_ptradalah dengan hanya mengembalikan dengan nilai:

shared_ptr<T> Foo()
{
    return shared_ptr<T>(/* acquire something */);
};

Ini adalah peluang RVO yang sangat jelas untuk kompiler C ++ modern. Saya tahu pasti bahwa kompiler Visual C ++ menerapkan RVO bahkan ketika semua pengoptimalan dimatikan. Dan dengan semantik move C ++ 11, kekhawatiran ini menjadi kurang relevan. (Tetapi satu-satunya cara untuk memastikan adalah membuat profil dan bereksperimen.)

Jika Anda masih belum yakin, Dave Abrahams sudah yakin artikel yang membuat argumen untuk dikembalikan berdasarkan nilai. Saya mereproduksi potongan di sini; Saya sangat menyarankan Anda membaca seluruh artikel:

Jujurlah: bagaimana kode berikut memengaruhi perasaan Anda?

std::vector<std::string> get_names();
...
std::vector<std::string> const names = get_names();

Terus terang, meskipun saya harus lebih tahu, itu membuat saya gugup. Pada prinsipnya, ketika get_names() kembali, kita harus menyalin vectordaristring s. Kemudian, kita perlu menyalinnya lagi saat kita menginisialisasi names, dan kita perlu menghancurkan salinan pertama. Jika ada N stringdalam vektor, setiap salinan dapat memerlukan alokasi memori N + 1 dan sejumlah besar akses data tidak ramah cache> saat konten string disalin.

Alih-alih menghadapi kecemasan semacam itu, saya sering mengabaikan referensi untuk menghindari salinan yang tidak perlu:

get_names(std::vector<std::string>& out_param );
...
std::vector<std::string> names;
get_names( names );

Sayangnya, pendekatan ini jauh dari ideal.

  • Kode tumbuh 150%
  • Kami harus turun const -ness karena kami mengubah nama.
  • Seperti yang sering diingatkan oleh programmer fungsional kepada kita, mutasi membuat kode lebih kompleks untuk dipikirkan dengan merusak transparansi referensial dan penalaran persamaan.
  • Kami tidak lagi memiliki semantik nilai ketat untuk nama.

Tetapi apakah benar-benar perlu mengacaukan kode kita dengan cara ini untuk mendapatkan efisiensi? Untungnya, jawabannya ternyata tidak (dan terutama tidak jika Anda menggunakan C ++ 0x).

In silico
sumber
Saya tidak tahu bahwa saya akan mengatakan RVO membuat pertanyaan diperdebatkan karena kembali dengan referensi jelas membuat RVO tidak mungkin.
Edward Strange
@CrazyEddie: Benar, itulah salah satu alasan mengapa saya merekomendasikan OP mengembalikan nilai.
In silico
Apakah aturan RVO, yang diizinkan oleh standar, mengalahkan aturan tentang sinkronisasi / hubungan yang terjadi sebelum, yang dijamin oleh standar?
edA-qa mort-ora-y
1
@ edA-qa mort-ora-y: RVO secara eksplisit diizinkan meskipun memiliki efek samping. Misalnya, jika Anda memiliki cout << "Hello World!";pernyataan dalam default dan copy konstruktor, Anda tidak akan melihat dua Hello World!saat RVO diterapkan. Namun, ini seharusnya tidak menjadi masalah untuk penunjuk cerdas yang dirancang dengan benar, bahkan sinkronisasi wrt.
In silico
23

Mengenai apapun pointer pintar (bukan hanya shared_ptr), saya tidak berpikir itu pernah diterima kembali referensi ke salah satu, dan saya akan sangat ragu-ragu untuk melewati mereka sekitar dengan referensi atau pointer mentah. Mengapa? Karena Anda tidak dapat memastikan bahwa itu tidak akan disalin secara dangkal melalui referensi nanti. Poin pertama Anda menjelaskan alasan mengapa hal ini harus menjadi perhatian. Ini dapat terjadi bahkan di lingkungan single-threaded. Anda tidak memerlukan akses bersamaan ke data untuk menempatkan semantik salinan yang buruk di program Anda. Anda tidak benar-benar mengontrol apa yang dilakukan pengguna Anda dengan penunjuk setelah Anda memberikannya, jadi jangan mendorong penyalahgunaan untuk memberi pengguna API cukup tali untuk menggantung diri.

Kedua, lihat implementasi smart pointer Anda, jika memungkinkan. Konstruksi dan penghancuran harus hampir dapat diabaikan. Jika overhead ini tidak dapat diterima, maka jangan gunakan smart pointer! Namun di luar ini, Anda juga perlu memeriksa arsitektur konkurensi yang Anda miliki, karena akses yang saling eksklusif ke mekanisme yang melacak penggunaan penunjuk akan memperlambat Anda lebih dari sekadar konstruksi objek shared_ptr.

Sunting, 3 tahun kemudian: dengan munculnya fitur yang lebih modern di C ++, saya akan mengubah jawaban saya menjadi lebih menerima kasus ketika Anda hanya menulis lambda yang tidak pernah berada di luar ruang lingkup fungsi panggilan, dan tidak disalin di tempat lain. Di sini, jika Anda ingin menghemat overhead yang sangat minimal untuk menyalin penunjuk bersama, itu akan adil dan aman. Mengapa? Karena Anda dapat menjamin bahwa referensi tersebut tidak akan pernah disalahgunakan.

San Jacinto
sumber