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?