Ketika saya mempelajari C ++ sejak lama, sangat ditekankan kepada saya bahwa bagian dari titik C ++ adalah seperti halnya loop memiliki "loop-invariants", kelas juga memiliki invarian yang terkait dengan masa hidup objek - hal-hal yang seharusnya benar selama benda itu hidup. Hal-hal yang harus ditetapkan oleh konstruktor, dan dilestarikan dengan metode. Enkapsulasi / kontrol akses ada untuk membantu Anda menegakkan invarian. RAII adalah satu hal yang dapat Anda lakukan dengan ide ini.
Sejak C ++ 11 kita sekarang telah memindahkan semantik. Untuk kelas yang mendukung gerakan, bergerak dari suatu objek tidak secara resmi mengakhiri masa pakainya - gerakan tersebut seharusnya membiarkannya dalam keadaan "valid".
Dalam mendesain sebuah kelas, apakah itu praktik yang buruk jika Anda mendesainnya sehingga invarian dari kelas hanya dipertahankan sampai pada titik di mana ia dipindahkan? Atau tidak apa - apa jika itu akan memungkinkan Anda untuk membuatnya lebih cepat.
Untuk membuatnya konkret, anggaplah saya memiliki tipe sumber daya yang tidak dapat disalin tetapi dapat dipindahkan seperti:
class opaque {
opaque(const opaque &) = delete;
public:
opaque(opaque &&);
...
void mysterious();
void mysterious(int);
void mysterious(std::vector<std::string>);
};
Dan untuk alasan apa pun, saya perlu membuat pembungkus yang dapat disalin untuk objek ini, sehingga dapat digunakan, mungkin dalam beberapa sistem pengiriman yang ada.
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { o_->mysterious(); }
void operator()(int i) { o_->mysterious(i); }
void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};
Dalam copyable_opaque
objek ini , invarian dari kelas yang didirikan pada konstruksi adalah bahwa anggota o_
selalu menunjuk ke objek yang valid, karena tidak ada ctor default, dan satu-satunya ctor yang bukan copy ctor menjamin ini. Semua operator()
metode mengasumsikan bahwa invarian ini berlaku, dan melestarikannya setelahnya.
Namun, jika objek tersebut dipindahkan, maka o_
akan menunjuk ke apa-apa. Dan setelah titik itu, memanggil salah satu metode operator()
akan menyebabkan UB / crash.
Jika objek tidak pernah dipindahkan, maka invarian akan disimpan hingga panggilan dtor.
Anggap saja secara hipotetis, saya menulis kelas ini, dan berbulan-bulan kemudian, rekan kerja imajiner saya mengalami UB karena, dalam beberapa fungsi rumit di mana banyak objek ini sedang diaduk-aduk karena suatu alasan, ia pindah dari salah satu hal ini dan kemudian memanggil salah satu dari metodenya. Jelas itu salahnya pada akhirnya, tetapi apakah kelas ini "dirancang dengan buruk?"
Pikiran:
Biasanya bentuk yang buruk di C ++ untuk membuat objek zombie yang meledak jika Anda menyentuhnya.
Jika Anda tidak dapat membuat beberapa objek, tidak dapat membuat invarian, lalu lempar pengecualian dari ctor. Jika Anda tidak dapat mempertahankan invarian dalam beberapa metode, berikan sinyal kesalahan dan gulung balik. Haruskah ini berbeda untuk objek yang dipindahkan dari objek?Apakah cukup dengan hanya mendokumentasikan "setelah objek ini dipindahkan, itu ilegal (UB) untuk melakukan apa pun selain menghancurkannya" di header?
Apakah lebih baik untuk terus-menerus menyatakan bahwa itu valid dalam setiap pemanggilan metode?
Seperti itu:
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { assert(o_); o_->mysterious(); }
void operator()(int i) { assert(o_); o_->mysterious(i); }
void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};
Pernyataan tidak secara substansial meningkatkan perilaku, dan mereka menyebabkan pelambatan. Jika proyek Anda memang menggunakan skema "rilis build / debug build", daripada hanya selalu berjalan dengan pernyataan, saya kira ini lebih menarik, karena Anda tidak membayar untuk cek di build rilis. Jika Anda tidak benar-benar memiliki debug build, ini tampaknya cukup tidak menarik.
- Apakah lebih baik membuat kelas dapat disalin, tetapi tidak dapat dipindahkan?
Ini juga tampaknya buruk dan menyebabkan kinerja yang buruk, tetapi ini memecahkan masalah "invarian" secara langsung.
Apa yang Anda anggap sebagai "praktik terbaik" yang relevan di sini?
sumber
Jawaban:
Tapi bukan itu yang kamu lakukan. Anda sedang membuat "objek zombie" yang akan meledak jika Anda menyentuhnya salah . Yang pada akhirnya tidak berbeda dengan prasyarat berbasis negara lainnya.
Pertimbangkan fungsi berikut:
Apakah fungsi ini aman? Tidak; pengguna dapat melewatkan yang kosong
vector
. Jadi fungsinya memiliki prasyarat de-facto yangv
memiliki setidaknya satu elemen di dalamnya. Jika tidak, maka Anda mendapatkan UB saat Anda meneleponfunc
.Jadi fungsi ini bukan "aman". Tapi itu tidak berarti itu rusak. Itu hanya rusak jika kode yang menggunakannya melanggar prasyarat. Mungkin
func
fungsi statis digunakan sebagai penolong dalam implementasi fungsi lainnya. Dilokalkan sedemikian rupa, tidak ada yang akan menyebutnya dengan cara yang melanggar prasyaratnya.Banyak fungsi, apakah namespace-scoped atau anggota kelas, akan memiliki harapan pada keadaan nilai yang mereka operasikan. Jika prasyarat ini tidak terpenuhi, maka fungsi akan gagal, biasanya dengan UB.
Pustaka standar C ++ mendefinisikan aturan "valid-but-unspecified". Ini mengatakan bahwa, kecuali standar mengatakan sebaliknya, setiap objek yang dipindahkan akan valid (itu adalah objek hukum dari jenis itu), tetapi keadaan spesifik objek itu tidak ditentukan. Berapa banyak elemen yang
vector
dimiliki dari pindah ? Tidak dikatakan.Ini berarti bahwa Anda tidak dapat memanggil fungsi apa pun yang memiliki prasyarat apa pun .
vector::operator[]
memiliki prasyarat bahwavector
setidaknya memiliki satu elemen. Karena Anda tidak tahu keadaannyavector
, Anda tidak bisa menyebutnya. Akan lebih baik daripada meneleponfunc
tanpa terlebih dahulu memverifikasi bahwavector
itu tidak kosong.Tetapi ini juga berarti bahwa fungsi yang tidak memiliki prasyarat baik-baik saja. Ini adalah kode C ++ 11 yang legal:
vector::assign
tidak memiliki prasyarat. Ini akan bekerja denganvector
objek yang valid , bahkan yang telah dipindahkan.Jadi, Anda tidak membuat objek yang rusak. Anda sedang membuat objek yang keadaannya tidak diketahui.
Melempar pengecualian dari konstruktor bergerak umumnya dianggap ... kasar. Jika Anda memindahkan objek yang memiliki memori, maka Anda mentransfer kepemilikan memori itu. Dan itu biasanya tidak melibatkan apa pun yang bisa melempar.
Sayangnya, kami tidak dapat menegakkan ini karena berbagai alasan . Kita harus menerima bahwa melempar-pindah adalah suatu kemungkinan.
Perlu juga dicatat bahwa Anda tidak harus mengikuti bahasa "valid-belum-ditentukan". Begitulah cara perpustakaan standar C ++ mengatakan bahwa gerakan untuk tipe standar bekerja secara default . Jenis perpustakaan standar tertentu memiliki jaminan yang lebih ketat. Sebagai contoh,
unique_ptr
sangat jelas tentang keadaanunique_ptr
instance move-from : itu sama dengannullptr
.Jadi, Anda dapat memilih untuk memberikan jaminan yang lebih kuat jika Anda mau.
Hanya ingat: gerakan adalah optimasi kinerja , salah satu yang biasanya dilakukan pada objek yang sekitar harus dihancurkan. Pertimbangkan kode ini:
Ini akan berpindah dari
v
nilai pengembalian (dengan anggapan kompiler tidak mengelaknya). Dan tidak ada cara untuk referensiv
setelah langkah selesai. Jadi pekerjaan apa pun yang Anda lakukan untuk menjadikannyav
berguna tidak ada artinya.Di sebagian besar kode, kemungkinan menggunakan instance objek yang dipindahkan dari rendah.
Inti dari memiliki prasyarat adalah untuk tidak memeriksa hal-hal seperti itu.
operator[]
memiliki prasyarat bahwavector
elemen memiliki indeks yang diberikan. Anda mendapatkan UB jika Anda mencoba mengakses di luar ukuranvector
.vector::at
tidak memiliki prasyarat seperti itu; itu secara eksplisit melempar pengecualian jikavector
tidak memiliki nilai seperti itu.Ada prasyarat untuk alasan kinerja. Itu agar Anda tidak perlu memeriksa hal-hal yang bisa diverifikasi oleh penelepon itu sendiri. Setiap panggilan ke
v[0]
tidak harus memeriksa apakahv
kosong; hanya yang pertama yang melakukannya.Sebenarnya, suatu kelas tidak boleh "dapat disalin tetapi tidak dapat dipindahkan". Jika itu bisa disalin, maka itu harus bisa dipindahkan dengan memanggil copy constructor. Ini adalah perilaku standar C ++ 11 jika Anda mendeklarasikan copy constructor yang ditentukan pengguna tetapi tidak menyatakan move constructor. Dan itu perilaku yang harus Anda adopsi jika Anda tidak ingin menerapkan semantik gerakan khusus.
Pindahkan semantik yang ada untuk memecahkan masalah yang sangat spesifik: berurusan dengan objek yang memiliki sumber daya besar di mana penyalinan akan menjadi sangat mahal atau tidak berarti (mis .: menangani file). Jika objek Anda tidak memenuhi syarat, maka menyalin dan memindahkan adalah sama untuk Anda.
sumber