Як обробляти випадки відмов у конструкторі класу C ++?


21

У мене є клас CPP, конструктор якого робить деякі операції. Деякі з цих операцій можуть бути невдалими. Я знаю, що конструктори нічого не повертають.

Мої запитання:

  1. Чи дозволено робити інші операції, які ініціалізують членів у конструкторі?

  2. Чи можна сказати викличній функції про те, що деякі операції в конструкторі не виконані?

  3. Чи можу я new ClassName()повернути NULL, якщо в конструкторі виникають деякі помилки?


22
Ви можете викинути виняток із конструктора. Це абсолютно дійсна модель.
Енді

1
Напевно, вам слід ознайомитись із деякими креативними моделями GoF . Я рекомендую заводський зразок.
SpaceTrucker

2
Поширений приклад №1 - перевірка даних. IE, якщо у вас є клас Square, з конструктором, який приймає один параметр, довжина сторони, ви хочете перевірити, чи не перевищує це значення 0.
Девід говорить: Поновити Моніку

1
На перше запитання дозвольте попередити, що віртуальні функції можуть поводитися неінтуїтивно в конструкторах. Те саме з деконструкторами. Остерігайтеся називати таких.

1
# 3 - Чому ви хочете повернути NULL? Однією з переваг ОО є НЕ необхідність перевіряти значення повернення. Просто вловіть () відповідні потенційні винятки.
MrWonderful

Відповіді:


42
  1. Так, хоча деякі стандарти кодування можуть заборонити це.

  2. Так. Рекомендований спосіб - кинути виняток. Крім того, ви можете зберігати інформацію про помилки всередині об’єкта та надавати методи доступу до цієї інформації.

  3. Ні.


4
Якщо об'єкт все ще знаходиться у дійсному стані, навіть якщо частина аргументів конструктора не відповідала вимогам і, таким чином, позначається як помилка, 2) робити це дійсно не рекомендується. Краще, коли об’єкт або існує у дійсному стані, або взагалі не існує.
Енді

@DavidPacker Погодився, дивіться тут: stackoverflow.com/questions/77639/… Але деякі вказівки щодо кодування забороняють винятки, що проблематично для конструкторів.
Себастьян Редл

Я якось уже дав вам нагороду за цю відповідь, Себастьян. Цікаво. : D
Енді

10
@ooxi Ні, це не так. Ваш переосмислений новий закликається виділяти пам'ять, але виклик конструктору здійснює компілятор після повернення оператора, що означає, що ви не зможете зрозуміти помилку. Це припущення, що нове називається взагалі; це не для об'єктів, виділених стеком, яких має бути більшість.
Себастьян Редл

1
Для №1 є поширеним прикладом RAII, коли може знадобитися більше в конструкторі.
Ерік

20

Ви можете створити статичний метод, який виконує обчислення та повертає або об’єкт у разі успіху, або у випадку відмови.

Залежно від того, як виконується така побудова об'єкта, може бути краще створити інший об’єкт, який дозволяє будувати об'єкти нестатичним методом.

Виклик конструктора опосередковано часто називають "фабрикою".

Це також дозволить повернути null-об'єкт, що може бути кращим рішенням, ніж повернення null.


