Apakah hari-hari berlalu const std :: string & sebagai parameter berakhir?

604

Aku mendengar pembicaraan baru-baru ini oleh Herb Sutter yang menyarankan bahwa alasan untuk lulus std::vectordan std::stringoleh 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_valakan menjadi nilai pada titik fungsi kembali dan karena itu dapat dikembalikan menggunakan semantik bergerak, yang sangat murah. Namun, invalmasih jauh lebih besar dari ukuran referensi (yang biasanya diimplementasikan sebagai pointer). Ini karena a std::stringmemiliki 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?

Benj
sumber
89
Saya pikir jawaban terbaik untuk pertanyaan itu mungkin dengan membaca artikel Dave Abrahams tentang hal itu di C ++ Next . Saya akan menambahkan bahwa saya tidak melihat apa pun tentang ini yang memenuhi syarat sebagai di luar topik atau tidak konstruktif. Ini pertanyaan yang jelas, tentang pemrograman, yang mana ada jawaban faktual.
Jerry Coffin
Menarik, jadi jika Anda tetap harus membuat salinannya, nilai demi nilai cenderung lebih cepat daripada pass-by-reference.
Benj
3
@Sz. Saya sensitif terhadap pertanyaan yang dikategorikan salah sebagai duplikat dan ditutup. Saya tidak ingat detail kasus ini dan belum meninjaunya kembali. Sebaliknya saya hanya akan menghapus komentar saya dengan asumsi bahwa saya melakukan kesalahan. Terima kasih untuk mengingatkan saya pada hal ini.
Howard Hinnant
2
@HowardHinnant, terima kasih banyak, itu selalu menjadi momen berharga ketika seseorang menemukan tingkat perhatian dan kepekaan ini, sungguh menyegarkan! (Aku akan menghapus milikku kalau begitu.)
Sz.

Jawaban:

393

Alasan Herb mengatakan apa yang dia katakan adalah karena kasus seperti ini.

Katakanlah saya memiliki fungsi Ayang memanggil fungsi B, yang memanggil fungsi C. Dan Amelewati sebuah string melalui Bdan ke dalam C. Atidak tahu atau tidak peduli C; semua yang Atahu adalah B. Artinya, Cadalah detail implementasi dari B.

Katakanlah A didefinisikan sebagai berikut:

void A()
{
  B("value");
}

Jika B dan C mengambil string const&, maka akan terlihat seperti ini:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

Semua baik dan bagus. Anda hanya melewati petunjuk, tidak menyalin, tidak bergerak, semua orang senang. CDibutuhkan const&karena tidak menyimpan string. Itu hanya menggunakannya.

Sekarang, saya ingin membuat satu perubahan sederhana: Cperlu menyimpan string di suatu tempat.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

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 Amelewati sementara; tidak ada alasan mengapa Charus menyalin data. Seharusnya hanya melarikan diri dengan apa yang diberikan padanya.

Kecuali itu tidak bisa. Karena itu diperlukan a const&.

Jika saya mengubah Cuntuk mengambil parameternya dengan nilai, itu hanya menyebabkan Buntuk melakukan salin ke parameter itu; Saya tidak mendapatkan apa-apa.

Jadi jika saya baru saja melewati strnilai melalui semua fungsi, mengandalkan std::moveuntuk 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?

