TL; DR: Lewati referensi const masih merupakan ide bagus di C ++, semua hal dipertimbangkan. Bukan optimasi prematur.
TL; DR2: Sebagian besar pepatah tidak masuk akal, sampai mereka melakukannya.
Tujuan
Jawaban ini hanya mencoba untuk memperpanjang item yang ditautkan pada Pedoman Inti C ++ (pertama kali disebutkan dalam komentar amon) sedikit.
Jawaban ini tidak mencoba untuk mengatasi masalah bagaimana berpikir dan menerapkan dengan baik berbagai pepatah yang beredar luas di kalangan programmer, terutama masalah rekonsiliasi antara kesimpulan atau bukti yang saling bertentangan.
Penerapan
Jawaban ini berlaku untuk panggilan fungsi (cakupan bersarang yang tidak dapat dilepas pada utas yang sama) saja.
(Catatan tambahan.) Ketika hal-hal yang dapat dilewati dapat lolos dari ruang lingkup (yaitu memiliki masa pakai yang berpotensi melebihi lingkup luar), menjadi lebih penting untuk memenuhi kebutuhan aplikasi akan manajemen seumur hidup objek sebelum hal lain. Biasanya, ini memerlukan menggunakan referensi yang juga mampu manajemen seumur hidup, seperti pointer pintar. Alternatif mungkin menggunakan manajer. Perhatikan bahwa, lambda adalah semacam ruang lingkup yang bisa dilepas; menangkap lambda berperilaku seperti memiliki cakupan objek. Karena itu, berhati-hatilah dengan tangkapan lambda. Juga berhati-hatilah dengan bagaimana lambda itu sendiri diteruskan - dengan salinan atau referensi.
Kapan melewati nilai
Untuk nilai-nilai yang skalar (primitif standar yang sesuai dengan register mesin dan memiliki semantik nilai) yang tidak perlu komunikasi-oleh-mutabilitas (referensi bersama), berikan nilai.
Untuk situasi di mana callee membutuhkan kloning objek atau agregat, berikan nilai, di mana salinan callee memenuhi kebutuhan objek kloning.
Kapan melewati referensi, dll.
untuk semua situasi lain, lewati petunjuk, referensi, petunjuk cerdas, pegangan (lihat: idiom pegangan-tubuh), dll. Setiap kali nasihat ini diikuti, terapkan prinsip koreksi-kebenaran seperti biasa.
Benda-benda (agregat, objek, susunan, struktur data) yang cukup besar dalam tapak memori harus selalu dirancang untuk memfasilitasi referensi demi referensi, untuk alasan kinerja. Nasihat ini jelas berlaku ketika jumlahnya ratusan byte atau lebih. Saran ini adalah garis batas ketika puluhan byte.
Paradigma yang tidak biasa
Ada paradigma pemrograman tujuan khusus yang beratnya disalin oleh niat. Misalnya, pemrosesan string, serialisasi, komunikasi jaringan, isolasi, pembungkus perpustakaan pihak ketiga, komunikasi antar-memori berbagi-memori, dll. Dalam area aplikasi atau paradigma pemrograman ini, data disalin dari struct ke struct, atau kadang-kadang dikemas kembali ke dalam array byte.
Bagaimana spesifikasi bahasa memengaruhi jawaban ini, sebelum optimasi dipertimbangkan.
Sub-TL; DR Menyebarluaskan referensi tidak boleh meminta kode; melewati referensi-referensi memenuhi kriteria ini. Namun, semua bahasa lain memenuhi kriteria ini dengan mudah.
(Pemrogram C ++ pemula disarankan untuk melewati bagian ini sepenuhnya.)
(Awal bagian ini sebagian diilhami oleh jawaban gnasher729. Namun, kesimpulan yang berbeda tercapai.)
C ++ memungkinkan copy constructor dan operator penugasan yang ditentukan pengguna.
(Ini adalah pilihan berani yang menakjubkan dan disesalkan. Ini jelas merupakan perbedaan dari norma yang dapat diterima saat ini dalam desain bahasa.)
Bahkan jika programmer C ++ tidak mendefinisikan satu, kompiler C ++ harus menghasilkan metode seperti itu berdasarkan pada prinsip-prinsip bahasa, dan kemudian menentukan apakah kode tambahan perlu dieksekusi selain memcpy
. Misalnya, a class
/ struct
yang mengandung astd::vector
anggota harus memiliki copy-constructor dan operator penugasan yang non-sepele.
Dalam bahasa lain, konstruktor salin dan kloning objek tidak disarankan (kecuali jika benar-benar diperlukan dan / atau bermakna bagi semantik aplikasi), karena objek memiliki semantik referensi, berdasarkan desain bahasa. Bahasa-bahasa ini biasanya akan memiliki mekanisme pengumpulan sampah yang didasarkan pada jangkauan alih-alih kepemilikan berbasis lingkup atau penghitungan referensi.
Ketika referensi atau pointer (termasuk referensi const) diteruskan dalam C ++ (atau C), programmer yakin bahwa tidak ada kode khusus (fungsi yang ditentukan pengguna atau kompiler yang dihasilkan) yang akan dieksekusi, selain penyebaran nilai alamat (referensi atau penunjuk). Ini adalah kejelasan perilaku yang menurut programmer C ++ nyaman.
Namun, latar belakangnya adalah bahwa bahasa C ++ tidak rumit, sehingga kejelasan perilaku ini seperti oasis (habitat yang dapat bertahan) di suatu tempat di sekitar zona kejatuhan nuklir.
Untuk menambahkan lebih banyak berkah (atau penghinaan), C ++ memperkenalkan referensi universal (nilai-r) untuk memfasilitasi operator pemindahan yang ditentukan pengguna (pemindah-konstruktor dan operator pemindah-pindah) dengan kinerja yang baik. Ini menguntungkan penggunaan kasus yang sangat relevan (memindahkan (mentransfer) objek dari satu contoh ke yang lain), dengan cara mengurangi kebutuhan untuk menyalin dan kloning mendalam. Namun, dalam bahasa lain, tidak masuk akal untuk berbicara tentang perpindahan benda semacam itu.
(Bagian di luar topik) Bagian yang didedikasikan untuk artikel, "Ingin Kecepatan? Lewati Nilai!" ditulis sekitar tahun 2009.
Artikel itu ditulis pada tahun 2009 dan menjelaskan alasan desain untuk nilai-r dalam C ++. Artikel itu menyajikan argumen balasan yang valid untuk kesimpulan saya di bagian sebelumnya. Namun, contoh kode artikel dan klaim kinerja telah lama ditolak.
Sub-TL; DR Desain semantik nilai-r dalam C ++ memungkinkan semantik sisi-pengguna yang elegan pada sebuahSort
fungsi, misalnya. Elegan ini tidak mungkin untuk model (meniru) dalam bahasa lain.
Fungsi pengurutan diterapkan pada keseluruhan struktur data. Seperti disebutkan di atas, akan lambat jika banyak penyalinan terlibat. Sebagai pengoptimalan kinerja (yang praktis relevan), fungsi pengurutan dirancang untuk bersifat merusak dalam beberapa bahasa selain C ++. Merusak berarti bahwa struktur data target dimodifikasi untuk mencapai tujuan penyortiran.
Dalam C ++, pengguna dapat memilih untuk memanggil salah satu dari dua implementasi: yang destruktif dengan kinerja yang lebih baik, atau yang normal yang tidak mengubah input. (Templat dihilangkan karena singkatnya.)
/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
std::vector<T> result(std::move(input)); /* destructive move */
std::sort(result.begin(), result.end()); /* in-place sorting */
return result; /* return-value optimization (RVO) */
}
/*caller specifically passes in read-only argument*/
std::vector<T> my_sort(const std::vector<T>& input)
{
/* reuse destructive implementation by letting it work on a clone. */
/* Several things involved; e.g. expiring temporaries as r-value */
/* return-value optimization, etc. */
return my_sort(std::vector<T>(input));
}
/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/
Selain penyortiran, keanggunan ini juga berguna dalam penerapan algoritma pencarian median destruktif dalam array (awalnya tidak disortir), dengan partisi rekursif.
Namun, perhatikan bahwa, sebagian besar bahasa akan menerapkan pendekatan pohon pencarian biner seimbang untuk menyortir, alih-alih menerapkan algoritma penyortiran destruktif ke array. Oleh karena itu, relevansi praktis dari teknik ini tidak setinggi kelihatannya.
Bagaimana optimasi kompiler memengaruhi jawaban ini
Ketika inlining (dan juga optimasi seluruh program / tautan-waktu) diterapkan di beberapa level pemanggilan fungsi, kompiler dapat melihat (kadang-kadang secara mendalam) aliran data. Ketika ini terjadi, kompiler dapat menerapkan banyak optimasi, beberapa di antaranya dapat menghilangkan penciptaan seluruh objek dalam memori. Biasanya, ketika situasi ini berlaku, tidak masalah jika parameter dilewatkan oleh nilai atau oleh referensi-konstanta, karena kompiler dapat menganalisis secara mendalam.
Namun, jika fungsi tingkat bawah memanggil sesuatu yang di luar analisis (misalnya sesuatu di pustaka yang berbeda di luar kompilasi, atau grafik panggilan yang terlalu rumit), maka kompiler harus mengoptimalkan secara defensif.
Objek yang lebih besar dari nilai register mesin dapat disalin dengan instruksi memuat / menyimpan memori eksplisit, atau dengan panggilan ke memcpy
fungsi terhormat . Pada beberapa platform, kompiler menghasilkan instruksi SIMD untuk berpindah di antara dua lokasi memori, setiap instruksi bergerak puluhan byte (16 atau 32).
Diskusi tentang masalah verbositas atau kekacauan visual
Pemrogram C ++ terbiasa dengan hal ini, yaitu selama seorang programmer tidak membenci C ++, overhead penulisan atau membaca referensi-referensi dalam kode sumber tidak mengerikan.
Analisis biaya-manfaat mungkin telah dilakukan berkali-kali sebelumnya. Saya tidak tahu apakah ada yang ilmiah yang harus dikutip. Saya kira sebagian besar analisis akan non-ilmiah atau tidak dapat direproduksi.
Inilah yang saya bayangkan (tanpa bukti atau referensi yang kredibel) ...
- Ya, itu mempengaruhi kinerja perangkat lunak yang ditulis dalam bahasa ini.
- Jika kompiler dapat memahami tujuan kode, itu berpotensi cukup pintar untuk mengotomatisasi itu
- Sayangnya, dalam bahasa yang mendukung mutabilitas (sebagai lawan kemurnian fungsional), kompiler akan mengklasifikasikan sebagian besar hal sebagai yang termutasi, oleh karena itu deduksi otomatis dari constness akan menolak sebagian besar hal sebagai non-konstanta.
- Overhead mental tergantung pada orang; orang-orang yang menganggap ini sebagai overhead mental yang tinggi akan menolak C ++ sebagai bahasa pemrograman yang layak.