Apakah lebih baik di C ++ untuk melewati nilai atau lulus dengan referensi konstan?

Jawaban:

203

Dulu umumnya direkomendasikan praktek terbaik 1 untuk penggunaan lewat ref const untuk semua jenis , kecuali untuk builtin jenis ( char, int, double, dll), untuk iterator dan untuk benda fungsi (lambdas, kelas berasal dari std::*_function).

Ini terutama benar sebelum keberadaan semantik bergerak . Alasannya sederhana: jika Anda melewati nilai, salinan objek harus dibuat dan, kecuali untuk objek yang sangat kecil, ini selalu lebih mahal daripada melewati referensi.

Dengan C ++ 11, kami telah mendapatkan semantik bergerak . Singkatnya, memindahkan semantik memungkinkan bahwa, dalam beberapa kasus, suatu objek dapat dilewati "dengan nilai" tanpa menyalinnya. Secara khusus, hal ini terjadi ketika objek yang Anda melewati sebuah nilai p .

Dalam dirinya sendiri, memindahkan objek masih setidaknya semahal melewati referensi. Namun, dalam banyak kasus suatu fungsi akan secara internal menyalin objek - yaitu akan mengambil kepemilikan dari argumen. 2

Dalam situasi ini kami memiliki pertukaran (disederhanakan) sebagai berikut:

  1. Kita dapat melewati objek dengan referensi, lalu menyalin secara internal.
  2. Kita bisa melewati objek dengan nilai.

"Pass by value" masih menyebabkan objek disalin, kecuali jika objek tersebut adalah nilai. Dalam hal nilai, objek dapat dipindahkan sebagai gantinya, sehingga kasus kedua tiba-tiba tidak lagi "menyalin, lalu memindahkan" tetapi "bergerak, lalu (berpotensi) bergerak lagi".

Untuk objek besar yang menerapkan konstruktor pemindahan yang tepat (seperti vektor, string ...), case kedua jauh lebih efisien daripada yang pertama. Oleh karena itu, disarankan untuk menggunakan pass by value jika fungsi tersebut mengambil kepemilikan dari argumen, dan jika jenis objek mendukung perpindahan efisien .


Catatan sejarah:

Bahkan, setiap kompiler modern harus dapat mencari tahu ketika melewati nilai mahal, dan secara implisit mengkonversi panggilan untuk menggunakan referensi ref jika memungkinkan.

Dalam teori. Dalam praktiknya, kompiler tidak selalu dapat mengubah ini tanpa merusak antarmuka biner fungsi. Dalam beberapa kasus khusus (ketika fungsi ini digarisbawahi) salinan akan benar-benar hilang jika kompiler dapat mengetahui bahwa objek asli tidak akan diubah melalui tindakan dalam fungsi.

Tetapi secara umum kompiler tidak dapat menentukan ini, dan munculnya semantik bergerak di C ++ telah membuat optimasi ini jauh kurang relevan.


1 Misalnya dalam Scott Meyers, Efektif C ++ .

2 Ini sering benar untuk konstruktor objek, yang dapat mengambil argumen dan menyimpannya secara internal untuk menjadi bagian dari keadaan objek yang dikonstruksi.

Konrad Rudolph
sumber
hmmm ... Saya tidak yakin bahwa itu layak untuk dilewati oleh ref. double-s
sergtk
3
Seperti biasa, dorongan membantu di sini. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm memiliki hal-hal templat untuk secara otomatis mengetahui kapan suatu jenis adalah tipe bawaan (berguna untuk templat, di mana Anda terkadang tidak dapat mengetahuinya dengan mudah).
CesarB
13
Jawaban ini melewatkan poin penting. Untuk menghindari pemotongan, Anda harus melewati referensi (const atau yang lain). Lihat stackoverflow.com/questions/274626/…
ChrisN
6
@ Chris: benar. Saya meninggalkan seluruh bagian polimorfisme karena itu adalah semantik yang sama sekali berbeda. Saya percaya OP (semantik) berarti lewat argumen "berdasarkan nilai". Ketika semantik lain diperlukan, pertanyaan itu bahkan tidak muncul dengan sendirinya.
Konrad Rudolph
98

Sunting: Artikel baru oleh Dave Abrahams di cpp-next:

Ingin kecepatan? Lewati dengan nilai.


Pass by value untuk struct di mana penyalinannya murah memiliki keuntungan tambahan bahwa kompiler dapat mengasumsikan bahwa objek tidak alias (bukan objek yang sama). Menggunakan pass-by-reference, kompiler tidak dapat menganggap itu selalu. Contoh sederhana:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