Дякую @null! На жаль, тут я не можу прийняти дві відповіді :( Інакше я б також прийняв цю відповідь !! Ще раз дякую!
MayurK

Чи не @MayurK ніяких турбот, то загальноприйнятий відповідь не відзначити на правильну відповідь, але той , який працює для вас.
null

3
@null: У програмі C ++ ви не можете просто повернутися NULL. Наприклад, у int foo() { return NULLвас фактично повернеться 0(нуль) цілий об'єкт. У std::string foo() { return NULL; }ви випадково називають std::string::string((const char*)NULL)що Невизначене поведінку (NULL не вказує на \ 0 з завершальним рядком).
MSalters

3
std :: необов'язково може бути далеко, але ви завжди можете використовувати boost :: необов'язково, якщо хочете піти цим шляхом.
Шон Бертон

1
@Vld: У C ++ об'єкти не обмежуються типами класів. І з загальним програмуванням не рідкість опинятися на заводах для int. Напр., std::allocator<int>Це ідеально розумна фабрика.
MSalters

5

@SebastianRedl вже дав прості, прямі відповіді, але деякі додаткові пояснення можуть бути корисні.

TL; DR = є правило стилю для спрощення конструкторів простим, для цього є причини, але ці причини в основному стосуються історичного (або просто поганого) стилю кодування. Поводження з винятками в конструкторах чітко визначено, і деструктори все ще будуть викликатися повністю сконструйованими локальними змінними та членами, а це означає, що в ідіоматичному коді C ++ не повинно виникнути жодних проблем. Правило стилю так чи інакше зберігається, але зазвичай це не проблема - не вся ініціалізація повинна бути в конструкторі, особливо не обов'язково в цьому конструкторі.


Це загальне правило стилю, що конструктори повинні робити абсолютний мінімум, який вони можуть, щоб створити визначений дійсний стан. Якщо ваша ініціалізація є більш складною, її слід обробляти поза конструктором. Якщо значення, яке може створити ваш конструктор, не має дешевого для ініціалізації, вам слід послабити інваранти, що застосовуються вашим класом, щоб додати його. Наприклад, якщо виділення пам’яті для вашого класу для управління занадто дороге, додайте ще не виділене ще нульове стан, оскільки, звичайно, такі особливі регістри, як null, ніколи нікому не викликали жодних проблем. Ахм.

Хоча це звичайно, але, безумовно, в цій крайній формі він дуже далеко не абсолютний. Зокрема, як свідчить мій сарказм, я перебуваю в таборі, який каже, що ослаблення інваріантів майже завжди є надто високою ціною. Тим не менш, є причини, що стоять за правилом стилю, і є способи мати як мінімальні конструктори, так і сильні інваріанти.

Причини пов'язані з автоматичним очищенням деструктора, особливо перед винятками. В основному, має бути чітко визначений момент, коли компілятор несе відповідальність за виклик деструкторів. Поки ви все ще перебуваєте у виклику конструктора, об'єкт не обов'язково повністю побудований, тому невірно викликати деструктор для цього об’єкта. Тому відповідальність за знищення об'єкта перекладається на компілятор лише тоді, коли конструктор успішно завершить. Це відоме як RAII (виділення ресурсів - ініціалізація), що насправді не найкраще ім'я.

Якщо викид винятку відбувається всередині конструктора, все, що побудовано частково, має бути чітко очищено, як правило, в а try .. catch.

Однак компоненти об'єкта, які вже успішно сконструйовані, є відповідальністю за компілятори. Це означає, що на практиці це насправді не велика справа. напр

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

Корпус цього конструктора порожній. До тих пір , як конструктори base1, member2і member3є безпечними винятком, немає нічого страшного. Наприклад, якщо конструктор member2кидає, той конструктор відповідає за очищення. База base1вже була повністю побудована, тому її деструктор буде автоматично викликаний. member3ніколи навіть частково не будувався, тому не потребує очищення.

Навіть коли є тіло, локальні змінні, які були повністю побудовані до викиду винятку, будуть автоматично знищені, як і будь-яка інша функція. Органи конструктора, що перемикають необроблені покажчики або "володіють" якимось неявним станом (зберігається в іншому місці) - як правило, означає, що виклик функції "почати / придбати" повинен відповідати виклику "кінець / випуск" - може спричинити проблеми безпеки виключення, але справжня проблема там не вдається правильно керувати ресурсом через клас. Наприклад, якщо замінити необроблені покажчики unique_ptrна в конструкторі, деструктор для unique_ptrбуде викликаний автоматично, якщо це потрібно.

Є ще інші причини, що люди віддають перевагу конструкторам "мінімальних". Одне це просто тому, що існує правило стилю, багато людей вважають, що виклики конструктора дешеві. Один із способів зробити це, але все ще мають сильні інваріанти, - це мати окремий заводський / будівельний клас, який замість цього має ослаблені інваріанти і який встановлює необхідне початкове значення, використовуючи (можливо, багато) звичайні виклики функцій членів. Після того, як у вас є початковий стан, який вам потрібен, передайте цей об'єкт як аргумент конструктору для класу із сильними інваріантами. Це може "вкрасти кишки" об'єкта слабких інваріантів - перемістити семантику - що є дешевою (і зазвичай noexcept) операцією.

І звичайно, ви можете зафіксувати це у make_whatever ()функції, тож абонентам цієї функції ніколи не потрібно бачити екземпляр класу ослаблених інваріантів.


Абзац, де ви пишете "Поки ви все ще знаходитесь у виклику конструктора, об'єкт не обов'язково повністю побудований, тому не вірно викликати деструктор для цього об'єкта. Тому відповідальність за знищення об'єкта перекладається лише на компілятор коли конструктор успішно закінчується, "може дійсно використовувати оновлення щодо делегування конструкторів. Об'єкт будується повністю, коли закінчується будь-який найбільш похідний конструктор, і деструктор буде викликаний, якщо виняток виникає всередині делегуючого конструктора.
Бен Войгт

Таким чином, конструктор "зробіть мінімум" може бути приватним, а функція "make_wever ()" може бути іншим конструктором, який викликає приватний.
Бен Войгт

Це не означення RAII, з яким я знайомий. Я розумію RAII - це навмисне придбати ресурс у (і тільки в) конструкторі об'єкта і випустити його в його деструктор. Таким чином, об'єкт можна використовувати на стеку для автоматичного управління придбанням та вивільненням ресурсів, які він інкапсулює. Класичний приклад - замок, який при конструюванні набуває мютекс і звільняє його від руйнування.
Ерік

1
@Eric - Так, це абсолютно стандартна практика - стандартна практика, яку зазвичай називають RAII. Визначення розтягує не тільки я - це навіть Струструп, в деяких переговорах. Так, RAII полягає в пов'язуванні зв'язків життєвих циклів ресурсів з об'єктними життєвими циклами.
Steve314

1
@Eric - попередні відповіді видалено, оскільки вони були погано пояснені. У будь-якому випадку самі об'єкти - це ресурси, якими можна володіти. Все повинно мати власника, в ланцюзі прямо до mainфункції або статичних / глобальних змінних. Об'єкт, виділений за допомогою new, не належить до тих пір, поки ви не призначите цю відповідальність, але розумні вказівники володіють об'єктами, виділеними нагромадженнями, на які вони посилаються, і контейнери володіють своїми структурами даних. Власники можуть вирішити видалення рано, власник деструктора несе відповідальність.
Steve314
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.