Nicol Bolas
sumber
2
Ketika Anda mengatakan bahwa pindah ke nilai lebih mahal daripada menggunakan referensi, itu masih lebih mahal dengan jumlah konstan (terlepas dari panjang string yang dipindahkan) kan?
Neil G
3
@ NeilG: Apakah Anda mengerti apa artinya "tergantung pada implementasi"? Apa yang Anda katakan salah, karena itu tergantung pada apakah dan bagaimana SSO diterapkan.
ildjarn
17
@ildjarn: Dalam analisis urutan, jika kasus terburuk dari sesuatu terikat oleh konstanta, maka itu masih waktu yang konstan. Apakah tidak ada tali kecil terpanjang? Bukankah string itu membutuhkan jumlah waktu yang konstan untuk menyalin? Tidak semua string yang lebih kecil membutuhkan waktu lebih sedikit untuk menyalin? Kemudian, menyalin string untuk string kecil adalah "waktu konstan" dalam analisis urutan - meskipun string kecil mengambil jumlah waktu yang berbeda untuk menyalin. Analisis urutan berkaitan dengan perilaku asimptotik .
Neil G
8
@ NeilG: Tentu, tapi pertanyaan awal Anda adalah " itu masih lebih mahal dengan jumlah konstan (terlepas dari panjang string yang dipindahkan) kan? " Maksud saya adalah, itu bisa lebih mahal dengan berbeda jumlah konstan tergantung pada panjang string, yang disimpulkan sebagai "tidak".
ildjarn
13
Mengapa string berasal moveddari B ke C dalam kasus nilai? Jika B adalah B(std::string b)dan C C(std::string c)maka kita harus memanggil C(std::move(b))B atau bharus tetap tidak berubah (dengan demikian 'tidak bergerak dari') sampai keluar B. (Mungkin kompilator pengoptimal akan memindahkan string di bawah aturan as-if jika btidak digunakan setelah panggilan tetapi saya tidak berpikir ada jaminan yang kuat.) Hal yang sama berlaku untuk salinan strto m_str. Bahkan jika paramter fungsi diinisialisasi dengan rvalue, ini adalah lvalue di dalam fungsi dan std::movediharuskan untuk beralih dari lvalue itu.
Pixelchemist
163

Apakah hari-hari berlalu const std :: string & sebagai parameter berakhir?

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 memberikan std::stringnilai 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:

Namun inval masih jauh lebih besar dari ukuran referensi (yang biasanya diimplementasikan sebagai pointer). Ini karena std :: string memiliki berbagai komponen termasuk pointer ke heap dan char anggota [] 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?

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.

justin
sumber
3
Pada penggunaan stack, tipikal ABI akan melewati satu referensi dalam register tanpa penggunaan stack.
ahcox
63

Ini sangat tergantung pada implementasi kompiler.

Namun, itu juga tergantung pada apa yang Anda gunakan.

Mari kita pertimbangkan fungsi-fungsi selanjutnya:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

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, foo2akan mengungguli foo1, karena foo1akan melakukan salinan yang dalam.

Di PC saya, menggunakan g ++ 4.6.1, saya mendapatkan hasil ini:

  • variabel dengan referensi: 1000000000 iterations -> waktu berlalu: 2.25912 detik
  • variabel dengan nilai: iterasi 1000000000 -> waktu berlalu: 27,2259 detik
  • literal dengan referensi: 100000000 iterations -> time berlalu: 9,10319 dtk
  • literal dengan nilai: iterasi 100000000 -> waktu yang berlalu: 8,62659 detik
BЈовић
sumber
4
Yang lebih relevan adalah apa yang terjadi di dalam fungsi: apakah itu, jika dipanggil dengan referensi, perlu membuat salinan secara internal yang dapat dihilangkan ketika melewati nilai ?
leftaroundabout
1
@leftaroundtentang Ya, tentu saja. Asumsi saya bahwa kedua fungsi melakukan hal yang persis sama.
BЈовић
5
Itu bukan poin saya. Apakah melewati dengan nilai atau referensi lebih baik tergantung pada apa yang Anda lakukan di dalam fungsi. Dalam contoh Anda, Anda sebenarnya tidak menggunakan banyak objek string, jadi referensi jelas lebih baik. Tetapi jika tugas fungsi adalah untuk menempatkan string dalam beberapa struct atau untuk melakukan, katakanlah, beberapa algoritma rekursif yang melibatkan beberapa pemisahan string, melewati nilai mungkin sebenarnya menyimpan beberapa penyalinan, dibandingkan dengan meneruskan dengan referensi. Nicol Bolas menjelaskannya dengan cukup baik.
leftaroundabout
3
Bagi saya "itu tergantung pada apa yang Anda lakukan di dalam fungsi" adalah desain yang buruk - karena Anda mendasarkan tanda tangan fungsi pada internal implementasi.
Hans Olsson
1
Mungkin salah ketik, tetapi dua timing literal terakhir memiliki loop 10x lebih sedikit.
TankorSmash
54