kompiler dapat mengoptimalkannya menjadi

g.i = 15;
f->i = 2;

karena ia tahu bahwa f dan g tidak berbagi lokasi yang sama. jika g adalah referensi (foo &), kompiler tidak dapat menganggap itu. karena gi kemudian dapat di-alias oleh f-> i dan harus memiliki nilai 7. sehingga compiler harus mengambil kembali nilai baru gi dari memori.

Untuk aturan yang lebih praktis, berikut adalah seperangkat aturan yang baik yang ditemukan di artikel Pindahkan Konstruktor (bacaan yang sangat disarankan).

  • Jika fungsi bermaksud mengubah argumen sebagai efek samping, bawa dengan referensi non-const.
  • Jika fungsi tidak mengubah argumennya dan argumennya adalah tipe primitif, ambil dengan nilainya.
  • Kalau tidak bawa dengan referensi const, kecuali dalam kasus berikut
    • Jika fungsi tersebut kemudian perlu membuat salinan referensi const, ambil dengan nilainya.

"Primitif" di atas pada dasarnya berarti tipe data kecil yang panjangnya beberapa byte dan tidak polimorfik (iterator, objek fungsi, dll ...) atau mahal untuk disalin. Dalam makalah itu, ada satu aturan lain. Idenya adalah bahwa kadang-kadang seseorang ingin membuat salinan (dalam kasus argumen tidak dapat dimodifikasi), dan kadang-kadang seseorang tidak mau (jika seseorang ingin menggunakan argumen itu sendiri dalam fungsi jika argumen itu bersifat sementara saja , sebagai contoh). Makalah ini menjelaskan secara rinci bagaimana hal itu dapat dilakukan. Dalam C ++ 1x teknik itu dapat digunakan secara native dengan dukungan bahasa. Sampai saat itu, saya akan pergi dengan aturan di atas.

Contoh: Untuk membuat string huruf besar dan mengembalikan versi huruf besar, kita harus selalu memberikan nilai: Kita tetap harus mengambil salinannya (kita tidak bisa mengubah referensi const secara langsung) - jadi lebih baik membuatnya menjadi setransparan mungkin untuk penelepon dan buat salinan itu lebih awal sehingga penelepon dapat mengoptimalkan sebanyak mungkin - sebagaimana dirinci dalam makalah itu:

my::string uppercase(my::string s) { /* change s and return it */ }

Namun, jika Anda tidak perlu mengubah parameternya, bawa dengan referensi ke const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

Namun, jika Anda tujuan dari parameter adalah untuk menulis sesuatu ke dalam argumen, maka berikan referensi non-const

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}
Johannes Schaub - litb
sumber
Saya menemukan aturan Anda baik tetapi saya tidak yakin tentang bagian pertama di mana Anda berbicara tentang tidak melewatinya sebagai ref akan mempercepatnya. ya tentu, tetapi tidak melewatkan sesuatu sebagai referensi hanya karena optimaztion tidak masuk akal sama sekali. jika Anda ingin mengubah objek tumpukan yang Anda lewati, lakukan dengan ref. jika tidak, berikan nilai. jika Anda tidak ingin mengubahnya, sampaikan sebagai const-ref. optimasi yang disertai dengan nilai-by-value seharusnya tidak penting karena Anda mendapatkan hal-hal lain ketika lulus sebagai referensi. saya tidak mengerti "ingin kecepatan?" sice jika Anda di mana akan melakukan op mereka Anda akan melewati nilai pula ..
chikuba
Johannes: Saya suka artikel itu ketika saya membacanya, tetapi saya kecewa ketika saya mencobanya. Kode ini gagal pada GCC dan MSVC. Apakah saya melewatkan sesuatu, atau tidak berhasil dalam praktik?
user541686
Saya rasa saya tidak setuju bahwa jika Anda ingin tetap membuat salinan, Anda akan meneruskannya dengan nilai (bukan const ref), lalu pindahkan. Lihatlah dengan cara ini, apa yang lebih efisien, salinan dan perpindahan (Anda bahkan dapat memiliki 2 salinan jika Anda meneruskannya), atau hanya salinan? Ya ada beberapa kasus khusus di kedua sisi, tetapi jika data Anda tetap tidak dapat dipindahkan (mis: POD dengan banyak bilangan bulat), tidak perlu salinan tambahan.
Ion Todirel
2
Mehrdad, tidak yakin apa yang Anda harapkan, tetapi kodenya berfungsi seperti yang diharapkan
Ion Todirel
Saya akan mempertimbangkan perlunya menyalin hanya untuk meyakinkan kompiler bahwa jenis tidak tumpang tindih dengan kekurangan dalam bahasa. Saya lebih suka menggunakan GCC __restrict__(yang juga bisa bekerja pada referensi) daripada melakukan salinan berlebihan. Sayang sekali standar C ++ belum mengadopsi restrictkata kunci C99 .
Ruslan
12

