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. Telah dumb_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
:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
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 hanyalah memcpy
(tersembunyi di bawah std::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:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
Atau mungkin jika Anda ingin memanfaatkan tugas pindahan di C ++ 11 yang seharusnya:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
Jika dumb_array
klien menghargai kecepatan, mereka harus memanggil operator=
. 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):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
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:
- Sementara.
- Objek yang ingin Anda percayai oleh si penelepon bersifat sementara.
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:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
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
:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Dalam kasus penggunaan tipikal dari tugas bergerak, *this
akan menjadi objek yang dipindahkan dan delete [] 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 bahwa swap(x, x)
hanya self-move-assignemnet pada nilai yang dipindahkan. Dan dalam dumb_array
contoh kita ini tidak akan berbahaya sama sekali jika kita hanya menghilangkan assert, atau membatasinya ke kasus yang dipindahkan:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
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:
x = y;
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 nilai x
.
Untuk tugas pindah:
x = std::move(y);
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 mengizinkan swap(x, x)
untuk hanya bekerja:
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
Di atas berfungsi, selama x = std::move(x)
tidak macet. Itu dapat pergi x
dalam keadaan valid tetapi tidak ditentukan.
Saya melihat tiga cara untuk memprogram operator penugasan pindahan untuk dumb_array
mencapai ini:
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Implementasi di atas mentolerir penugasan mandiri, tetapi *this
dan other
akhirnya menjadi larik berukuran nol setelah penugasan gerak sendiri, tidak peduli berapa nilai aslinya *this
. Ini bagus.
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
Implementasi di atas mentolerir penugasan mandiri dengan cara yang sama seperti yang dilakukan operator penugasan salinan, dengan menjadikannya no-op. Ini juga bagus.
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
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. Jika dumb_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:
Class& operator=(Class&&) = default;
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 sana strong_assign
.