Jawaban singkat: TIDAK! Jawaban panjang:

  • Jika Anda tidak akan memodifikasi string (perlakukan sebagai read-only), berikan sebagai const ref&.
    (yang const ref&jelas harus tetap dalam ruang lingkup sementara fungsi yang menggunakannya dijalankan)
  • Jika Anda berencana untuk memodifikasinya atau Anda tahu itu akan keluar dari ruang lingkup (utas) , berikan sebagai a value, jangan menyalin bagian const ref&dalam tubuh fungsi Anda.

Ada posting di cpp-next.com yang disebut "Ingin kecepatan, berikan nilai!" . TL; DR:

Pedoman : Jangan menyalin argumen fungsi Anda. Alih-alih, berikan nilai dan biarkan kompiler melakukan penyalinan.

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 :

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

lakukan ini :

std::string function(std::string aString){
    aString.clear();
    return aString;
}

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.

CodeAngry
sumber
2
Anda merekomendasikan lewat referensi dalam beberapa kasus, tetapi Anda menunjuk ke pedoman yang merekomendasikan selalu lewat nilai.
Keith Thompson
3
@KeithThompson Jangan menyalin argumen fungsi Anda. Berarti tidak menyalin const ref&ke variabel internal untuk memodifikasinya. Jika Anda perlu memodifikasinya ... buat parameter sebagai nilai. Cukup jelas untuk saya yang tidak bisa berbahasa Inggris.
CodeAngry
5
@KeithThompson Kutipan pedoman (Jangan menyalin argumen fungsi Anda. Sebaliknya, sampaikan dengan nilai dan biarkan kompiler melakukan penyalinan.) DI COPIED dari halaman itu. Jika itu tidak cukup jelas, saya tidak dapat membantu. Saya tidak sepenuhnya percaya kompiler untuk membuat pilihan terbaik. Saya lebih suka sangat jelas tentang maksud saya dalam cara saya mendefinisikan argumen fungsi. # 1 Jika hanya baca, itu a 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 lewat ref&. # 4 Saya menggunakan pointers *jika argumen opsional jadi saya bisa nullptr.
CodeAngry
11
Saya tidak memihak pada pertanyaan apakah harus melewati nilai atau referensi. Maksud saya adalah bahwa Anda menganjurkan lewat dengan referensi dalam beberapa kasus, tetapi kemudian mengutip (tampaknya untuk mendukung posisi Anda) pedoman yang merekomendasikan selalu lewat nilai. Jika Anda tidak setuju dengan pedoman ini, Anda mungkin ingin mengatakannya dan menjelaskan alasannya. (Tautan ke cpp-next.com tidak berfungsi untuk saya.)
Keith Thompson
4
@KeithThompson: Anda salah memparafrasekan pedoman. Bukan untuk "selalu" melewati nilai. Untuk meringkas, itu adalah "Jika Anda akan membuat salinan lokal, gunakan nilai by pass untuk membuat kompiler melakukan salinan itu untuk Anda." Itu tidak mengatakan untuk menggunakan pass-by-value ketika Anda tidak akan membuat salinan.
Ben Voigt
43

Kecuali Anda benar-benar membutuhkan salinannya, itu masih masuk akal const &. Sebagai contoh:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

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.

bames53
sumber
2
Pindahkan konstruktor biasanya diterapkan dengan noexcept, tetapi copy konstruktor jelas tidak.
leftaroundabout
25

Hampir.

Di C ++ 17, kita punya basic_string_view<?>, yang membawa kita ke dasarnya satu kasus penggunaan sempit untuk std::string const&parameter.

Keberadaan semantik langkah telah menghilangkan satu use case untuk std::string const&- jika Anda berencana menyimpan parameter, mengambil std::stringnilai dengan lebih optimal, karena Anda dapat movekeluar dari parameter.

Jika seseorang memanggil fungsi Anda dengan C mentah, "string"ini berarti hanya satu std::stringbuffer yang pernah dialokasikan, sebagai lawan dari dua dalam std::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 mendapatkan std::stringfungsionalitas seperti tanpa risiko alokasi apa pun. String C mentah bahkan dapat diubah menjadi std::string_viewtanpa 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 yang std::stringmenyediakan. Dalam praktiknya, ini adalah serangkaian persyaratan yang langka.