Tergantung pada jenisnya. Anda menambahkan overhead kecil karena harus membuat referensi dan referensi. Untuk jenis dengan ukuran yang sama atau lebih kecil dari pointer yang menggunakan copy ctor default, mungkin akan lebih cepat untuk melewati nilai.

Lou Franco
sumber
Untuk tipe non-asli, Anda mungkin (tergantung pada seberapa baik kompiler mengoptimalkan kode) mendapatkan peningkatan kinerja menggunakan referensi const bukan hanya referensi.
OJ.
9

Seperti yang telah ditunjukkan, itu tergantung pada jenisnya. Untuk tipe data bawaan, yang terbaik adalah memberikan nilai. Bahkan beberapa struktur yang sangat kecil, seperti sepasang int dapat bekerja lebih baik dengan melewati nilai.

Berikut ini sebuah contoh, anggap Anda memiliki nilai integer dan Anda ingin meneruskannya ke rutin lain. Jika nilai itu telah dioptimalkan untuk disimpan dalam register, maka jika Anda ingin meneruskannya menjadi referensi, pertama-tama harus disimpan dalam memori dan kemudian sebuah penunjuk ke memori yang ditempatkan pada tumpukan untuk melakukan panggilan. Jika itu diteruskan oleh nilai, semua yang diperlukan adalah register didorong ke stack. (Detailnya sedikit lebih rumit daripada yang diberikan sistem panggilan dan CPU yang berbeda).

Jika Anda melakukan pemrograman templat, Anda biasanya dipaksa untuk selalu melewati const ref karena Anda tidak tahu jenis yang dilewati. Meneruskan hukuman karena melewatkan sesuatu yang buruk menurut nilainya jauh lebih buruk daripada hukuman lewat tipe bawaan oleh const ref.

Torlack
sumber
Catatan tentang terminologi: struct yang berisi sejuta int masih merupakan "tipe POD". Mungkin maksud Anda 'untuk tipe bawaan yang terbaik adalah melalui nilai'.
Steve Jessop
6

Inilah yang biasanya saya kerjakan saat mendesain antarmuka fungsi non-templat:

  1. Lewati nilai jika fungsi tidak ingin memodifikasi parameter dan nilainya murah untuk disalin (int, dobel, float, char, bool, dll. Perhatikan bahwa std :: string, std :: vector, dan yang lainnya kontainer di perpustakaan standar TIDAK)

  2. Lewati oleh pointer pointer jika nilainya mahal untuk disalin dan fungsinya tidak ingin mengubah nilai yang ditunjukkan dan NULL adalah nilai yang ditangani oleh fungsi tersebut.

  3. Lewati oleh pointer non-const jika nilainya mahal untuk disalin dan fungsi ingin memodifikasi nilai yang ditunjukkan dan NULL adalah nilai yang ditangani oleh fungsi tersebut.

  4. Lewati referensi const ketika nilainya mahal untuk disalin dan fungsinya tidak ingin mengubah nilai yang dirujuk dan NULL tidak akan menjadi nilai yang valid jika pointer digunakan sebagai gantinya.

  5. Lewati referensi non-const ketika nilainya mahal untuk disalin dan fungsi ingin memodifikasi nilai yang dirujuk dan NULL tidak akan menjadi nilai yang valid jika pointer digunakan sebagai gantinya.

Martin G
sumber
Tambahkan std::optionalke gambar dan Anda tidak perlu lagi pointer.
Violet Giraffe
5

Sepertinya Anda mendapat jawaban Anda. Melewati dengan nilai itu mahal, tetapi memberi Anda salinan untuk bekerja jika Anda membutuhkannya.

