Коли я давно вивчив C ++, мені було наголошено, що частина пункту C ++ полягає в тому, що так само, як петлі мають "петлю-інваріанти", класи також мають інваріанти, пов'язані з життям об'єкта - речі, які повинні бути правдивими доки об’єкт живий. Речі, які повинні бути встановлені конструкторами, а збережені методами. Інкапсуляція / контроль доступу є для того, щоб допомогти вам застосувати інваріанти. RAII - це одне, що ви можете зробити з цією ідеєю.
Оскільки C ++ 11, тепер ми маємо семантику переміщення. Для класу, який підтримує переміщення, переміщення з об'єкта формально не закінчує його життєвий час - цей крок повинен залишити його в якомусь "дійсному" стані.
При проектуванні класу це погана практика, якщо ви проектуєте його так, щоб інваріанти класу зберігалися лише до того моменту, з якого він переміщений? Або це нормально, якщо це дозволить вам змусити його йти швидше.
Щоб зробити це конкретним, припустімо, у мене є тип копіюваного ресурсу, який не можна скопіювати, але так:
class opaque {
opaque(const opaque &) = delete;
public:
opaque(opaque &&);
...
void mysterious();
void mysterious(int);
void mysterious(std::vector<std::string>);
};
І з будь-якої причини мені потрібно зробити обгортальну копію для цього об’єкта, щоб вона могла бути використана, можливо, в якійсь існуючій системі відправки.
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); }
};
У цьому copyable_opaque
об'єкті інваріантом класу, встановленого при будівництві, є те, що член o_
завжди вказує на дійсний об'єкт, оскільки немає ctor за замовчуванням, і єдиний ctor, який не є копіюючим ctor, гарантує це. Усі operator()
методи припускають, що цей інваріант дотримується, і зберігають його згодом.
Однак, якщо об’єкт переміщено з, то він o_
буде вказувати ні на що. І після цього, виклик будь-якого з методів operator()
спричинить UB / збій.
Якщо об'єкт ніколи не переміщується, інваріант буде збережений аж до виклику dtor.
Припустимо, що я гіпотетично написав цей клас, і через кілька місяців мій уявний колега випробував UB, оскільки в якійсь складної функції, де багато цих предметів чомусь перетасовувались, він перейшов від однієї з цих речей і пізніше назвав одну з її методи. Зрозуміло, це його вина в кінці дня, але чи цей клас "погано розроблений?"
Думки:
Зазвичай у C ++ погана форма створювати зомбі-об’єкти, які вибухають, якщо торкнутися їх.
Якщо ви не можете побудувати якийсь об'єкт, не зможете встановити інваріанти, тоді киньте виняток із ctor. Якщо ви не можете зберегти інваріанти в якомусь методі, тоді якось сигналізуйте про помилку і відкатуйтесь. Чи повинно це відрізнятися для об'єктів, що переміщуються?Чи достатньо лише задокументувати "після переміщення цього об'єкта, заборонено (UB) робити з ним що-небудь, крім знищення" в заголовку?
Чи краще постійно твердити, що він дійсний у кожному виклику методу?
Так:
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); }
};
Твердження істотно не покращують поведінку, і вони викликають уповільнення. Якщо ваш проект використовує схему "build build / debug build", а не завжди працює з твердженнями, я думаю, що це привабливіше, оскільки ви не платите за чеки в складанні випусків. Якщо ви фактично не маєте налагоджень, це здається зовсім непривабливим.
- Чи краще зробити клас копіюваним, але не рухомим?
Це також здається поганим і спричиняє хіт продуктивності, але це вирішує "інваріантне" питання прямо.
Що б ви вважали відповідними "найкращими практиками" тут?