Yakk - Adam Nevraumont
sumber
2
Saya menghargai jawaban ini - tetapi saya ingin menunjukkan bahwa itu memang menderita (seperti banyak jawaban berkualitas) dari sedikit bias spesifik domain. Yang jelas: "Dalam praktiknya, ini adalah serangkaian persyaratan yang langka" ... dalam pengalaman pengembangan saya sendiri, kendala-kendala ini - yang tampak sangat sempit bagi penulis - dipenuhi, seperti, secara harfiah sepanjang waktu. Perlu menunjukkan ini.
fish2000
1
@ fish2000 Supaya jelas, untuk std::stringmendominasi Anda tidak hanya membutuhkan beberapa persyaratan itu tetapi semuanya . Saya akui, satu atau bahkan dua dari itu adalah hal biasa. Mungkin Anda memang membutuhkan
ketiganya
@ Yakk-AdamNevraumont Ini adalah hal YMMV - tetapi ini adalah kasus penggunaan yang sering jika (katakanlah) Anda memprogram melawan POSIX, atau API lain di mana semantik C-string, seperti, penyebut umum terendah. Saya harus mengatakan dengan sungguh-sungguh bahwa saya cinta std::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.
fish2000
1
@ fish2000 "'Sebuah string C mentah bahkan dapat diubah menjadi std :: string_view tanpa alokasi atau penyalinan karakter' yang merupakan sesuatu yang patut diingat". Memang, tetapi meninggalkan bagian terbaik - dalam kasus bahwa string mentah adalah string literal, bahkan tidak memerlukan runtime strlen () !
Don Hatch
17

std::stringbukan 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 invaldibangun 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, constreferensi masih bisa menjadi cara yang lebih baik.

Anak anjing
sumber
2
Poin menarik tentang pembuat salinan cukup pintar untuk tidak khawatir tentang SSO jika tidak menggunakannya. Mungkin benar, saya harus memeriksa apakah itu benar ;-)
Benj
3
@ Benj: Komentar lama saya tahu, tetapi jika SSO cukup kecil menyalinnya tanpa syarat lebih cepat daripada melakukan cabang kondisional. Misalnya, 64 byte adalah garis cache dan dapat disalin dalam jumlah waktu yang sangat sepele. Mungkin 8 siklus atau kurang pada x86_64.
Zan Lynx
Bahkan jika SSO tidak disalin oleh copy constructor, sebuah 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.
cmaster - mengembalikan monica
Oh, dan saya lupa berbicara tentang melewati argumen fungsi di register; yang akan membawa tumpukan penggunaan referensi ke nol untuk callee terakhir ...
cmaster - mengembalikan monica
16

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:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

Bagi saya ini output:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

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:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

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.

Howard Hinnant
sumber
1
std :: string adalah kelas perpustakaan standar. Ini sudah bisa dipindah-pindahkan dan dapat disalin. Saya tidak melihat bagaimana ini relevan. OP bertanya lebih banyak tentang kinerja bergerak vs referensi , bukan kinerja bergerak vs salinan.
Nicol Bolas
3
Jawaban ini menghitung jumlah gerakan dan salinan string std :: akan menjalani di bawah desain pass-by-value yang dijelaskan oleh Herb dan Dave, vs melewati referensi dengan sepasang fungsi yang kelebihan beban. Saya menggunakan kode OP dalam demo, kecuali untuk mengganti string dummy untuk berteriak ketika disalin / dipindahkan.
Howard Hinnant
Anda mungkin harus mengoptimalkan kode sebelum melakukan tes ...
The Paramagnetic Croissant
3
@TheParamagneticCroissant: Apakah Anda mendapatkan hasil yang berbeda? Jika demikian, menggunakan kompiler apa dengan argumen baris perintah apa?
Howard Hinnant
14

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 adalah const char[N]tipe, yang membutuhkan promosi std::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 ()).

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

KELUARAN:

const char * constructor
const std::string& constructor

