Saya melihat kode di suatu tempat di mana seseorang memutuskan untuk menyalin objek dan kemudian memindahkannya ke anggota data kelas. Ini membuat saya bingung karena saya pikir inti dari pindah adalah untuk menghindari penyalinan. Berikut contohnya:
struct S
{
S(std::string str) : data(std::move(str))
{}
};
Inilah pertanyaan saya:
- Mengapa kita tidak mengambil rvalue-reference
str
? - Bukankah salinannya mahal, terutama jika diberi sesuatu seperti itu
std::string
? - Apa alasan penulis memutuskan untuk membuat salinan lalu pindah?
- Kapan saya harus melakukan ini sendiri?
c++
c++11
move-semantics
pengguna2030677
sumber
sumber
Jawaban:
Sebelum saya menjawab pertanyaan Anda, satu hal yang tampaknya Anda salah: menganggap nilai dalam C ++ 11 tidak selalu berarti menyalin. Jika nilai r dilewatkan, itu akan dipindahkan (asalkan ada konstruktor pemindahan yang layak) daripada disalin. Dan
std::string
memang memiliki konstruktor bergerak.Tidak seperti di C ++ 03, di C ++ 11 sering idiomatis untuk mengambil parameter berdasarkan nilai, untuk alasan yang akan saya jelaskan di bawah. Lihat juga Tanya Jawab ini di StackOverflow untuk kumpulan pedoman yang lebih umum tentang cara menerima parameter.
Karena itu akan membuat tidak mungkin untuk melewatkan lvalues, seperti di:
Jika
S
hanya memiliki konstruktor yang menerima rvalues, hal di atas tidak akan dikompilasi.Jika Anda melewatkan nilai r, nilai itu akan dipindahkan ke
str
, dan pada akhirnya akan dipindahkan kedata
. Tidak ada penyalinan yang akan dilakukan. Sebaliknya, jika Anda memberikan nilai l, nilai l itu akan disalin kestr
, dan kemudian dipindahkan kedata
.Jadi untuk meringkasnya, dua gerakan untuk nilai r, satu salinan dan satu gerakan untuk nilai l.
Pertama-tama, seperti yang saya sebutkan di atas, yang pertama tidak selalu merupakan salinan; dan ini berkata, jawabannya adalah: " Karena efisien (memindahkan
std::string
benda murah) dan sederhana ".Dengan asumsi bahwa pergerakan itu murah (mengabaikan SSO di sini), mereka praktis dapat diabaikan saat mempertimbangkan efisiensi keseluruhan dari desain ini. Jika kami melakukannya, kami memiliki satu salinan untuk lvalues (seperti yang akan kami miliki jika kami menerima referensi lvalue
const
) dan tidak ada salinan untuk rvalues (sementara kami masih akan memiliki salinan jika kami menerima referensi lvalueconst
).Ini berarti bahwa mengambil nilai sama baiknya dengan mengambil dengan referensi nilai
const
l saat nilai l diberikan, dan lebih baik lagi saat nilai r diberikan.PS: Untuk memberikan beberapa konteks, saya yakin ini adalah T&J yang dimaksud OP.
sumber
const T&
argumen: dalam kasus terburuk (lvalue) ini sama, tetapi dalam kasus sementara Anda hanya perlu memindahkan yang sementara. Menang-menang.data
anggota Anda )? Anda akan memiliki salinannya bahkan jika Anda akan mengambil dengan referensi lvalueconst
data
!Untuk memahami mengapa ini adalah pola yang baik, kita harus memeriksa alternatifnya, baik di C ++ 03 dan di C ++ 11.
Kami memiliki metode C ++ 03 untuk mengambil
std::string const&
:dalam hal ini, akan selalu ada satu salinan yang dilakukan. Jika Anda membuat dari string C mentah, a
std::string
akan dibangun, lalu disalin lagi: dua alokasi.Ada metode C ++ 03 untuk mengambil referensi ke a
std::string
, lalu menukarnya menjadi lokalstd::string
:itu adalah "semantik bergerak" versi C ++ 03, dan
swap
sering kali dapat dioptimalkan agar sangat murah untuk dilakukan (seperti amove
). Ini juga harus dianalisis dalam konteks:dan memaksa Anda untuk membentuk non-sementara
std::string
, lalu membuangnya. (Sementarastd::string
tidak dapat mengikat ke referensi non-const). Namun, hanya satu alokasi yang dilakukan. Versi C ++ 11 akan mengambil&&
dan meminta Anda untuk memanggilnya denganstd::move
, atau dengan sementara: ini mengharuskan pemanggil secara eksplisit membuat salinan di luar panggilan, dan memindahkan salinan itu ke dalam fungsi atau konstruktor.Menggunakan:
Selanjutnya, kita dapat melakukan versi C ++ 11 lengkap, yang mendukung penyalinan dan
move
:Kami kemudian dapat memeriksa bagaimana ini digunakan:
Cukup jelas bahwa teknik kelebihan beban 2 ini setidaknya sama efisiennya, jika tidak lebih efisien, daripada dua gaya C ++ 03 di atas. Saya akan menjuluki versi 2-kelebihan ini sebagai versi "paling optimal".
Sekarang, kita akan memeriksa versi take-by-copy:
di setiap skenario tersebut:
Jika Anda membandingkan ini secara berdampingan dengan versi "paling optimal", kami melakukan satu tambahan
move
! Tidak sekali kami melakukan ekstracopy
.Jadi jika kami menganggap itu
move
murah, versi ini memberi kami kinerja yang hampir sama dengan versi paling optimal, tetapi kode 2 kali lebih sedikit.Dan jika Anda mengambil 2 hingga 10 argumen, pengurangan kode adalah eksponensial - 2x kali lebih kecil dengan 1 argumen, 4x dengan 2, 8x dengan 3, 16x dengan 4, 1024x dengan 10 argumen.
Sekarang, kita bisa menyiasatinya melalui penerusan sempurna dan SFINAE, memungkinkan Anda untuk menulis satu konstruktor atau template fungsi yang membutuhkan 10 argumen, melakukan SFINAE untuk memastikan bahwa argumen memiliki jenis yang sesuai, dan kemudian memindahkan-atau-menyalinnya ke dalam negara bagian lokal sesuai kebutuhan. Meskipun hal ini mencegah masalah ukuran program yang bertambah ribuan kali lipat, masih ada tumpukan fungsi yang dihasilkan dari template ini. (Instansiasi fungsi template menghasilkan fungsi)
Dan banyak fungsi yang dihasilkan berarti ukuran kode yang dapat dieksekusi lebih besar, yang dengan sendirinya dapat mengurangi kinerja.
Dengan biaya beberapa
move
detik, kita mendapatkan kode yang lebih pendek dan kinerja yang hampir sama, dan seringkali lebih mudah untuk memahami kode.Sekarang, ini hanya berfungsi karena kita tahu, ketika fungsi (dalam hal ini, konstruktor) dipanggil, bahwa kita akan menginginkan salinan lokal dari argumen itu. Idenya adalah jika kita tahu bahwa kita akan membuat salinan, kita harus memberi tahu penelepon bahwa kita sedang membuat salinan dengan memasukkannya ke dalam daftar argumen kita. Mereka kemudian dapat mengoptimalkan sekitar fakta bahwa mereka akan memberi kita salinannya (dengan beralih ke argumen kita, misalnya).
Keuntungan lain dari teknik 'ambil dengan nilai "adalah bahwa sering memindahkan konstruktor tidak terkecuali. Itu berarti fungsi yang mengambil nilai demi dan keluar dari argumennya sering kali tidak terkecuali, memindahkan apa pun
throw
keluar dari tubuhnya dan ke dalam lingkup pemanggilan (yang kadang-kadang dapat menghindarinya melalui konstruksi langsung, atau membangun item danmove
menjadi argumen, untuk mengontrol di mana terjadi lemparan) Membuat metode nothrow seringkali sepadan.sumber
noexcept
. Dengan mengambil data demi-salinan, Anda dapat membuat fungsi Andanoexcept
, dan memiliki konstruksi salinan yang menyebabkan potensi lemparan (seperti kehabisan memori) terjadi di luar pemanggilan fungsi Anda.Ini mungkin disengaja dan mirip dengan idiom salin dan tukar . Pada dasarnya karena string disalin sebelum konstruktor, konstruktor itu sendiri adalah pengecualian aman karena hanya menukar (memindahkan) string sementara str.
sumber
Anda tidak ingin mengulang diri sendiri dengan menulis konstruktor untuk pemindahan dan satu untuk salinannya:
Ini banyak kode boilerplate, terutama jika Anda memiliki banyak argumen. Solusi Anda menghindari duplikasi pada biaya perpindahan yang tidak perlu. (Namun, operasi pemindahan seharusnya cukup murah.)
Idiom yang bersaing adalah menggunakan penerusan sempurna:
Template ajaib akan memilih untuk dipindahkan atau disalin tergantung pada parameter yang Anda berikan. Ini pada dasarnya memperluas ke versi pertama, di mana kedua konstruktor ditulis dengan tangan. Untuk informasi latar belakang, lihat posting Scott Meyer tentang referensi universal .
Dari aspek kinerja, versi penerusan yang sempurna lebih unggul dari versi Anda karena menghindari gerakan yang tidak perlu. Namun, orang dapat membantah bahwa versi Anda lebih mudah dibaca dan ditulis. Dampak kinerja yang mungkin terjadi seharusnya tidak menjadi masalah dalam kebanyakan situasi, jadi pada akhirnya ini tampaknya menjadi masalah gaya.
sumber