Di operator penugasan sebuah kelas, Anda biasanya perlu memeriksa apakah objek yang ditugaskan adalah objek pemanggilan sehingga Anda tidak mengacaukan semuanya:
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
Apakah Anda membutuhkan hal yang sama untuk operator penugasan pindahan? Apakah pernah ada situasi yang this == &rhs
benar?
? Class::operator=(Class&& rhs) {
?
}
c++
c++11
move-semantics
move-assignment-operator
Seth Carnegie
sumber
sumber
A a; a = std::move(a);
.std::move
normal. Kemudian pertimbangkan aliasing, dan ketika Anda berada jauh di dalam tumpukan panggilan dan Anda memiliki satu referensi keT
, dan satu sama lain referensi keT
... apakah Anda akan memeriksa identitas di sini? Apakah Anda ingin menemukan panggilan (atau panggilan) pertama di mana mendokumentasikan bahwa Anda tidak dapat menyampaikan argumen yang sama dua kali akan secara statis membuktikan bahwa kedua referensi tersebut tidak akan disebut alias? Atau apakah Anda akan membuat penugasan sendiri berfungsi?std::sort
ataustd::shuffle
- setiap kali Anda menukar elemeni
th danj
th dari sebuah array tanpa memeriksai != j
terlebih dahulu. (std::swap
diimplementasikan dalam hal tugas pindah.)Jawaban:
Wow, begitu banyak yang harus dibersihkan di sini ...
Pertama, Copy dan Swap tidak selalu merupakan cara yang benar untuk mengimplementasikan Copy Assignment. Hampir pasti dalam kasus
dumb_array
, ini adalah solusi yang kurang optimal.Penggunaan Copy dan Swap adalah untuk
dumb_array
contoh klasik menempatkan operasi paling mahal dengan fitur terlengkap di lapisan bawah. Ini sempurna untuk klien yang menginginkan fitur terlengkap dan bersedia membayar penalti kinerja. Mereka mendapatkan apa yang mereka inginkan.Namun, ini merupakan bencana bagi klien yang tidak membutuhkan fitur lengkap dan malah mencari kinerja tertinggi. Bagi mereka
dumb_array
hanyalah perangkat lunak lain yang harus mereka tulis ulang karena terlalu lambat. Telahdumb_array
dirancang secara berbeda, itu bisa memuaskan kedua klien tanpa kompromi ke salah satu klien.Kunci untuk memuaskan kedua klien adalah dengan membangun operasi tercepat di tingkat terendah, dan kemudian menambahkan API di atasnya untuk fitur yang lebih lengkap dengan biaya lebih banyak. Yaitu Anda membutuhkan jaminan pengecualian yang kuat, baiklah, Anda membayarnya. Anda tidak membutuhkannya? Inilah solusi yang lebih cepat.
Mari kita konkret: Berikut ini, jaminan pengecualian dasar yang cepat, operator Tugas Salin untuk
dumb_array
:Penjelasan:
Salah satu hal yang lebih mahal yang dapat Anda lakukan pada perangkat keras modern adalah melakukan perjalanan ke heap. Apa pun yang dapat Anda lakukan untuk menghindari perjalanan ke heap adalah menghabiskan waktu & tenaga dengan baik. Klien
dumb_array
mungkin ingin sering menetapkan array dengan ukuran yang sama. Dan ketika mereka melakukannya, yang perlu Anda lakukan hanyalahmemcpy
(tersembunyi di bawahstd::copy
). Anda tidak ingin mengalokasikan array baru dengan ukuran yang sama dan kemudian membatalkan alokasi yang lama dengan ukuran yang sama!Sekarang untuk klien Anda yang benar-benar menginginkan keamanan pengecualian yang kuat:
Atau mungkin jika Anda ingin memanfaatkan tugas pindahan di C ++ 11 yang seharusnya:
Jika
dumb_array
klien menghargai kecepatan, mereka harus memanggiloperator=
. Jika mereka membutuhkan keamanan pengecualian yang kuat, ada algoritme umum yang dapat mereka panggil yang akan bekerja pada berbagai macam objek dan hanya perlu diterapkan sekali.Sekarang kembali ke pertanyaan awal (yang memiliki tipe-o pada saat ini):
Ini sebenarnya pertanyaan kontroversial. Beberapa akan mengatakan ya, tentu saja, beberapa akan mengatakan tidak.
Pendapat pribadi saya tidak, Anda tidak perlu cek ini.
Alasan:
Ketika sebuah objek terikat ke referensi nilai r, itu adalah salah satu dari dua hal:
Jika Anda memiliki referensi ke objek yang bersifat sementara, maka menurut definisi, Anda memiliki referensi unik ke objek tersebut. Itu tidak mungkin direferensikan oleh tempat lain di seluruh program Anda. Yaitu
this == &temporary
tidak mungkin .Sekarang jika klien Anda telah berbohong kepada Anda dan berjanji kepada Anda bahwa Anda mendapatkan sementara sementara Anda tidak, maka klien bertanggung jawab untuk memastikan bahwa Anda tidak perlu peduli. Jika Anda ingin benar-benar berhati-hati, saya yakin ini akan menjadi implementasi yang lebih baik:
Yaitu Jika Anda sedang melewati referensi diri, ini adalah bug pada bagian dari klien yang harus diperbaiki.
Untuk kelengkapannya, berikut adalah operator penugasan pindah untuk
dumb_array
:Dalam kasus penggunaan tipikal dari tugas bergerak,
*this
akan menjadi objek yang dipindahkan dandelete [] mArray;
seharusnya tidak ada operasi. Sangat penting bahwa implementasi menghapus pada nullptr secepat mungkin.Peringatan:
Beberapa orang akan berpendapat bahwa itu
swap(x, x)
adalah ide yang bagus, atau hanya kejahatan yang perlu. Dan ini, jika swap beralih ke swap default, dapat menyebabkan penugasan pindah sendiri.Saya tidak setuju bahwa
swap(x, x)
adalah pernah ide yang baik. Jika ditemukan di kode saya sendiri, saya akan menganggapnya sebagai bug kinerja dan memperbaikinya. Tetapi jika Anda ingin mengizinkannya, sadari bahwaswap(x, x)
hanya self-move-assignemnet pada nilai yang dipindahkan. Dan dalamdumb_array
contoh kita ini tidak akan berbahaya sama sekali jika kita hanya menghilangkan assert, atau membatasinya ke kasus yang dipindahkan:Jika Anda menetapkan sendiri dua dipindahkan dari (kosong)
dumb_array
, Anda tidak melakukan sesuatu yang salah selain memasukkan instruksi yang tidak berguna ke dalam program Anda. Pengamatan yang sama ini dapat dilakukan untuk sebagian besar objek.<
Memperbarui>
Saya telah memikirkan masalah ini lagi, dan agak mengubah posisi saya. Sekarang saya percaya bahwa penugasan harus toleran terhadap penugasan mandiri, tetapi kondisi pengeposan pada penugasan salinan dan pemindahan tugas berbeda:
Untuk tugas menyalin:
seseorang harus memiliki kondisi pasca di mana nilai
y
tidak boleh diubah. Bila&x == &y
kemudian kondisi pos ini diterjemahkan menjadi: tugas salin sendiri seharusnya tidak berdampak pada nilaix
.Untuk tugas pindah:
seseorang harus memiliki kondisi pasca yang
y
memiliki status valid tetapi tidak ditentukan. Bila&x == &y
kemudian kondisi pos ini diterjemahkan menjadi:x
memiliki status yang valid tetapi tidak ditentukan. Yaitu tugas pindah sendiri tidak harus tanpa operasi. Tapi seharusnya tidak crash. Kondisi pasca ini konsisten dengan mengizinkanswap(x, x)
untuk hanya bekerja:Di atas berfungsi, selama
x = std::move(x)
tidak macet. Itu dapat pergix
dalam keadaan valid tetapi tidak ditentukan.Saya melihat tiga cara untuk memprogram operator penugasan pindahan untuk
dumb_array
mencapai ini:Implementasi di atas mentolerir penugasan mandiri, tetapi
*this
danother
akhirnya menjadi larik berukuran nol setelah penugasan gerak sendiri, tidak peduli berapa nilai aslinya*this
. Ini bagus.Implementasi di atas mentolerir penugasan mandiri dengan cara yang sama seperti yang dilakukan operator penugasan salinan, dengan menjadikannya no-op. Ini juga bagus.
Hal di atas tidak masalah hanya jika
dumb_array
tidak menyimpan sumber daya yang harus dihancurkan "segera". Misalnya jika satu-satunya sumber daya adalah memori, hal di atas sudah cukup. Jikadumb_array
mungkin dapat menahan kunci mutex atau status buka file, klien dapat mengharapkan sumber daya tersebut di lh tugas pemindahan segera dirilis dan oleh karena itu implementasi ini dapat menjadi masalah.Biaya yang pertama adalah dua toko tambahan. Biaya yang kedua adalah tes-dan-cabang. Keduanya bekerja. Keduanya memenuhi semua persyaratan Tabel 22 persyaratan MoveAssignable dalam standar C ++ 11. Yang ketiga juga bekerja modulo non-memory-resource-concern.
Ketiga implementasi dapat memiliki biaya yang berbeda tergantung pada perangkat kerasnya: Seberapa mahalkah sebuah cabang? Apakah ada banyak register atau sangat sedikit?
Pengambilannya adalah bahwa tugas-pindah-sendiri, tidak seperti tugas-menyalin-sendiri, tidak harus mempertahankan nilai saat ini.
<
/Memperbarui>
Satu suntingan terakhir (semoga) terinspirasi oleh komentar Luc Danton:
Jika Anda menulis kelas tingkat tinggi yang tidak secara langsung mengelola memori (tetapi mungkin memiliki basis atau anggota yang melakukannya), implementasi terbaik dari tugas pindah sering kali:
Ini akan memindahkan menetapkan setiap basis dan setiap anggota secara bergiliran, dan tidak akan termasuk
this != &other
cek. Ini akan memberi Anda kinerja tertinggi dan keamanan pengecualian dasar dengan asumsi tidak ada invarian yang perlu dipertahankan di antara basis dan anggota Anda. Untuk klien Anda yang menuntut keamanan pengecualian yang kuat, arahkan mereka ke sanastrong_assign
.sumber
std::swap(x,x)
, lalu mengapa saya harus memercayainya untuk menangani operasi yang lebih rumit dengan benar?std::swap(x,x)
, itu hanya berfungsi bahkan ketikax = std::move(x)
menghasilkan hasil yang tidak ditentukan. Cobalah! Anda tidak harus mempercayai saya.swap
berfungsi selamax = move(x)
daunx
dalam kondisi dapat dipindahkan. Danstd::copy
/std::move
algoritme telah ditentukan untuk menghasilkan perilaku yang tidak terdefinisi pada salinan tanpa operasi (aduh; pemain berusia 20 tahun inimemmove
mendapatkan kasus yang sepele tapistd::move
tidak!). Jadi saya kira saya belum memikirkan "slam dunk" untuk penugasan diri. Tapi jelas penugasan diri adalah sesuatu yang sering terjadi dalam kode nyata, apakah Standar telah memberkatinya atau tidak.Pertama, Anda salah menerima tanda tangan operator penugasan pindah. Karena gerakan mencuri sumber daya dari objek sumber, sumber harus berupa
const
referensi non -nilai-r.Perhatikan bahwa Anda masih kembali melalui referensi nilai- l (non-
const
) .Untuk kedua jenis penugasan langsung, standarnya bukan untuk memeriksa penugasan mandiri, tetapi untuk memastikan penugasan mandiri tidak menyebabkan crash-and-burn. Umumnya, tidak ada yang secara eksplisit melakukan
x = x
atauy = std::move(y)
memanggil, tetapi aliasing, terutama melalui beberapa fungsi, dapat menyebabkana = b
atauc = std::move(d)
menjadi penugasan mandiri. Pemeriksaan eksplisit untuk penugasan mandiri, yaituthis == &rhs
, yang melewatkan inti fungsi saat benar adalah salah satu cara untuk memastikan keamanan penugasan mandiri. Tapi itu salah satu cara terburuk, karena ini mengoptimalkan kasus langka (semoga) sementara itu anti-pengoptimalan untuk kasus yang lebih umum (karena percabangan dan mungkin cache hilang).Sekarang ketika (setidaknya) salah satu operand adalah objek sementara secara langsung, Anda tidak akan pernah bisa memiliki skenario penugasan mandiri. Beberapa orang menganjurkan asumsi kasus itu dan mengoptimalkan kodenya sedemikian rupa sehingga kode itu menjadi bodoh untuk bunuh diri ketika anggapannya salah. Saya mengatakan bahwa membuang pemeriksaan objek yang sama pada pengguna tidak bertanggung jawab. Kami tidak membuat argumen itu untuk penugasan salinan; mengapa membalik posisi untuk pindah-tugas?
Mari kita buat contoh, diubah dari responden lain:
Penugasan salinan ini menangani penugasan mandiri dengan baik tanpa pemeriksaan eksplisit. Jika ukuran sumber dan tujuan berbeda, maka deallocation dan realokasi mendahului penyalinan. Jika tidak, penyalinan sudah selesai. Penetapan sendiri tidak mendapatkan jalur yang dioptimalkan, itu dibuang ke jalur yang sama seperti saat ukuran sumber dan tujuan dimulai dengan ukuran yang sama. Penyalinan secara teknis tidak diperlukan ketika dua objek itu setara (termasuk ketika mereka adalah objek yang sama), tetapi itulah harga ketika tidak melakukan pemeriksaan kesetaraan (dari segi nilai atau alamat) karena pemeriksaan tersebut sendiri akan sangat sia-sia. waktu. Perhatikan bahwa penugasan sendiri objek di sini akan menyebabkan serangkaian penugasan mandiri tingkat elemen; jenis elemen harus aman untuk melakukan ini.
Seperti contoh sumbernya, penugasan salinan ini memberikan jaminan keamanan pengecualian dasar. Jika Anda menginginkan jaminan yang kuat, gunakan operator tugas terpadu dari kueri Salin dan Tukar asli , yang menangani tugas penyalinan dan pemindahan. Tapi inti dari contoh ini adalah mengurangi keselamatan satu peringkat untuk mendapatkan kecepatan. (BTW, kami mengasumsikan bahwa nilai elemen individu adalah independen; bahwa tidak ada batasan invarian yang membatasi beberapa nilai dibandingkan dengan yang lain.)
Mari kita lihat tugas-pindah untuk tipe yang sama ini:
Jenis yang dapat ditukar yang memerlukan penyesuaian harus memiliki fungsi bebas dua argumen yang dipanggil
swap
dalam namespace yang sama dengan jenisnya. (Pembatasan namespace memungkinkan panggilan yang tidak memenuhi syarat untuk ditukar bekerja.) Jenis penampung juga harus menambahkanswap
fungsi anggota publik agar sesuai dengan penampung standar. Jika anggotaswap
tidak tersedia, maka fungsi bebasswap
mungkin perlu ditandai sebagai teman tipe yang dapat ditukar. Jika Anda menyesuaikan gerakan yang akan digunakanswap
, maka Anda harus memberikan kode pertukaran Anda sendiri; kode standar memanggil kode pemindahan tipe, yang akan menghasilkan rekursi timbal balik tak terbatas untuk tipe yang disesuaikan pemindahan.Seperti destruktor, fungsi swap dan operasi pemindahan harus tidak pernah dibuang jika memungkinkan, dan mungkin ditandai seperti itu (dalam C ++ 11). Jenis dan rutinitas library standar memiliki pengoptimalan untuk jenis pemindahan yang tidak dapat dilempar.
Versi pertama dari tugas pindah ini memenuhi kontrak dasar. Penanda sumber daya sumber ditransfer ke objek tujuan. Sumber daya lama tidak akan bocor karena objek sumber sekarang mengelolanya. Dan objek sumber dibiarkan dalam keadaan dapat digunakan di mana operasi lebih lanjut, termasuk penugasan dan penghancuran, dapat diterapkan padanya.
Perhatikan bahwa pemindahan hak ini secara otomatis aman untuk pemindahan hak, karena
swap
panggilan itu. Ini juga sangat aman untuk pengecualian. Masalahnya adalah retensi sumber daya yang tidak perlu. Sumber daya lama untuk tujuan secara konseptual tidak lagi diperlukan, tetapi di sini mereka masih ada sehingga objek sumber tetap valid. Jika penghancuran objek sumber yang dijadwalkan berlangsung lama, kita membuang-buang ruang sumber daya, atau lebih buruk lagi jika total ruang sumber daya terbatas dan petisi sumber daya lainnya akan terjadi sebelum objek sumber (baru) resmi mati.Masalah inilah yang menyebabkan nasehat guru saat ini yang kontroversial tentang penargetan diri selama penugasan pindah. Cara menulis tugas-pindah tanpa sisa sumber daya adalah seperti:
Sumber disetel ulang ke kondisi default, sementara sumber daya tujuan lama dimusnahkan. Dalam kasus penugasan diri, objek Anda saat ini berakhir dengan bunuh diri. Cara utama untuk mengatasinya adalah mengelilingi kode tindakan dengan sebuah
if(this != &other)
blok, atau mengencangkannya dan membiarkan klien memakanassert(this != &other)
baris awal (jika Anda merasa baik).Alternatifnya adalah mempelajari cara membuat tugas menyalin sangat aman untuk pengecualian, tanpa tugas terpadu, dan menerapkannya ke tugas pindah:
Ketika
other
danthis
berbeda,other
dikosongkan oleh perpindahan ketemp
dan tetap seperti itu. Kemudianthis
kehilangan sumber daya lama untuktemp
sementara mendapatkan sumber daya yang semula dipegangother
. Kemudian sumber daya lamathis
terbunuh ketikatemp
melakukannya.Ketika penugasan diri terjadi, pengosongan
other
menjaditemp
kosongthis
juga. Kemudian objek target mendapatkan sumber dayanya kembali kapantemp
danthis
bertukar. Kematiantemp
mengklaim benda kosong, yang seharusnya tidak ada operasi. Ituthis
/other
object terus sumber daya.Pindah-tugas tidak boleh melempar selama konstruksi-pindah dan pertukaran juga. Biaya juga untuk menjadi aman selama penugasan mandiri adalah beberapa instruksi lagi di atas tipe tingkat rendah, yang harus dibanjiri oleh panggilan deallocation.
sumber
delete
blok kode kedua Anda?std::copy
menyebabkan perilaku tidak terdefinisi jika rentang sumber dan tujuan tumpang tindih (termasuk kasus saat keduanya bertepatan). Lihat C ++ 14 [alg.copy] / 3.Saya termasuk dalam kelompok orang yang menginginkan operator aman penugasan mandiri, tetapi tidak ingin menulis cek penugasan mandiri dalam penerapannya
operator=
. Dan sebenarnya saya bahkan tidak ingin menerapkannyaoperator=
sama sekali, saya ingin perilaku default berfungsi 'langsung di luar kotak'. Anggota spesial terbaik adalah mereka yang datang secara gratis.Dengan demikian, persyaratan MoveAssignable yang ada dalam Standar dijelaskan sebagai berikut (dari 17.6.3.1 Persyaratan argumen template [utility.arg.requirements], n3290):
di mana placeholder dijelaskan sebagai: "
t
[adalah] nilai l yang dapat dimodifikasi dari tipe T;" dan "rv
adalah nilai r tipe T;". Perhatikan bahwa itu adalah persyaratan yang diletakkan pada tipe yang digunakan sebagai argumen untuk templat pustaka Standar, tetapi mencari di tempat lain di Standar Saya perhatikan bahwa setiap persyaratan pada tugas pemindahan serupa dengan yang ini.Ini berarti
a = std::move(a)
harus 'aman'. Jika yang Anda butuhkan adalah tes identitas (misalnyathis != &other
), maka lakukanlah, atau Anda bahkan tidak dapat memasukkan objek Andastd::vector
! (Kecuali jika Anda tidak menggunakan anggota / operasi yang memerlukan MoveAssignable; tapi lupakan itu.) Perhatikan bahwa dengan contoh sebelumnyaa = std::move(a)
, makathis == &other
akan benar-benar tahan.sumber
a = std::move(a)
tidak bekerja akan membuat kelas tidak berfungsistd::vector
? Contoh?std::vector<T>::erase
tidak diperbolehkan kecualiT
MoveAssignable. (Sebagai tambahan IIRC, beberapa persyaratan MoveAssignable dilonggarkan ke MoveInsertable sebagai gantinya di C ++ 14.)T
harus MoveAssignable, tapi mengapa haruserase()
bergantung pada pemindahan elemen ke dirinya sendiri ?Saat
operator=
fungsi Anda saat ini ditulis, karena Anda telah membuat argumen rvalue-referenceconst
, tidak mungkin Anda dapat "mencuri" pointer dan mengubah nilai referensi rvalue yang masuk ... Anda tidak bisa mengubahnya, Anda hanya bisa membaca darinya. Saya hanya akan melihat masalah jika Anda mulai memanggildelete
pointer, dll. Dithis
objek Anda seperti yang Anda lakukan dalamoperator=
metode referensi-lvaue normal , tetapi semacam itu mengalahkan poin dari rvalue-version ... yaitu, itu akan Tampaknya berlebihan untuk menggunakan versi rvalue untuk pada dasarnya melakukan operasi yang sama biasanya dengan metodeconst
-lvalueoperator=
.Sekarang jika Anda mendefinisikan Anda
operator=
untuk mengambilconst
referensi non -rvalue, maka satu-satunya cara saya bisa melihat pemeriksaan yang diperlukan adalah jika Anda meneruskanthis
objek ke fungsi yang dengan sengaja mengembalikan referensi nilai r daripada sementara.Sebagai contoh, misalkan seseorang mencoba untuk menulis sebuah
operator+
fungsi, dan menggunakan campuran referensi nilai r dan referensi nilai untuk "mencegah" sementara tambahan dibuat selama beberapa operasi penjumlahan bertumpuk pada tipe objek:Sekarang, dari apa yang saya pahami tentang referensi rvalue, melakukan hal di atas tidak disarankan (yaitu, Anda harus mengembalikan referensi sementara, bukan rvalue), tetapi, jika seseorang masih melakukan itu, maka Anda ingin memeriksa untuk membuatnya yakin rvalue-reference yang masuk tidak mereferensikan objek yang sama dengan
this
pointer.sumber
const
, maka Anda hanya dapat membacanya, jadi hanya perlu buatlah pemeriksaan jika Anda memutuskanoperator=(const T&&)
untuk melakukan inisialisasi ulang yang sama denganthis
yang akan Anda lakukan dalamoperator=(const T&)
metode tipikal daripada operasi gaya bertukar (misalnya, mencuri pointer, dll daripada membuat salinan dalam).Jawaban saya tetap bahwa tugas pindah tidak harus menyelamatkan diri sendiri, tetapi memiliki penjelasan yang berbeda. Pertimbangkan std :: unique_ptr. Jika saya menerapkannya, saya akan melakukan sesuatu seperti ini:
Jika Anda melihat Scott Meyers menjelaskan hal ini, dia melakukan hal serupa. (Jika Anda mengembara mengapa tidak melakukan swap - ini memiliki satu tulisan tambahan). Dan ini tidak aman untuk penugasan mandiri.
Terkadang hal ini sangat disayangkan. Pertimbangkan untuk keluar dari vektor semua bilangan genap:
Ini tidak masalah untuk bilangan bulat tetapi saya tidak yakin Anda dapat membuat sesuatu seperti ini bekerja dengan semantik bergerak.
Kesimpulannya: memindahkan tugas ke objek itu sendiri tidak baik dan Anda harus hati-hati.
Pembaruan kecil.
swap(x, x)
harus bekerja. Algoritma menyukai hal-hal ini! Selalu menyenangkan ketika kasing sudut berfungsi. (Dan saya belum melihat kasus di mana itu tidak gratis. Tidak berarti itu tidak ada).unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
Aman untuk tugas pemindahan sendiri.sumber
Ada situasi yang (ini == rhs) bisa saya pikirkan. Untuk pernyataan ini: Myclass obj; std :: move (obj) = std :: move (obj)
sumber