Second string
Second string
circlepi314
sumber
Ini adalah masalah yang berbeda dan WidgetBadRef tidak perlu memiliki konst & parameter yang salah. Pertanyaannya adalah apakah WidgetSafeCopy hanya mengambil parameter string, apakah akan lebih lambat? (Saya pikir salinan sementara untuk anggota tentu lebih mudah dikenali)
Superfly Jon
Catatan yang 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.
Justin Time - Pasang kembali Monica
@JustinTime: terima kasih; Saya menghapus kalimat akhir yang salah yang menyatakan bahwa std :: string && akan menjadi referensi universal.
circlepi314
@ circlepi314 Terima kasih. Ini adalah campuran yang mudah dibuat, kadang-kadang dapat membingungkan apakah yang diberikan 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.
Justin Time - Pasang kembali Monica
8

IMO menggunakan referensi C ++ std::stringadalah 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:

  1. Jika Anda menulis semua kode dari luar ke fungsi dalam, Anda tahu apa yang dilakukan kode, Anda dapat menggunakan referensi const std::string &.
  2. Jika Anda menulis kode pustaka atau menggunakan kode pustaka berat di mana string dilewatkan, Anda mungkin mendapatkan lebih banyak dalam arti global dengan mempercayai std::stringperilaku copy constructor.
digital_infinity
sumber
6

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.

slide 24

Benchmark menunjukkan bahwa kelalaian std::stringoleh 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.

JDługosz
sumber
3

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 fyang 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 fdisesuaikan dengan argumen lvalue dan rvalue masing-masing. Untuk melihat mengapa hal ini terjadi, anggaplah fmengambil parameter nilai, di mana Tada beberapa salin dan pindahkan tipe konstruktif:

void f(T x) {
  T y{std::move(x)};
}

Memanggil fdengan argumen lvalue akan menghasilkan copy constructor yang dipanggil untuk membangun x, dan move constructor dipanggil untuk membangun y. Di sisi lain, memanggil fdengan argumen nilai akan menyebabkan pemindah konstruktor dipanggil untuk membangun x, dan pemindah pemindah lain dipanggil untuk membangun y.

Secara umum, implementasi optimal funtuk argumen lvalue adalah sebagai berikut:

void f(const T& x) {
  T y{x};
}

Dalam hal ini, hanya satu copy constructor yang dipanggil untuk membangun y. Implementasi optimal funtuk argumen nilai, sekali lagi secara umum, sebagai berikut:

void f(T&& x) {
  T y{std::move(x)};
}

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 fyang menyalin argumennya, pendekatan pass-by-value akan memiliki lebih banyak overhead daripada pendekatan umum pass-by-const-reference. Pendekatan nilai demi nilai untuk fungsi fyang menyimpan salinan parameternya akan berbentuk:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

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:

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

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 fyang mempertahankan salinan memiliki bentuk:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

Jadi, hanya tugas pindah dalam hal ini. Melewati nilai ke versi fyang mengambil referensi konst hanya biaya tugas alih-alih tugas pindah. Jadi relatif berbicara, versi fmengambil 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 fjika Anda memilih untuk kelebihan pada kategori nilai argumen. Penerusan sempurna memiliki kelemahan yang fmenjadi fungsi templat, yang mencegah membuatnya virtual, dan menghasilkan kode yang jauh lebih kompleks jika Anda ingin mendapatkannya 100% benar (lihat pembicaraan untuk detail berdarah).

Ton van den Heuvel
sumber
Lihat temuan Herb Sutter dalam jawaban baru saya: hanya lakukan itu ketika Anda memindahkan konstruk , bukan memindahkan penetapan.
JDługosz
1
@ JDługosz, terima kasih atas petunjuk pembicaraan Herb, saya hanya menontonnya dan merevisi jawaban saya sepenuhnya. Saya tidak tahu tentang nasihat (pindah).
Ton van den Heuvel
angka dan saran itu ada dalam dokumen Pedoman Standar , sekarang.
JDługosz
1

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.

Erik Aronesty
sumber
@BenVoigt ya ... alih-alih membuang, itu harus bisa berubah ... tetapi Anda tidak bisa mengubah anggota STL menjadi bisa berubah di kelas turunan.
Erik Aronesty