Aku mendengar pembicaraan baru-baru ini oleh Herb Sutter yang menyarankan bahwa alasan untuk lulus std::vector
dan std::string
oleh const &
sebagian besar hilang. Dia menyarankan agar menulis fungsi seperti berikut ini sekarang lebih disukai:
std::string do_something ( std::string inval )
{
std::string return_val;
// ... do stuff ...
return return_val;
}
Saya mengerti bahwa return_val
akan menjadi nilai pada titik fungsi kembali dan karena itu dapat dikembalikan menggunakan semantik bergerak, yang sangat murah. Namun, inval
masih jauh lebih besar dari ukuran referensi (yang biasanya diimplementasikan sebagai pointer). Ini karena a std::string
memiliki berbagai komponen termasuk pointer ke heap dan anggota char[]
untuk optimasi string pendek. Jadi sepertinya bagi saya bahwa melewati referensi masih merupakan ide yang bagus.
Adakah yang bisa menjelaskan mengapa Herb mungkin mengatakan ini?
Jawaban:
Alasan Herb mengatakan apa yang dia katakan adalah karena kasus seperti ini.
Katakanlah saya memiliki fungsi
A
yang memanggil fungsiB
, yang memanggil fungsiC
. DanA
melewati sebuah string melaluiB
dan ke dalamC
.A
tidak tahu atau tidak peduliC
; semua yangA
tahu adalahB
. Artinya,C
adalah detail implementasi dariB
.Katakanlah A didefinisikan sebagai berikut:
Jika B dan C mengambil string
const&
, maka akan terlihat seperti ini:Semua baik dan bagus. Anda hanya melewati petunjuk, tidak menyalin, tidak bergerak, semua orang senang.
C
Dibutuhkanconst&
karena tidak menyimpan string. Itu hanya menggunakannya.Sekarang, saya ingin membuat satu perubahan sederhana:
C
perlu menyimpan string di suatu tempat.Halo, salin konstruktor dan alokasi memori potensial (abaikan Short String Optimization (SSO) ). Semantik langkah C ++ 11 seharusnya memungkinkan untuk menghapus pembuatan salinan yang tidak perlu, kan? Dan
A
melewati sementara; tidak ada alasan mengapaC
harus menyalin data. Seharusnya hanya melarikan diri dengan apa yang diberikan padanya.Kecuali itu tidak bisa. Karena itu diperlukan a
const&
.Jika saya mengubah
C
untuk mengambil parameternya dengan nilai, itu hanya menyebabkanB
untuk melakukan salin ke parameter itu; Saya tidak mendapatkan apa-apa.Jadi jika saya baru saja melewati
str
nilai melalui semua fungsi, mengandalkanstd::move
untuk mengacak data di sekitar, kita tidak akan memiliki masalah ini. Jika seseorang ingin mempertahankannya, mereka bisa. Jika tidak, oh well.Apakah lebih mahal? Iya; pindah ke suatu nilai lebih mahal daripada menggunakan referensi. Apakah lebih murah daripada salinannya? Bukan untuk string kecil dengan SSO. Apakah ini layak dilakukan?
Itu tergantung pada kasus penggunaan Anda. Berapa Anda benci alokasi memori?
sumber
moved
dari B ke C dalam kasus nilai? Jika B adalahB(std::string b)
dan CC(std::string c)
maka kita harus memanggilC(std::move(b))
B ataub
harus tetap tidak berubah (dengan demikian 'tidak bergerak dari') sampai keluarB
. (Mungkin kompilator pengoptimal akan memindahkan string di bawah aturan as-if jikab
tidak digunakan setelah panggilan tetapi saya tidak berpikir ada jaminan yang kuat.) Hal yang sama berlaku untuk salinanstr
tom_str
. Bahkan jika paramter fungsi diinisialisasi dengan rvalue, ini adalah lvalue di dalam fungsi danstd::move
diharuskan untuk beralih dari lvalue itu.Tidak ada . Banyak orang menerima saran ini (termasuk Dave Abrahams) di luar domain yang digunakannya, dan menyederhanakannya untuk berlaku untuk semua
std::string
parameter - Selalu memberikanstd::string
nilai bukanlah "praktik terbaik" untuk setiap dan semua parameter dan aplikasi sewenang-wenang karena optimasi ini pembicaraan / artikel fokus pada penerapan hanya pada serangkaian kasus terbatas .Jika Anda mengembalikan nilai, mengubah parameter, atau mengambil nilai, lalu memberikan nilai dapat menghemat penyalinan yang mahal dan menawarkan kenyamanan sintaksis.
Seperti biasa, melewati referensi const menyimpan banyak penyalinan ketika Anda tidak membutuhkan salinan .
Sekarang untuk contoh spesifik:
Jika ukuran tumpukan menjadi perhatian (dan dengan asumsi ini tidak inline / dioptimalkan),
return_val
+inval
>return_val
- TKI, penggunaan tumpukan puncak dapat dikurangi dengan melewati nilai di sini (catatan: penyederhanaan ABI yang berlebihan). Sementara itu, melewati referensi const dapat menonaktifkan optimasi. Alasan utama di sini bukan untuk menghindari pertumbuhan tumpukan, tetapi untuk memastikan optimasi dapat dilakukan di mana itu berlaku .Hari-hari berlalu dengan referensi const belum berakhir - aturan hanya lebih rumit dari sebelumnya. Jika kinerja itu penting, Anda akan bijaksana untuk mempertimbangkan bagaimana Anda melewati tipe-tipe ini, berdasarkan detail yang Anda gunakan dalam implementasi Anda.
sumber
Ini sangat tergantung pada implementasi kompiler.
Namun, itu juga tergantung pada apa yang Anda gunakan.
Mari kita pertimbangkan fungsi-fungsi selanjutnya:
Fungsi-fungsi ini diimplementasikan dalam unit kompilasi terpisah untuk menghindari inlining. Kemudian:
1. Jika Anda memberikan literal ke dua fungsi ini, Anda tidak akan melihat banyak perbedaan dalam penampilan. Dalam kedua kasus, objek string harus dibuat
2. Jika Anda melewati objek std :: string lainnya,
foo2
akan mengunggulifoo1
, karenafoo1
akan melakukan salinan yang dalam.Di PC saya, menggunakan g ++ 4.6.1, saya mendapatkan hasil ini:
sumber
Jawaban singkat: TIDAK! Jawaban panjang:
const ref&
.(yang
const ref&
jelas harus tetap dalam ruang lingkup sementara fungsi yang menggunakannya dijalankan)value
, jangan menyalin bagianconst ref&
dalam tubuh fungsi Anda.Ada posting di cpp-next.com yang disebut "Ingin kecepatan, berikan nilai!" . TL; DR:
TERJEMAHAN ^
Jangan menyalin argumen fungsi Anda --- berarti: jika Anda berencana untuk mengubah nilai argumen dengan menyalinnya ke variabel internal, cukup gunakan argumen nilai .
Jadi, jangan lakukan ini :
lakukan ini :
Ketika Anda perlu mengubah nilai argumen di tubuh fungsi Anda.
Anda hanya perlu menyadari bagaimana Anda berencana untuk menggunakan argumen di badan fungsi. Read-only atau NOT ... dan jika itu tetap dalam lingkup.
sumber
const ref&
ke variabel internal untuk memodifikasinya. Jika Anda perlu memodifikasinya ... buat parameter sebagai nilai. Cukup jelas untuk saya yang tidak bisa berbahasa Inggris.const ref&
. # 2 Jika saya perlu menulisnya atau saya tahu itu keluar dari ruang lingkup ... Saya menggunakan nilai. # 3 Jika saya perlu mengubah nilai asli, saya lewatref&
. # 4 Saya menggunakanpointers *
jika argumen opsional jadi saya bisanullptr
.Kecuali Anda benar-benar membutuhkan salinannya, itu masih masuk akal
const &
. Sebagai contoh:Jika Anda mengubah ini untuk mengambil string dengan nilai maka Anda akhirnya akan memindahkan atau menyalin parameter, dan tidak perlu untuk itu. Tidak hanya menyalin / memindahkan kemungkinan lebih mahal, tetapi juga memperkenalkan potensi kegagalan baru; salinan / pemindahan bisa membuat pengecualian (mis., alokasi selama penyalinan bisa gagal) sedangkan mengambil referensi ke nilai yang ada tidak bisa.
Jika Anda benar- benar membutuhkan salinan, maka meneruskan dan mengembalikan dengan nilai biasanya (selalu?) Adalah pilihan terbaik. Sebenarnya saya biasanya tidak akan khawatir tentang hal itu di C ++ 03 kecuali jika Anda menemukan bahwa salinan tambahan sebenarnya menyebabkan masalah kinerja. Copy elision tampaknya cukup dapat diandalkan pada kompiler modern. Saya pikir orang-orang skeptis dan desakan bahwa Anda harus memeriksa tabel dukungan kompiler Anda untuk RVO sebagian besar sudah usang saat ini.
Singkatnya, C ++ 11 tidak benar-benar mengubah apa pun dalam hal ini kecuali untuk orang-orang yang tidak percaya pada salinan elision.
sumber
noexcept
, tetapi copy konstruktor jelas tidak.Hampir.
Di C ++ 17, kita punya
basic_string_view<?>
, yang membawa kita ke dasarnya satu kasus penggunaan sempit untukstd::string const&
parameter.Keberadaan semantik langkah telah menghilangkan satu use case untuk
std::string const&
- jika Anda berencana menyimpan parameter, mengambilstd::string
nilai dengan lebih optimal, karena Anda dapatmove
keluar dari parameter.Jika seseorang memanggil fungsi Anda dengan C mentah,
"string"
ini berarti hanya satustd::string
buffer yang pernah dialokasikan, sebagai lawan dari dua dalamstd::string const&
kasus ini.Namun, jika Anda tidak berniat untuk membuat salinan, mengambil dengan
std::string const&
masih berguna di C ++ 14.Dengan
std::string_view
, selama Anda tidak meneruskan string tersebut ke API yang mengharapkan'\0'
buffer karakter yang dimusnahkan dengan gaya-C , Anda dapat lebih efisien mendapatkanstd::string
fungsionalitas seperti tanpa risiko alokasi apa pun. String C mentah bahkan dapat diubah menjadistd::string_view
tanpa penyalinan atau alokasi karakter apa pun.Pada saat itu, penggunaannya
std::string const&
adalah ketika Anda tidak menyalin data grosir, dan akan meneruskannya ke API gaya-C yang mengharapkan buffer null dihentikan, dan Anda memerlukan fungsi string tingkat yang lebih tinggi yangstd::string
menyediakan. Dalam praktiknya, ini adalah serangkaian persyaratan yang langka.sumber
std::string
mendominasi Anda tidak hanya membutuhkan beberapa persyaratan itu tetapi semuanya . Saya akui, satu atau bahkan dua dari itu adalah hal biasa. Mungkin Anda memang membutuhkanstd::string_view
- seperti yang Anda tunjukkan, “Sebuah string C mentah bahkan dapat diubah menjadi std :: string_view tanpa alokasi atau penyalinan karakter” yang merupakan sesuatu yang patut diingat oleh mereka yang menggunakan C ++ dalam konteks penggunaan API seperti itu, memang.std::string
bukan Data Lama Biasa (POD) , dan ukuran mentahnya bukan yang paling relevan. Sebagai contoh, jika Anda memasukkan string yang di atas panjang SSO dan dialokasikan pada heap, saya akan mengharapkan copy constructor untuk tidak menyalin penyimpanan SSO.Alasan ini direkomendasikan adalah karena
inval
dibangun dari ekspresi argumen, dan dengan demikian selalu dipindahkan atau disalin sebagaimana mestinya - tidak ada kehilangan kinerja, dengan asumsi bahwa Anda memerlukan kepemilikan argumen. Jika tidak,const
referensi masih bisa menjadi cara yang lebih baik.sumber
std::string<>
32 byte yang dialokasikan dari stack, 16 di antaranya perlu diinisialisasi. Bandingkan ini dengan hanya 8 byte yang dialokasikan dan diinisialisasi untuk referensi: Ini adalah dua kali jumlah kerja CPU, dan membutuhkan ruang cache empat kali lebih banyak yang tidak akan tersedia untuk data lain.Saya telah menyalin / menempelkan jawaban dari pertanyaan ini di sini, dan mengubah nama dan ejaan agar sesuai dengan pertanyaan ini.
Berikut ini kode untuk mengukur apa yang ditanyakan:
Bagi saya ini output:
Tabel di bawah ini merangkum hasil saya (menggunakan dentang -std = c ++ 11). Angka pertama adalah jumlah konstruksi salin dan nomor kedua adalah jumlah konstruksi langkah:
Solusi pass-by-value hanya membutuhkan satu kelebihan tetapi biaya konstruksi gerakan tambahan ketika melewati nilai l dan nilai x. Ini mungkin atau mungkin tidak dapat diterima untuk situasi apa pun. Kedua solusi memiliki kelebihan dan kekurangan.
sumber
Herb Sutter masih dalam catatan, bersama dengan Bjarne Stroustroup, dalam merekomendasikan
const std::string&
sebagai tipe parameter; lihat https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .Ada jebakan yang tidak disebutkan dalam jawaban lain di sini: jika Anda meneruskan string literal ke
const std::string&
parameter, itu akan melewati referensi ke string sementara, dibuat saat itu juga untuk memegang karakter literal. Jika Anda kemudian menyimpan referensi itu, itu akan menjadi tidak valid setelah string sementara dibatalkan alokasi. Agar aman, Anda harus menyimpan salinan , bukan referensi. Masalahnya berasal dari fakta bahwa string literal adalahconst char[N]
tipe, yang membutuhkan promosistd::string
.Kode di bawah ini mengilustrasikan jebakan dan solusi, bersama dengan opsi efisiensi kecil - kelebihan dengan
const char*
metode, seperti yang dijelaskan pada Apakah ada cara untuk melewatkan string string sebagai referensi dalam C ++ .(Catatan: Sutter & Stroustroup menyarankan bahwa jika Anda menyimpan salinan string, berikan juga fungsi kelebihan dengan parameter && dan std :: move ()).
KELUARAN:
sumber
T&&
tidak selalu merupakan referensi universal; pada kenyataannya,std::string&&
akan selalu menjadi referensi nilai, dan tidak pernah menjadi referensi universal, karena tidak ada pengurangan tipe yang dilakukan . Dengan demikian, saran Stroustroup & Sutter tidak bertentangan dengan Meyers.T&&
adalah referensi universal yang disimpulkan atau referensi nilai tidak-dikurangi; mungkin akan lebih jelas jika mereka memperkenalkan simbol berbeda untuk referensi universal (seperti&&&
, kombinasi dari&
dan&&
), tetapi itu mungkin hanya akan terlihat konyol.IMO menggunakan referensi C ++
std::string
adalah optimisasi lokal cepat dan pendek, sementara menggunakan passing oleh nilai bisa (atau tidak) optimasi global yang lebih baik.Jadi jawabannya adalah: itu tergantung pada keadaan:
const std::string &
.std::string
perilaku copy constructor.sumber
Lihat “Herb Sutter" Kembali ke Dasar! Esensi Gaya C ++ Modern . " Di antara topik-topik lain, ia meninjau saran lewat parameter yang telah diberikan di masa lalu, dan ide-ide baru yang datang dengan C ++ 11 dan secara khusus melihat pada ide melewati string oleh nilai.
Benchmark menunjukkan bahwa kelalaian
std::string
oleh nilai, dalam kasus di mana fungsi akan menyalinnya, dapat secara signifikan lebih lambat!Ini karena Anda memaksanya untuk selalu membuat salinan lengkap (dan kemudian pindah ke tempatnya), sementara
const&
versi akan memperbarui string lama yang dapat menggunakan kembali buffer yang sudah dialokasikan.Lihat slide-nya 27: Untuk fungsi "set", opsi 1 sama seperti biasanya. Opsi 2 menambahkan kelebihan untuk referensi nilai, tetapi ini memberikan ledakan kombinatorial jika ada beberapa parameter.
Hanya untuk parameter "sink" di mana string harus dibuat (tidak ada nilainya yang berubah) bahwa trik pass-by-value valid. Yaitu, konstruktor di mana parameter secara langsung menginisialisasi anggota dari jenis yang cocok.
Jika Anda ingin melihat seberapa dalam Anda bisa mengkhawatirkan hal ini, saksikan presentasi Nicolai Josuttis dan semoga sukses dengan itu ( "Sempurna - Selesai!" Dan beberapa kali setelah menemukan kesalahan dengan versi sebelumnya. Pernah ke sana?)
Ini juga diringkas sebagai ⧺F.15 dalam Pedoman Standar.
sumber
Seperti yang ditunjukkan oleh @ JDługosz dalam komentar, Herb memberikan saran lain dalam pembicaraan lain (nanti?), Lihat kira-kira dari sini: https://youtu.be/xnqTKD8uD64?t=54m50s .
Sarannya bermuara pada hanya menggunakan parameter nilai untuk fungsi
f
yang mengambil apa yang disebut argumen wastafel, dengan asumsi Anda akan memindahkan konstruk dari argumen wastafel ini.Pendekatan umum ini hanya menambahkan overhead dari konstruktor bergerak untuk argumen lvalue dan rvalue dibandingkan dengan implementasi optimal yang
f
disesuaikan dengan argumen lvalue dan rvalue masing-masing. Untuk melihat mengapa hal ini terjadi, anggaplahf
mengambil parameter nilai, di manaT
ada beberapa salin dan pindahkan tipe konstruktif:Memanggil
f
dengan argumen lvalue akan menghasilkan copy constructor yang dipanggil untuk membangunx
, dan move constructor dipanggil untuk membanguny
. Di sisi lain, memanggilf
dengan argumen nilai akan menyebabkan pemindah konstruktor dipanggil untuk membangunx
, dan pemindah pemindah lain dipanggil untuk membanguny
.Secara umum, implementasi optimal
f
untuk argumen lvalue adalah sebagai berikut:Dalam hal ini, hanya satu copy constructor yang dipanggil untuk membangun
y
. Implementasi optimalf
untuk argumen nilai, sekali lagi secara umum, sebagai berikut:Dalam hal ini, hanya satu konstruktor bergerak dipanggil untuk membangun
y
.Jadi kompromi yang masuk akal adalah untuk mengambil parameter nilai dan memiliki satu panggilan konstruktor gerakan ekstra untuk argumen nilai atau nilai sehubungan dengan implementasi yang optimal, yang juga merupakan saran yang diberikan dalam pembicaraan Herb.
Seperti @ JDługosz tunjukkan dalam komentar, melewati nilai hanya masuk akal untuk fungsi yang akan membangun beberapa objek dari argumen wastafel. Ketika Anda memiliki fungsi
f
yang menyalin argumennya, pendekatan pass-by-value akan memiliki lebih banyak overhead daripada pendekatan umum pass-by-const-reference. Pendekatan nilai demi nilai untuk fungsif
yang menyimpan salinan parameternya akan berbentuk:Dalam hal ini, ada konstruksi salinan dan tugas pemindahan untuk argumen nilai, dan konstruksi pemindahan dan pemindahan tugas untuk argumen nilai. Kasus paling optimal untuk argumen nilai adalah:
Ini bermuara pada penugasan saja, yang berpotensi jauh lebih murah daripada copy constructor ditambah tugas penugasan yang diperlukan untuk pendekatan pass-by-value. Alasan untuk ini adalah bahwa penugasan mungkin menggunakan kembali memori yang dialokasikan yang ada di
y
, dan karena itu mencegah alokasi (de), sedangkan copy constructor biasanya akan mengalokasikan memori.Untuk argumen nilai, implementasi paling optimal untuk
f
yang mempertahankan salinan memiliki bentuk:Jadi, hanya tugas pindah dalam hal ini. Melewati nilai ke versi
f
yang mengambil referensi konst hanya biaya tugas alih-alih tugas pindah. Jadi relatif berbicara, versif
mengambil referensi const dalam hal ini sebagai implementasi umum lebih disukai.Jadi secara umum, untuk implementasi yang paling optimal, Anda perlu membebani atau melakukan semacam penerusan sempurna seperti yang ditunjukkan dalam pembicaraan. Kekurangannya adalah ledakan kombinatorial dalam jumlah kelebihan yang dibutuhkan, tergantung pada jumlah parameter
f
jika Anda memilih untuk kelebihan pada kategori nilai argumen. Penerusan sempurna memiliki kelemahan yangf
menjadi fungsi templat, yang mencegah membuatnya virtual, dan menghasilkan kode yang jauh lebih kompleks jika Anda ingin mendapatkannya 100% benar (lihat pembicaraan untuk detail berdarah).sumber
Masalahnya adalah bahwa "const" adalah kualifikasi non-granular. Apa yang biasanya dimaksud dengan "const string ref" adalah "jangan modifikasi string ini", bukan "jangan modifikasi jumlah referensi". Tidak ada cara, di C ++, untuk mengatakan anggota mana yang "const". Mereka semua, atau tidak ada.
Dalam rangka untuk hack sekitar masalah bahasa ini, STL bisa memungkinkan "C ()" dalam contoh Anda untuk membuat salinan langkah-semantik pula , dan patuh mengabaikan "const" sehubungan dengan jumlah referensi (bisa berubah). Selama itu ditentukan dengan baik, ini akan baik-baik saja.
Karena STL tidak, saya memiliki versi string yang const_casts <> menghilangkan penghitung referensi (tidak ada cara untuk secara retroaktif membuat sesuatu dapat berubah dalam hierarki kelas), dan - lihatlah - Anda dapat dengan bebas melewatkan cmstring sebagai referensi const, dan membuat salinannya dalam fungsi yang mendalam, sepanjang hari, tanpa kebocoran atau masalah.
Karena C ++ tidak menawarkan "granularity const turunan kelas" di sini, menulis spesifikasi yang baik dan membuat objek "const movable string" (cmstring) yang mengkilap baru adalah solusi terbaik yang pernah saya lihat.
sumber