GeekyMonkey
sumber
Saya tidak yakin mengapa ini ditolak? Masuk akal bagi saya. Jika Anda membutuhkan nilai yang saat ini disimpan, maka berikan nilai. Jika tidak, sampaikan referensi.
Totty
4
Ini sepenuhnya tergantung tipe. Melakukan tipe POD (data lama biasa) dengan referensi sebenarnya dapat mengurangi kinerja dengan menyebabkan lebih banyak akses memori.
Torlack
1
Jelas lewat int dengan referensi tidak menyimpan apa pun! Saya pikir pertanyaannya menyiratkan hal-hal yang lebih besar dari sebuah pointer.
GeekyMonkey
4
Tidak begitu jelas, saya telah melihat banyak kode oleh orang-orang yang tidak benar-benar mengerti bagaimana komputer bekerja melewati hal-hal sederhana oleh const ref karena mereka telah diberitahu bahwa itu adalah hal terbaik untuk dilakukan.
Torlack
4

Sebagai aturan yang melewati referensi const lebih baik. Tetapi jika Anda perlu memodifikasi argumen fungsi Anda secara lokal, Anda sebaiknya menggunakan passing oleh nilai. Untuk beberapa tipe dasar, kinerja pada umumnya sama baik untuk melewati nilai dan referensi. Sebenarnya referensi yang diwakili secara internal oleh pointer, itulah sebabnya Anda dapat berharap misalnya bahwa untuk pointer keduanya lewat sama dalam hal kinerja, atau bahkan lewat nilai dapat lebih cepat karena dereference yang tidak perlu.

sergtk
sumber
Jika Anda perlu memodifikasi salinan parameter callee, Anda bisa membuat salinan dalam kode yang dipanggil daripada meneruskan dengan nilai. IMO Anda pada umumnya tidak boleh memilih API berdasarkan pada detail implementasi seperti itu: sumber kode panggilan sama saja, tetapi kode objeknya tidak.
Steve Jessop
Jika Anda melewati salinan nilai dibuat. Dan IMO tidak ada masalah dengan cara mana Anda membuat salinan: melalui argumen yang melewati nilai atau secara lokal - inilah yang menjadi perhatian C ++. Tapi dari sudut pandang desain saya setuju dengan Anda. Tapi saya menjelaskan fitur C ++ di sini saja dan tidak menyentuh desain.
sergtk
1

Sebagai patokan, nilai untuk tipe non-kelas dan referensi const untuk kelas. Jika sebuah kelas sangat kecil mungkin lebih baik untuk lulus dengan nilai, tetapi perbedaannya minimal. Apa yang Anda benar-benar ingin hindari adalah melewati beberapa kelas raksasa dengan nilai dan menduplikasi semuanya - ini akan membuat perbedaan besar jika Anda meneruskan, katakanlah, std :: vector dengan beberapa elemen di dalamnya.

Peter
sumber
Pemahaman saya adalah bahwa std::vectorsebenarnya mengalokasikan item pada heap dan objek vektor itu sendiri tidak pernah tumbuh. Oh tunggu. Jika operasi menyebabkan salinan vektor dibuat, sebenarnya ia akan pergi dan menduplikasi semua elemen. Itu buruk.
Steven Lu
1
Ya, itulah yang saya pikirkan. sizeof(std::vector<int>)adalah konstan, tetapi meneruskannya dengan nilai masih akan menyalin konten tanpa adanya kepintaran kompiler.
Peter
1

Lewati nilai untuk tipe kecil.

Lewati referensi const untuk tipe besar (definisi big dapat bervariasi di antara mesin) TETAPI, dalam C ++ 11, berikan nilai jika Anda akan mengkonsumsi data, karena Anda dapat mengeksploitasi semantik yang bergerak. Sebagai contoh:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

Sekarang kode panggilan akan dilakukan:

Person p(std::string("Albert"));

Dan hanya satu objek yang akan dibuat dan dipindahkan langsung ke anggota name_ di kelas Person. Jika Anda melewati referensi const, salinan harus dibuat untuk memasukkannya ke dalam name_.

Germán Diago
sumber
-5

Perbedaan sederhana: - Dalam fungsi kita memiliki parameter input dan output, jadi jika input dan parameter keluar yang lewat sama, maka gunakan panggilan dengan referensi lain jika input dan parameter output berbeda maka lebih baik menggunakan panggilan berdasarkan nilai.

contoh void amount(int account , int deposit , int total )

parameter input: akun, parameter output setoran: total

input dan out berbeda penggunaan panggilan oleh vaule

  1. void amount(int total , int deposit )

total input jumlah total deposit

Dhirendra Sengar
sumber