Saya baru saja selesai mendengarkan wawancara podcast radio Rekayasa Perangkat Lunak dengan Scott Meyers mengenai C ++ 0x . Sebagian besar fitur baru masuk akal bagi saya, dan saya benar-benar bersemangat tentang C ++ 0x sekarang, dengan pengecualian satu. Saya masih tidak bisa bergerak semantik ... Apa itu sebenarnya?
c++
c++-faq
c++11
move-semantics
dicroce
sumber
sumber
Jawaban:
Saya merasa paling mudah untuk memahami semantik bergerak dengan kode contoh. Mari kita mulai dengan kelas string yang sangat sederhana yang hanya memegang pointer ke blok memori yang dialokasikan heap:
Karena kita memilih untuk mengelola ingatan kita sendiri, kita perlu mengikuti aturan ketiganya . Saya akan menunda menulis operator penugasan dan hanya mengimplementasikan destructor dan copy constructor untuk saat ini:
Copy constructor mendefinisikan apa artinya menyalin objek string. Parameter
const string& that
mengikat semua ekspresi string tipe yang memungkinkan Anda untuk membuat salinan dalam contoh berikut:Sekarang tiba wawasan kunci ke dalam semantik bergerak. Perhatikan bahwa hanya pada baris pertama di mana kita menyalin, salinan
x
yang dalam ini benar-benar diperlukan, karena kita mungkin ingin memeriksanyax
nanti dan akan sangat terkejut jikax
entah bagaimana telah berubah. Apakah Anda memperhatikan bagaimana saya hanya mengatakanx
tiga kali (empat kali jika Anda memasukkan kalimat ini) dan berarti objek yang sama persis setiap kali? Kami menyebut ekspresi sepertix
"nilai".Argumen di baris 2 dan 3 bukanlah nilai, tetapi nilai, karena objek string yang mendasari tidak memiliki nama, sehingga klien tidak memiliki cara untuk memeriksanya lagi pada titik waktu berikutnya. rvalues menunjukkan objek sementara yang dihancurkan pada titik koma berikutnya (lebih tepatnya: pada akhir ekspresi penuh yang secara leksikal berisi nilai). Ini penting karena selama inisialisasi
b
danc
, kita bisa melakukan apa pun yang kita inginkan dengan string sumber, dan klien tidak dapat membedakannya !C ++ 0x memperkenalkan mekanisme baru yang disebut "referensi nilai" yang, antara lain, memungkinkan kami untuk mendeteksi argumen nilai melalui fungsi yang berlebihan. Yang harus kita lakukan adalah menulis konstruktor dengan parameter referensi nilai. Di dalam konstruktor bahwa kita bisa melakukan apapun yang kita inginkan dengan sumber, selama kita meninggalkannya di beberapa negara yang valid:
Apa yang sudah kita lakukan di sini? Alih-alih menyalin secara mendalam data tumpukan, kami hanya menyalin pointer dan kemudian mengatur pointer asli ke nol (untuk mencegah 'hapus []' dari destruktor objek sumber dari merilis 'data yang baru saja dicuri'). Akibatnya, kami telah "mencuri" data yang semula milik string sumber. Sekali lagi, wawasan kuncinya adalah bahwa dalam keadaan apa pun klien tidak dapat mendeteksi bahwa sumber telah dimodifikasi. Karena kami tidak benar-benar menyalin di sini, kami menyebut konstruktor ini sebagai "pindahkan konstruktor". Tugasnya adalah memindahkan sumber daya dari satu objek ke objek lain alih-alih menyalinnya.
Selamat, Anda sekarang mengerti dasar-dasar pindahan semantik! Mari kita lanjutkan dengan mengimplementasikan operator penugasan. Jika Anda tidak terbiasa dengan idiom copy dan swap , pelajari dan kembali, karena ini adalah idiom C ++ yang luar biasa terkait dengan keamanan pengecualian.
Hah, itu dia? "Di mana referensi nilainya?" Anda mungkin bertanya. "Kami tidak membutuhkannya di sini!" adalah jawaban saya :)
Perhatikan bahwa kita melewatkan parameter
that
dengan nilai , jadithat
harus diinisialisasi sama seperti objek string lainnya. Bagaimana tepatnyathat
akan diinisialisasi? Di masa lalu C ++ 98 , jawabannya adalah "oleh copy constructor". Dalam C ++ 0x, kompiler memilih antara copy constructor dan move constructor berdasarkan pada apakah argumen kepada operator penugasan adalah lvalue atau rvalue.Jadi jika Anda mengatakan
a = b
, konstruktor salin akan menginisialisasithat
(karena ekspresib
adalah nilai), dan operator penugasan menukar konten dengan salinan yang baru dibuat, dalam. Itulah definisi idiom copy dan swap - buat salinan, tukar konten dengan salinan, lalu singkirkan salinan dengan meninggalkan ruang lingkup. Tidak ada yang baru di sini.Tetapi jika Anda mengatakan
a = x + y
, konstruktor bergerak akan menginisialisasithat
(karena ekspresix + y
adalah nilai), sehingga tidak ada salinan yang terlibat, hanya langkah efisien.that
masih merupakan objek independen dari argumen, tetapi konstruksinya sepele, karena tumpukan data tidak harus disalin, hanya dipindahkan. Tidak perlu menyalinnya karenax + y
merupakan nilai, dan sekali lagi, tidak apa-apa untuk bergerak dari objek string yang dilambangkan dengan nilai.Untuk meringkas, konstruktor salinan membuat salinan yang dalam, karena sumbernya harus tetap tidak tersentuh. Konstruktor pemindahan, di sisi lain, cukup menyalin pointer dan kemudian mengatur pointer di sumber ke nol. Tidak masalah untuk "membatalkan" objek sumber dengan cara ini, karena klien tidak memiliki cara untuk memeriksa objek lagi.
Saya harap contoh ini sampai di titik utama. Ada banyak lagi untuk menilai kembali referensi dan memindahkan semantik yang sengaja saya tinggalkan agar tetap sederhana. Jika Anda ingin detail lebih lanjut, lihat jawaban pelengkap saya .
sumber
that.data = 0
, karakter akan dihancurkan terlalu dini (ketika sementara mati), dan juga dua kali. Anda ingin mencuri data, bukan membagikannya!delete[]
pada nullptr didefinisikan oleh standar C ++ sebagai no-op.Jawaban pertama saya adalah pengantar yang sangat disederhanakan untuk memindahkan semantik, dan banyak detail yang sengaja dibuat untuk membuatnya tetap sederhana. Namun, ada banyak lagi untuk memindahkan semantik, dan saya pikir sudah waktunya untuk jawaban kedua untuk mengisi kekosongan. Jawaban pertama sudah cukup lama, dan rasanya tidak benar hanya menggantinya dengan teks yang sama sekali berbeda. Saya pikir ini masih berfungsi dengan baik sebagai pengantar pertama. Tetapi jika Anda ingin menggali lebih dalam, baca terus :)
Stephan T. Lavavej meluangkan waktu untuk memberikan umpan balik yang berharga. Terima kasih banyak, Stephan!
pengantar
Pindah semantik memungkinkan suatu objek, dalam kondisi tertentu, untuk mengambil kepemilikan sumber daya eksternal beberapa objek lain. Ini penting dalam dua cara:
Mengubah salinan mahal menjadi gerakan murah. Lihat jawaban pertama saya sebagai contoh. Perhatikan bahwa jika suatu objek tidak mengelola setidaknya satu sumber daya eksternal (baik secara langsung, atau tidak langsung melalui objek anggotanya), pindahkan semantik tidak akan menawarkan keuntungan apa pun dibandingkan salinan semantik. Dalam hal itu, menyalin objek dan memindahkan objek berarti hal yang sama persis:
Menerapkan tipe aman "hanya bergerak"; yaitu, jenis-jenis yang penyalinannya tidak masuk akal, tetapi pemindahan tidak. Contohnya termasuk kunci, pegangan file, dan petunjuk pintar dengan semantik kepemilikan yang unik. Catatan: Jawaban ini membahas
std::auto_ptr
, templat perpustakaan standar C ++ 98 yang sudah tidak digunakan lagi, yang digantikan olehstd::unique_ptr
dalam C ++ 11. Pemrogram C ++ menengah mungkin paling tidak agak familiarstd::auto_ptr
, dan karena "pindahkan semantik" yang ditampilkannya, sepertinya merupakan titik awal yang baik untuk membahas pindahan semantik di C ++ 11. YMMV.Apa itu langkah?
Pustaka standar C ++ 98 menawarkan penunjuk cerdas dengan semantik kepemilikan unik yang disebut
std::auto_ptr<T>
. Jika Anda belum terbiasaauto_ptr
, tujuannya adalah untuk menjamin bahwa objek yang dialokasikan secara dinamis selalu dirilis, bahkan dalam menghadapi pengecualian:Hal yang tidak biasa
auto_ptr
adalah perilaku "menyalin":Perhatikan bagaimana inisialisasi
b
dengana
tidak tidak menyalin segitiga, melainkan transfer kepemilikan segitiga daria
keb
. Kami juga mengatakan "a
ini dipindahkan keb
" atau "segitiga tersebut dipindahkan daria
keb
". Ini mungkin terdengar membingungkan karena segitiga itu sendiri selalu berada di tempat yang sama dalam memori.Pembuat salinan dari
auto_ptr
mungkin terlihat seperti ini (agak disederhanakan):Bergerak berbahaya dan tidak berbahaya
Yang berbahaya tentang
auto_ptr
apa yang secara sintaksis tampak seperti salinan sebenarnya adalah sebuah langkah. Mencoba memanggil fungsi anggota pada pindah-dariauto_ptr
akan memanggil perilaku yang tidak terdefinisi, jadi Anda harus sangat berhati-hati untuk tidak menggunakanauto_ptr
setelah itu telah dipindahkan dari:Namun
auto_ptr
tidak selalu berbahaya. Fungsi pabrik adalah kasus penggunaan yang sangat baik untukauto_ptr
:Perhatikan bagaimana kedua contoh mengikuti pola sintaksis yang sama:
Namun, salah satu dari mereka memanggil perilaku yang tidak terdefinisi, sedangkan yang lain tidak. Jadi apa perbedaan antara ekspresi
a
danmake_triangle()
? Bukankah keduanya memiliki tipe yang sama? Memang benar, tetapi mereka memiliki kategori nilai yang berbeda .Kategori nilai
Jelas, harus ada beberapa perbedaan besar antara ekspresi
a
yang menunjukkanauto_ptr
variabel, dan ekspresimake_triangle()
yang menunjukkan panggilan fungsi yang mengembalikanauto_ptr
nilai, sehingga menciptakanauto_ptr
objek sementara yang baru setiap kali dipanggil.a
adalah contoh dari sebuah lvalue , sedangkanmake_triangle()
adalah contoh dari nilai p .Pindah dari nilai-nilai seperti
a
itu berbahaya, karena kita nanti bisa mencoba memanggil fungsi anggota melaluia
, memanggil perilaku yang tidak terdefinisi. Di sisi lain, pindah dari nilai-nilai sepertimake_triangle()
sangat aman, karena setelah copy constructor melakukan tugasnya, kita tidak bisa menggunakan yang sementara lagi. Tidak ada ungkapan yang menunjukkan kata sementara; jika kita hanya menulismake_triangle()
lagi, kita mendapatkan perbedaan sementara. Bahkan, pindah-dari sementara sudah hilang di baris berikutnya:Perhatikan bahwa surat-surat
l
danr
memiliki asal bersejarah di sisi kiri dan kanan tugas. Ini tidak lagi benar dalam C ++, karena ada nilai yang tidak dapat muncul di sisi kiri suatu tugas (seperti array atau tipe yang ditentukan pengguna tanpa operator penugasan), dan ada nilai yang bisa (semua nilai dari tipe kelas dengan operator penugasan).Referensi nilai
Kita sekarang mengerti bahwa pindah dari nilai-nilai berpotensi berbahaya, tetapi bergerak dari nilai-nilai tidak berbahaya. Jika C ++ memiliki dukungan bahasa untuk membedakan argumen lvalue dari argumen rvalue, kami dapat melarang sepenuhnya pindah dari lvalues, atau setidaknya membuat bergerak dari lvalues secara eksplisit di situs panggilan, sehingga kami tidak lagi bergerak secara tidak sengaja.
Jawaban C ++ 11 untuk masalah ini adalah referensi nilai . Referensi nilai adalah jenis referensi baru yang hanya mengikat nilai, dan sintaksnya
X&&
. Referensi lama yang baikX&
sekarang dikenal sebagai referensi nilai . (Perhatikan bahwaX&&
adalah tidak referensi ke referensi, tidak ada hal seperti itu di C ++.)Jika kita memasukkan
const
campuran, kita sudah memiliki empat jenis referensi. Jenis ekspresi seperti apa yangX
bisa mereka ikat?Dalam praktiknya, Anda bisa melupakannya
const X&&
. Terbatas untuk membaca dari nilai tidak sangat berguna.Konversi tersirat
Referensi nilai melewati beberapa versi. Sejak versi 2.1, referensi nilai
X&&
juga mengikat semua kategori nilai dari jenis yang berbedaY
, asalkan ada konversi implisit dariY
keX
. Dalam hal itu, sementara jenisX
dibuat, dan referensi nilai terikat untuk sementara itu:Dalam contoh di atas,
"hello world"
adalah lvalue tipeconst char[12]
. Karena ada konversi implisit dariconst char[12]
melaluiconst char*
kestd::string
, sebuah sementara tipestd::string
dibuat, danr
pasti bahwa sementara. Ini adalah salah satu kasus di mana perbedaan antara rvalues (ekspresi) dan temporaries (objek) agak buram.Pindahkan konstruktor
Contoh berguna dari fungsi dengan
X&&
parameter adalah move constructorX::X(X&& source)
. Tujuannya adalah untuk mentransfer kepemilikan sumber daya yang dikelola dari sumber ke objek saat ini.Dalam C ++ 11,
std::auto_ptr<T>
telah digantikan olehstd::unique_ptr<T>
yang mengambil keuntungan dari referensi nilai. Saya akan mengembangkan dan mendiskusikan versi sederhana dariunique_ptr
. Pertama, kami merangkum pointer mentah dan membebani operator->
dan*
, jadi kelas kami terasa seperti pointer:Konstruktor mengambil kepemilikan objek, dan destruktor menghapusnya:
Sekarang sampai pada bagian yang menarik, move constructor:
Konstruktor gerakan ini melakukan persis apa yang dilakukan
auto_ptr
konstruktor salin, tetapi hanya dapat diberikan dengan nilai:Baris kedua gagal dikompilasi, karena
a
merupakan nilai lv, tetapi parameterunique_ptr&& source
hanya dapat terikat ke nilai. Inilah yang kami inginkan; gerakan berbahaya tidak boleh implisit. Baris ketiga mengkompilasi dengan baik, karenamake_triangle()
merupakan nilai. Konstruktor pemindahan akan mengalihkan kepemilikan dari sementara kec
. Sekali lagi, inilah yang kami inginkan.Pindahkan operator penugasan
Bagian yang hilang terakhir adalah operator penugasan pindah. Tugasnya adalah melepaskan sumber daya lama dan memperoleh sumber daya baru dari argumennya:
Perhatikan bagaimana implementasi operator penugasan langkah ini menduplikasi logika destruktor dan konstruktor bergerak. Apakah Anda terbiasa dengan idiom copy-and-swap? Ini juga dapat diterapkan untuk memindahkan semantik sebagai idiom move-and-swap:
Sekarang itu
source
adalah variabel tipeunique_ptr
, itu akan diinisialisasi oleh move constructor; artinya, argumen akan dipindahkan ke parameter. Argumen masih diperlukan untuk menjadi nilai, karena konstruktor bergerak itu sendiri memiliki parameter referensi nilai. Ketika aliran kontrol mencapai penjepit penutupanoperator=
,source
keluar dari ruang lingkup, melepaskan sumber daya lama secara otomatis.Pindah dari nilai-nilai
Terkadang, kami ingin beralih dari nilai-nilai. Yaitu, kadang-kadang kita ingin kompiler memperlakukan nilai l seolah-olah itu nilai, sehingga dapat memanggil move constructor, meskipun itu bisa berpotensi tidak aman. Untuk tujuan ini, C ++ 11 menawarkan templat fungsi pustaka standar yang disebut
std::move
di dalam header<utility>
. Nama ini agak disayangkan, karenastd::move
hanya melemparkan nilai ke nilai; itu tidak memindahkan apa pun dengan sendirinya. Itu hanya memungkinkan bergerak. Mungkin seharusnya dinamaistd::cast_to_rvalue
ataustd::enable_move
, tapi kita terjebak dengan namanya sekarang.Inilah cara Anda secara eksplisit beralih dari nilai:
Perhatikan bahwa setelah baris ketiga,
a
tidak lagi memiliki segitiga. Tidak apa-apa, karena dengan menulis secara eksplisitstd::move(a)
, kami membuat niat kami jelas: "Konstruktor yang terhormat, lakukan apa pun yang Anda inginkana
untuk menginisialisasic
; Saya tidak pedulia
lagi. Jangan ragu untuk membantu Andaa
."Nilai X
Perhatikan bahwa meskipun
std::move(a)
merupakan nilai, evaluasinya tidak membuat objek sementara. Teka-teki ini memaksa komite untuk memperkenalkan kategori nilai ketiga. Sesuatu yang dapat terikat pada referensi nilai, meskipun itu bukan nilai dalam arti tradisional, disebut nilai x (nilai eXpiring). Nilai-nilai tradisional diubah namanya menjadi nilai-nilai (Nilai murni).Baik prvalues dan xvalues adalah rvalues. Xvalues dan lvalues keduanya adalah glvalues (Generalized lvalues). Hubungan lebih mudah dipahami dengan diagram:
Perhatikan bahwa hanya nilai x yang benar-benar baru; sisanya hanya karena penggantian nama dan pengelompokan.
Keluar dari fungsi
Sejauh ini, kita telah melihat pergerakan ke variabel lokal, dan ke dalam parameter fungsi. Tetapi bergerak juga mungkin terjadi dalam arah yang berlawanan. Jika fungsi kembali berdasarkan nilai, beberapa objek di situs panggilan (mungkin variabel lokal atau sementara, tetapi bisa berupa objek apa pun) diinisialisasi dengan ekspresi setelah
return
pernyataan sebagai argumen ke konstruktor bergerak:Mungkin mengejutkan, objek otomatis (variabel lokal yang tidak dideklarasikan sebagai
static
) juga dapat secara implisit dipindahkan dari fungsi:Kenapa konstruktor langkah menerima nilai
result
sebagai argumen? Ruang lingkupresult
akan berakhir, dan itu akan hancur selama tumpukan unwinding. Tidak ada yang bisa mengeluh setelah itu yangresult
entah bagaimana berubah; ketika aliran kontrol kembali ke pemanggil,result
tidak ada lagi! Untuk alasan itu, C ++ 11 memiliki aturan khusus yang memungkinkan pengembalian objek otomatis dari fungsi tanpa harus menulisstd::move
. Bahkan, Anda tidak boleh menggunakanstd::move
untuk memindahkan objek otomatis dari fungsi, karena ini menghambat "optimasi nilai pengembalian bernama" (NRVO).Perhatikan bahwa di kedua fungsi pabrik, tipe pengembalian adalah nilai, bukan referensi nilai. Referensi nilai masih referensi, dan seperti biasa, Anda tidak boleh mengembalikan referensi ke objek otomatis; penelepon akan berakhir dengan referensi menggantung jika Anda menipu kompiler agar menerima kode Anda, seperti ini:
Pindah ke anggota
Cepat atau lambat, Anda akan menulis kode seperti ini:
Pada dasarnya, kompiler akan mengeluh bahwa itu
parameter
adalah nilai. Jika Anda melihat tipenya, Anda melihat referensi nilai, tetapi referensi nilai berarti "referensi yang terikat dengan nilai"; itu tidak berarti bahwa referensi itu sendiri adalah nilai! Memang,parameter
hanya variabel biasa dengan nama. Anda dapat menggunakanparameter
sesering mungkin di dalam tubuh konstruktor, dan selalu menunjukkan objek yang sama. Secara implisit pindah darinya akan berbahaya, karena itu bahasa melarangnya.Solusinya adalah mengaktifkan langkah secara manual:
Anda bisa membantah bahwa
parameter
tidak digunakan lagi setelah inisialisasimember
. Mengapa tidak ada aturan khusus untuk memasukkan secara diam-diamstd::move
seperti halnya dengan nilai pengembalian? Mungkin karena akan terlalu banyak beban pada implementor compiler. Misalnya, bagaimana jika badan konstruktor ada di unit terjemahan lain? Sebaliknya, aturan nilai balik hanya perlu memeriksa tabel simbol untuk menentukan apakah pengidentifikasi setelahreturn
kata kunci menunjukkan objek otomatis.Anda juga dapat melewati nilai
parameter
berdasarkan. Untuk tipe bergerak saja sepertiunique_ptr
, sepertinya belum ada idiom yang mapan. Secara pribadi, saya lebih suka memberikan nilai, karena menyebabkan lebih sedikit kekacauan dalam antarmuka.Fungsi anggota khusus
C ++ 98 secara implisit mendeklarasikan tiga fungsi anggota khusus sesuai permintaan, yaitu, ketika dibutuhkan di suatu tempat: copy constructor, operator penugasan copy, dan destructor.
Referensi nilai melewati beberapa versi. Sejak versi 3.0, C ++ 11 menyatakan dua fungsi anggota khusus tambahan sesuai permintaan: konstruktor gerakan dan operator penugasan langkah. Perhatikan bahwa baik VC10 maupun VC11 belum sesuai dengan versi 3.0, jadi Anda harus mengimplementasikannya sendiri.
Dua fungsi anggota khusus yang baru ini hanya secara implisit dinyatakan jika tidak ada fungsi anggota khusus yang dinyatakan secara manual. Juga, jika Anda mendeklarasikan move constructor Anda sendiri atau memindahkan operator penugasan, baik copy constructor maupun operator penugasan salinan tidak akan dinyatakan secara implisit.
Apa arti aturan ini dalam praktik?
Perhatikan bahwa operator penugasan salinan dan operator penugasan bergerak dapat digabungkan menjadi satu operator penugasan tunggal, yang mengambil argumen berdasarkan nilai:
Dengan cara ini, jumlah fungsi anggota khusus untuk mengimplementasikan turun dari lima menjadi empat. Ada tradeoff antara pengecualian-keselamatan dan efisiensi di sini, tapi saya bukan ahli dalam masalah ini.
Referensi penerusan ( sebelumnya dikenal sebagai referensi Universal )
Pertimbangkan templat fungsi berikut:
Anda mungkin berharap
T&&
hanya mengikat nilai, karena pada pandangan pertama, itu terlihat seperti referensi nilai. Namun ternyata,T&&
juga mengikat nilai-nilai:Jika argumennya adalah nilai jenis
X
,T
disimpulkanX
, makaT&&
berartiX&&
. Inilah yang orang harapkan. Tetapi jika argumen adalah nilai jenisX
, karena aturan khusus,T
disimpulkanX&
, makaT&&
akan berarti sesuatu sepertiX& &&
. Tapi karena C ++ masih tidak memiliki gagasan referensi ke referensi, tipeX& &&
ini diciutkan menjadiX&
. Ini mungkin terdengar membingungkan dan tidak berguna pada awalnya, tetapi runtuh referensi sangat penting untuk penerusan yang sempurna (yang tidak akan dibahas di sini).Jika Anda ingin membatasi templat fungsi ke nilai, Anda bisa menggabungkan SFINAE dengan tipe traits:
Implementasi langkah
Sekarang setelah Anda memahami runtuhnya referensi, berikut ini cara
std::move
penerapannya:Seperti yang Anda lihat,
move
menerima segala jenis parameter berkat referensi penerusanT&&
, dan mengembalikan referensi nilai. Thestd::remove_reference<T>::type
meta-fungsi panggilan diperlukan karena jika tidak, untuk lvalues tipeX
, jenis kembali akanX& &&
, yang akan runtuh keX&
. Karenat
selalu merupakan lvalue (ingat bahwa referensi rvalue bernama adalah lvalue), tetapi kami ingin mengikatt
ke rvalue referensi, kami harus secara eksplisit melemparkant
ke tipe pengembalian yang benar. Panggilan fungsi yang mengembalikan referensi nilai sendiri merupakan nilai tambah. Sekarang Anda tahu dari mana nilai-nilai berasal;)Perhatikan bahwa mengembalikan dengan referensi nilai baik-baik saja dalam contoh ini, karena
t
tidak menunjukkan objek otomatis, tetapi sebaliknya objek yang dilewatkan oleh pemanggil.sumber
Pindah semantik didasarkan pada referensi nilai .
Nilai adalah objek sementara, yang akan dihancurkan pada akhir ekspresi. Dalam C ++ saat ini, nilai hanya mengikat ke
const
referensi. C ++ 1x akan memungkinkanconst
referensi non-nilai , diejaT&&
, yang merupakan referensi ke objek nilai.Karena nilai akan mati pada akhir ekspresi, Anda dapat mencuri datanya . Alih-alih menyalinnya ke objek lain, Anda memindahkan datanya ke dalamnya.
Pada kode di atas, dengan kompiler lama hasil
f()
yang disalin ke dalamx
menggunakanX
's copy constructor. Jika kompiler Anda mendukung pindahan semantik danX
memiliki pindahkan-konstruktor, maka itu disebut sebagai gantinya. Sejakrhs
argumen adalah nilai p , kita tahu itu tidak diperlukan lagi dan kami bisa mencuri nilainya.Jadi nilai dipindahkan dari sementara yang tidak disebutkan namanya dikembalikan dari
f()
kex
(sementara datax
, diinisialisasi ke kosongX
, dipindahkan ke sementara, yang akan hancur setelah penugasan).sumber
this->swap(std::move(rhs));
karena referensi nilai yang dinamai adalah nilai-nilairhs
adalah nilai dalam konteksX::X(X&& rhs)
. Anda perlu meneleponstd::move(rhs)
untuk mendapatkan nilai, tetapi ini agak membuat jawabannya diperdebatkan.Misalkan Anda memiliki fungsi yang mengembalikan objek besar:
Ketika Anda menulis kode seperti ini:
kemudian kompiler C ++ biasa akan membuat objek sementara untuk hasil
multiply()
, memanggil copy constructor untuk menginisialisasir
, dan kemudian merusak nilai pengembalian sementara. Memindahkan semantik di C ++ 0x memungkinkan "move constructor" dipanggil untuk menginisialisasir
dengan menyalin isinya, dan kemudian membuang nilai sementara tanpa harus merusaknya.Ini sangat penting jika (seperti mungkin
Matrix
contoh di atas), objek yang disalin mengalokasikan memori tambahan pada heap untuk menyimpan representasi internal. Pembuat salinan harus membuat salinan lengkap dari representasi internal, atau menggunakan penghitungan referensi dan semantik copy-on-write secara internal. Konstruktor bergerak akan meninggalkan memori tumpukan dan hanya menyalin pointer di dalamMatrix
objek.sumber
Jika Anda benar-benar tertarik dengan penjelasan yang baik dan mendalam tentang semantik langkah, saya sangat merekomendasikan membaca makalah asli pada mereka, "Sebuah Proposal untuk Menambahkan Dukungan Pindah Semantik ke Bahasa C ++."
Ini sangat mudah diakses dan mudah dibaca dan itu membuat kasus yang sangat baik untuk manfaat yang mereka tawarkan. Ada makalah lain yang lebih baru dan terkini tentang semantik bergerak yang tersedia di situs WG21 , tetapi yang ini mungkin yang paling mudah karena mendekati hal-hal dari tampilan tingkat atas dan tidak terlalu banyak membahas detail bahasa kasar.
sumber
Pindah semantik adalah tentang mentransfer sumber daya daripada menyalinnya ketika tidak ada yang membutuhkan nilai sumber lagi.
Dalam C ++ 03, objek sering disalin, hanya untuk dihancurkan atau ditugaskan sebelum kode apa pun menggunakan nilai lagi. Misalnya, ketika Anda kembali dengan nilai dari suatu fungsi — kecuali RVO yang menendang — nilai yang Anda kembalikan disalin ke bingkai tumpukan pemanggil, dan kemudian keluar dari ruang lingkup dan dihancurkan. Ini hanyalah salah satu dari banyak contoh: lihat nilai demi nilai ketika objek sumber bersifat sementara, algoritme seperti
sort
itu hanya mengatur ulang item, realokasivector
ketikacapacity()
terlampaui, dll.Ketika pasangan salinan / penghancuran seperti itu mahal, itu biasanya karena objek memiliki beberapa sumber daya kelas berat. Misalnya,
vector<string>
mungkin memiliki blok memori yang dialokasikan secara dinamis yang berisi berbagaistring
objek, masing-masing dengan memori dinamisnya sendiri. Menyalin objek seperti itu mahal: Anda harus mengalokasikan memori baru untuk setiap blok yang dialokasikan secara dinamis di sumber, dan menyalin semua nilai di seluruh. Maka Anda perlu membatalkan semua memori yang baru saja Anda salin. Namun, memindahkan besarvector<string>
berarti hanya menyalin beberapa petunjuk (yang merujuk pada blok memori dinamis) ke tujuan dan memusatkannya pada sumber.sumber
Secara mudah (praktis):
Menyalin suatu objek berarti menyalin anggota "statis" dan memanggil
new
operator untuk objek dinamisnya. Baik?Namun, untuk memindahkan objek (saya ulangi, dalam sudut pandang praktis) menyiratkan hanya untuk menyalin pointer dari objek dinamis, dan bukan untuk membuat yang baru.
Tapi, bukankah itu berbahaya? Tentu saja, Anda dapat merusak objek dinamis dua kali (kesalahan segmentasi). Jadi, untuk menghindari itu, Anda harus "membatalkan" pointer sumber untuk menghindari merusaknya dua kali:
Ok, tetapi jika saya memindahkan objek, objek sumber menjadi tidak berguna, bukan? Tentu saja, tetapi dalam situasi tertentu itu sangat berguna. Yang paling jelas adalah ketika saya memanggil suatu fungsi dengan objek anonim (temporal, objek nilai, ..., Anda dapat memanggilnya dengan nama yang berbeda):
Dalam situasi itu, objek anonim dibuat, selanjutnya disalin ke parameter fungsi, dan kemudian dihapus. Jadi, di sini lebih baik untuk memindahkan objek, karena Anda tidak memerlukan objek anonim dan Anda dapat menghemat waktu dan memori.
Ini mengarah pada konsep referensi "nilai". Mereka ada di C ++ 11 hanya untuk mendeteksi apakah objek yang diterima anonim atau tidak. Saya pikir Anda sudah tahu bahwa "lvalue" adalah entitas yang dapat dialihkan (bagian kiri dari
=
operator), jadi Anda memerlukan referensi bernama ke objek untuk dapat bertindak sebagai lvalue. Nilai adalah kebalikannya, objek tanpa referensi nama. Karena itu, objek dan nilai anonim adalah sinonim. Begitu:Dalam hal ini, ketika suatu objek tipe
A
harus "disalin", kompiler membuat referensi nilai atau referensi nilai berdasarkan apakah objek yang dilewati dinamai atau tidak. Ketika tidak, move-constructor Anda dipanggil dan Anda tahu objek itu temporal dan Anda dapat memindahkan objek dinamisnya daripada menyalinnya, menghemat ruang dan memori.Penting untuk diingat bahwa objek "statis" selalu disalin. Tidak ada cara untuk "memindahkan" objek statis (objek dalam tumpukan dan bukan pada tumpukan). Jadi, perbedaan "bergerak" / "menyalin" ketika suatu objek tidak memiliki anggota dinamis (langsung atau tidak langsung) tidak relevan.
Jika objek Anda kompleks dan destruktor memiliki efek sekunder lainnya, seperti memanggil ke fungsi perpustakaan, memanggil ke fungsi global lainnya atau apa pun itu, mungkin lebih baik memberi sinyal gerakan dengan bendera:
Jadi, kode Anda lebih pendek (Anda tidak perlu melakukan
nullptr
tugas untuk setiap anggota dinamis) dan lebih umum.Pertanyaan khas lainnya: apa perbedaan antara
A&&
danconst A&&
? Tentu saja, dalam kasus pertama, Anda dapat memodifikasi objek dan yang kedua tidak, tetapi, makna praktis? Dalam kasus kedua, Anda tidak dapat memodifikasinya, jadi Anda tidak memiliki cara untuk membatalkan objek (kecuali dengan tanda yang dapat diubah atau sesuatu seperti itu), dan tidak ada perbedaan praktis dengan pembuat salinan.Dan apakah penerusan yang sempurna ? Penting untuk mengetahui bahwa "referensi nilai" adalah referensi ke objek bernama dalam "ruang lingkup pemanggil". Tetapi dalam lingkup yang sebenarnya, referensi nilai adalah nama ke objek, jadi, itu bertindak sebagai objek bernama. Jika Anda meneruskan referensi nilai ke fungsi lain, Anda meneruskan objek yang dinamai, jadi, objek tidak diterima seperti objek temporal.
Objek
a
akan disalin ke parameter aktualother_function
. Jika Anda ingin objeka
terus diperlakukan sebagai objek sementara, Anda harus menggunakanstd::move
fungsi:Dengan baris ini,
std::move
akan dilemparkana
ke nilai ulang danother_function
akan menerima objek sebagai objek yang tidak disebutkan namanya. Tentu saja, jikaother_function
tidak memiliki kelebihan beban khusus untuk bekerja dengan objek yang tidak disebutkan namanya, perbedaan ini tidak penting.Apakah itu penerusan yang sempurna? Tidak, tapi kami sangat dekat. Penerusan sempurna hanya berguna untuk bekerja dengan templat, dengan tujuan untuk mengatakan: jika saya perlu meneruskan objek ke fungsi lain, saya perlu bahwa jika saya menerima objek bernama, objek dilewatkan sebagai objek bernama, dan ketika tidak, Saya ingin meneruskannya seperti objek yang tidak disebutkan namanya:
Itulah tanda tangan dari fungsi prototipe yang menggunakan penerusan sempurna, diimplementasikan dalam C ++ 11 dengan cara
std::forward
. Fungsi ini mengeksploitasi beberapa aturan instantiation template:Jadi, jika
T
merupakan referensi nilai untukA
( T = A &),a
juga ( A & && => A &). JikaT
merupakan referensi nilai untukA
,a
juga (A&& && = = A&&). Dalam kedua kasus,a
adalah objek bernama dalam lingkup aktual, tetapiT
berisi informasi "tipe referensi" dari sudut pandang ruang lingkup pemanggil. Informasi ini (T
) diteruskan sebagai parameter templat keforward
dan 'a' dipindahkan atau tidak sesuai dengan jenisT
.sumber
Ini seperti menyalin semantik, tetapi alih-alih harus menduplikasi semua data Anda bisa mencuri data dari objek yang "dipindahkan" dari.
sumber
Anda tahu apa artinya semantik salinan? itu berarti Anda memiliki jenis yang dapat disalin, untuk jenis yang ditentukan pengguna, Anda menentukan ini baik membeli secara eksplisit menulis operator konstruktor & penugasan salinan atau kompilator membuatnya secara implisit. Ini akan melakukan salinan.
Pindah semantik pada dasarnya adalah tipe yang ditentukan pengguna dengan konstruktor yang mengambil r-nilai referensi (tipe baru referensi menggunakan && (ya dua ampersand)) yang non-const, ini disebut konstruktor bergerak, yang sama berlaku untuk operator penugasan. Jadi apa yang dilakukan seorang konstruktor bergerak, dan bukannya menyalin memori dari argumen sumbernya, ia 'memindahkan' memori dari sumber ke tujuan.
Kapan Anda ingin melakukan itu? well std :: vector adalah contoh, misalkan Anda membuat std :: vector sementara dan Anda mengembalikannya dari fungsi say:
Anda akan memiliki overhead dari copy constructor ketika fungsi kembali, jika (dan itu akan di C ++ 0x) std :: vector memiliki move constructor alih-alih menyalinnya hanya dapat mengatur pointer itu dan 'memindahkan' dialokasikan secara dinamis memori ke instance baru. Ini semacam semantik pengalihan kepemilikan dengan std :: auto_ptr.
sumber
Untuk mengilustrasikan perlunya memindahkan semantik , mari kita pertimbangkan contoh ini tanpa memindahkan semantik:
Berikut adalah fungsi yang mengambil objek bertipe
T
dan mengembalikan objek dengan tipe yang samaT
:Fungsi di atas menggunakan panggilan dengan nilai yang berarti bahwa ketika fungsi ini disebut objek harus dibangun untuk digunakan oleh fungsi.
Karena fungsi ini juga mengembalikan berdasarkan nilai , objek baru lainnya dibangun untuk nilai pengembalian:
Dua objek baru telah dibangun, salah satunya adalah objek sementara yang hanya digunakan selama durasi fungsi.
Ketika objek baru dibuat dari nilai balik, konstruktor salin dipanggil untuk menyalin konten objek sementara ke objek baru b. Setelah fungsi selesai, objek sementara yang digunakan dalam fungsi keluar dari ruang lingkup dan dihancurkan.
Sekarang, mari kita pertimbangkan apa yang dilakukan copy constructor .
Pertama-tama harus menginisialisasi objek, lalu menyalin semua data yang relevan dari objek lama ke yang baru.
Tergantung pada kelasnya, mungkin wadahnya dengan data yang sangat banyak, maka itu bisa mewakili banyak waktu dan penggunaan memori
Dengan memindahkan semantik sekarang mungkin untuk membuat sebagian besar pekerjaan ini kurang menyenangkan dengan hanya memindahkan data daripada menyalin.
Memindahkan data melibatkan mengaitkan kembali data dengan objek baru. Dan tidak ada salinan sama sekali.
Ini dilakukan dengan
rvalue
referensi.Sebuah
rvalue
referensi bekerja cukup banyak sepertilvalue
referensi dengan satu perbedaan penting:sebuah referensi nilai p dapat dipindahkan dan lvalue tidak bisa.
Dari cppreference.com :
sumber
Saya menulis ini untuk memastikan saya memahaminya dengan benar.
Pindah semantik diciptakan untuk menghindari penyalinan objek besar yang tidak perlu. Bjarne Stroustrup dalam bukunya "Bahasa Pemrograman C ++" menggunakan dua contoh di mana penyalinan yang tidak perlu terjadi secara default: satu, pertukaran dua objek besar, dan dua, pengembalian objek besar dari suatu metode.
Menukar dua objek besar biasanya melibatkan menyalin objek pertama ke objek sementara, menyalin objek kedua ke objek pertama, dan menyalin objek sementara ke objek kedua. Untuk tipe bawaan, ini sangat cepat, tetapi untuk objek besar ketiga salinan ini bisa memakan banyak waktu. "Pindah tugas" memungkinkan pemrogram untuk menimpa perilaku salin default dan alih-alih bertukar referensi ke objek, yang berarti bahwa tidak ada penyalinan sama sekali dan operasi swap jauh lebih cepat. Tugas pemindahan dapat dipanggil dengan memanggil metode std :: move ().
Mengembalikan objek dari suatu metode secara default melibatkan membuat salinan dari objek lokal dan data terkait di lokasi yang dapat diakses oleh penelepon (karena objek lokal tidak dapat diakses oleh penelepon dan menghilang ketika metode selesai). Ketika tipe bawaan dikembalikan, operasi ini sangat cepat, tetapi jika objek besar dikembalikan, ini bisa memakan waktu lama. Konstruktor bergerak memungkinkan pemrogram untuk menimpa perilaku default ini dan sebagai gantinya "menggunakan kembali" data tumpukan yang terkait dengan objek lokal dengan mengarahkan objek yang dikembalikan ke pemanggil untuk menumpuk data yang terkait dengan objek lokal. Dengan demikian, tidak perlu menyalin.
Dalam bahasa yang tidak memungkinkan pembuatan objek lokal (yaitu, objek pada stack) jenis masalah ini tidak terjadi karena semua objek dialokasikan pada heap dan selalu diakses dengan referensi.
sumber
x
dany
, Anda tidak bisa hanya "bertukar referensi ke objek" ; mungkin saja objek berisi pointer yang mereferensikan data lain, dan pointer itu dapat ditukar, tetapi operator yang bergerak tidak diharuskan untuk menukar apa pun. Mereka dapat menghapus data dari objek yang dipindahkan-dari, daripada menyimpan data dest di dalamnya.swap()
tanpa memindahkan semantik. "Tugas pemindahan dapat dipanggil dengan memanggil metode std :: move ()." - kadang - kadang diperlukan untuk digunakanstd::move()
- meskipun itu tidak benar-benar memindahkan apa pun - biarkan kompiler tahu bahwa argumennya dapat dipindahkan, kadang-kadangstd::forward<>()
(dengan referensi penerusan), dan di lain waktu kompiler mengetahui nilai dapat dipindahkan.Berikut jawaban dari buku "Bahasa Pemrograman C ++" oleh Bjarne Stroustrup. Jika Anda tidak ingin melihat video, Anda dapat melihat teks di bawah ini:
Pertimbangkan cuplikan ini. Kembali dari operator + melibatkan menyalin hasil dari variabel lokal
res
dan ke suatu tempat di mana pemanggil dapat mengaksesnya.Kami tidak benar-benar menginginkan salinan; kami hanya ingin mendapatkan hasil dari suatu fungsi. Jadi kita perlu memindahkan Vektor daripada menyalinnya. Kita dapat mendefinisikan move constructor sebagai berikut:
&& berarti "referensi nilai" dan merupakan referensi di mana kita dapat mengikat nilai. "rvalue" 'dimaksudkan untuk melengkapi "lvalue" yang secara kasar berarti "sesuatu yang dapat muncul di sisi kiri suatu tugas." Jadi nilai berarti kira-kira "nilai yang Anda tidak dapat menetapkan", seperti integer yang dikembalikan oleh panggilan fungsi, dan
res
variabel lokal di operator + () untuk Vektor.Sekarang, pernyataan
return res;
itu tidak akan disalin!sumber