Як обробляти збій у конструкторі в C ++?


78

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



1
"Якщо ви не можете використати механізм обробки помилок без винятків у конструкторі, тоді не використовуйте конструктор." Детальніше читайте тут: foonathan.net/2017/01//exceptions-constructor .
Габріель Стейплз

Відповіді:


39

Якщо конструкція об’єкта не вдається, видаліть виняток.

Альтернатива жахлива. Вам довелося б створити прапор, якщо побудова вдалася, і перевіряти його у кожному методі.


5
Було б чудово запропонувати і "реальне" рішення, наприклад, передачу istream&параметра :)
Matthieu M.

@Matthie Так, інверсія контролю. Я постійно про це забуваю. Дякуємо за нагадування.
BЈовић

Ні, ні, це не жахливо без винятків: llvm.org/docs/ProgrammersManual.html#fallible-constructors
Нільс,

@Nils Це було б названо гидотою в будь-якому звичайному огляді коду. Плюс вам потрібно перевірити, чи знаходиться об’єкт у нормальному стані, перш ніж щось робити з ним.
BЈовић

28

Я хочу відкрити файл у конструкторі класів. Не виключено, що отвір міг провалитися, тоді будівництво об’єкта не могло бути завершено. Як впоратися з цією невдачею? Викинути виняток?

Так.

Якщо це можливо, як це обробляти в конструкторі, що не кидає?

Ваші варіанти:

  • перепроектуйте програму, щоб їй не потрібні конструктори, щоб не кидати - дійсно, зробіть це, якщо це можливо
  • додати прапор і тест на успішне будівництво
    • у вас може бути кожна функція-член, яку можна законно викликати відразу після тестування конструктора прапора, в ідеалі кидати, якщо він встановлений, але в іншому випадку повертати код помилки
      • Це потворно і важко втримати правильно, якщо у вас є нестабільна група розробників, яка працює над кодом.
      • Ви можете отримати деяку перевірку під час компіляції, якщо об'єкт поліморфно відкласти на будь-яку з двох реалізацій: успішно побудовану та версію, що завжди має помилку, але це вводить витрати на використання купи та продуктивність.
    • Ви можете перенести тягар перевірки прапора із викликаного коду на викликаного, задокументувавши вимогу про те, що вони викликають якусь "is_valid ()" або подібну функцію перед використанням об'єкта: знову схильний до помилок і потворний, але ще більш розподілений, нездійсненний і неконтрольований.
      • Ви можете зробити це дещо простішим та локалізованішим для абонента, якщо підтримуєте щось на зразок: if (X x) ...(тобто об’єкт може бути оцінений у логічному контексті, як правило, шляхом надання operator bool() constабо подібного інтегрального перетворення), але тоді у вас немає xможливості для запит про деталі помилки. Це може бути знайомо з напрif (std::ifstream f(filename)) { ... } else ...;
  • попросіть абонента надати потік, за який вони відповідають за відкриття ... (відоме як інжекція залежності або DI) ... в деяких випадках це працює не так добре:
    • у вас все ще можуть бути помилки, коли ви йдете використовувати потік всередині свого конструктора, що тоді?
    • сам файл може бути деталлю реалізації, яка повинна бути приватною для вашого класу, а не піддаватися виклику: що, якщо ви захочете видалити цю вимогу пізніше? Наприклад: ви, можливо, читали таблицю підрахунку попередньо розрахованих результатів з файлу, але зробили свої розрахунки настільки швидко, що не потрібно проводити попередню обчислення - боляче (іноді навіть непрактично в корпоративному середовищі) видаляти файл у кожній точці використання клієнтом, і змушує набагато більше перекомпілювати, а не просто перенаправляти.
  • змусити абонента надати буфер для змінної успіху / відмови / помилки-умови, яку встановлює конструктор: наприклад bool worked; X x(&worked); if (worked) ...
    • цей тягар і багатослівність привертає увагу і, сподіваємось, робить абонента набагато більш свідомим необхідності звертатися до змінної після побудови об'єкта
  • змусити абонента побудувати об’єкт за допомогою іншої функції, яка може використовувати коди повернення та / або винятки:
    • if (X* p = x_factory()) ...
    • Smart_Ptr_Throws_On_Null_Deref p_x = x_factory (); </li> <li>X x; // ніколи не придатний для використання; if (init_x (& x)) ... `
    • тощо ...

Коротше кажучи, C ++ розроблений для надання елегантних рішень таких питань: у цьому випадку винятки. Якщо ви штучно обмежуєтесь у їх використанні, то не сподівайтесь, що буде щось інше, що робить вдвічі меншу роботу.

(PS Мені подобається передавати змінні, які будуть модифіковані покажчиком - як workedзазначено вище - я знаю, що заданий FAQ задає це питання, але я не погоджуюся з аргументацією. Не особливо зацікавлений в обговоренні цього питання, якщо у вас щось не охоплюється частими запитаннями.)


17

Новий стандарт C ++ переосмислює це так багато способів, що настав час переглянути це питання.

Найкращий вибір:

  • Названий по бажанню : мати мінімальний приватний конструктор і конструктор з ім'ям: static std::experimental::optional<T> construct(...). Останній намагається налаштувати поля-члени, забезпечує інваріант і викликає приватний конструктор, лише якщо це напевно вдасться. Приватний конструктор заповнює лише поля-члени. Легко протестувати додатковий матеріал, і він недорогий (навіть копія може бути збережена в хорошій реалізації).

  • Функціональний стиль : Гарною новиною є те, що (неіменовані) конструктори ніколи не бувають віртуальними. Тому ви можете замінити їх статичною функцією-членом шаблону, яка, крім параметрів конструктора, приймає дві (або більше) лямбди: одну, якщо вона була успішною, одну, якщо вона не вдалася. "Реальний" конструктор все ще приватний і не може зазнати невдачі. Це може здатися надмірним, але компілятори чудово оптимізують лямбди. Ви можете навіть пощадити ifнеобов’язковий таким чином.

Хороший вибір:

  • Виняток : Якщо все інше не вдається, використовуйте виняток, але зверніть увагу, що ви не можете зловити виняток під час статичної ініціалізації. Можливим обхідним шляхом є повернення значення функції для ініціалізації об’єкта в цьому випадку.

  • Клас Builder : Якщо конструкція ускладнена, майте клас, який виконує перевірку та, можливо, деяку попередню обробку до такої міри, що операція не може провалитися. Нехай він має спосіб повернути статус (так, функція помилки). Я б особисто зробив це лише для стека, щоб люди не передавали його; тоді нехай він має .build()метод, який будує інший клас. Якщо будівельник є другом, конструктор може бути приватним. Може знадобитися навіть щось, що може побудувати тільки конструктор, щоб було задокументовано, що цей конструктор повинен викликати тільки конструктор.

Неправильний вибір: (але бачений багато разів)

  • Прапор : Не псуйте свій інваріант класу, маючи недійсний стан. Це саме те, чому ми маємо optional<>. Подумайте, optional<T>що може бути недійсним, Tщо не може. Функція (член або глобальна), яка працює лише з дійсними об'єктами, працює T. Той, який, безсумнівно, повертає діючі роботи T. Той, який може повернути недійсне повернення об'єкта optional<T>. Той, який може призвести до недійсності об'єкта, приймає non-const optional<T>&або optional<T>*. Таким чином, вам не потрібно буде перевіряти в кожній функції кожну функцію, чи дійсний ваш об’єкт (і ці ifs можуть стати дещо дорогими), але тоді також не зазнайте збоїв у конструкторі.

  • Конструкт за замовчуванням і сетери : Це в основному те саме, що і прапор, тільки на цей раз ви змушені мати змінний шаблон. Забудьте про сетерів, вони без потреби ускладнюють ваш інваріант класу. Пам’ятайте, щоб ваш клас був простим, а не простим у побудові.

  • Конструкція за замовчуванням, init()яка приймає параметри ctor : це ніщо краще, ніж функція, яка повертає optional<>, але вимагає двох конструкцій і псує ваш інваріант.

  • Візьмітьbool& succeed : Це те, що ми робили раніше optional<>. Причина optional<>вища, ви не можете помилково (або необережно!) Ігнорувати succeedпрапор і продовжувати використовувати частково побудований об'єкт.

  • Фабрика, яка повертає покажчик : Це менш загально, оскільки змушує об’єкт динамічно розподілятися. Або ви повертаєте заданий тип керованого вказівника (і, отже, обмежуєте схему розподілу / масштабування), або повертаєте оголений ptr і ризикуєте, що клієнти просочуються. Крім того, із схемою переміщення з точки зору продуктивності це може стати менш бажаним (місцеві жителі, коли вони перебувають у стеку, дуже швидкі та зручні для кешу).

Приклад:

#include <iostream>
#include <experimental/optional>
#include <cmath>

class C
{
public:
    friend std::ostream& operator<<(std::ostream& os, const C& c)
    {
        return os << c.m_d << " " << c.m_sqrtd;
    }

    static std::experimental::optional<C> construct(const double d)
    {
        if (d>=0)
            return C(d, sqrt(d));

        return std::experimental::nullopt;
    }

    template<typename Success, typename Failed>
    static auto if_construct(const double d, Success success, Failed failed = []{})
    {
        return d>=0? success( C(d, sqrt(d)) ): failed();
    }

    /*C(const double d)
    : m_d(d), m_sqrtd(d>=0? sqrt(d): throw std::logic_error("C: Negative d"))
    {
    }*/
private:
    C(const double d, const double sqrtd)
    : m_d(d), m_sqrtd(sqrtd)
    {
    }

    double m_d;
    double m_sqrtd;
};

int main()
{
    const double d = 2.0; // -1.0

    // method 1. Named optional
    if (auto&& COpt = C::construct(d))
    {
        C& c = *COpt;
        std::cout << c << std::endl;
    }
    else
    {
        std::cout << "Error in 1." << std::endl;
    }

    // method 2. Functional style
    C::if_construct(d, [&](C c)
    {
        std::cout << c << std::endl;
    },
    []
    {
        std::cout << "Error in 2." << std::endl;
    });
}

bool& succeed насправді не обов’язково бути булем. Це також може бути код помилки, який дасть вам більше інформації, ніж std :: optional.
Ganea Dan Andrei

@GaneaDanAndrei: Якщо ваш тип містить або значення, або інформацію про помилку, ви б повернули його (не сприймали як аргумент). Якщо вона містить лише інформацію про помилку, ви можете повернути std::variant<ValueType, ErrorInfo>- також не потрібно змінювати введення. Будьте готові, boost::variantякщо у вас його ще немає у вашому компіляторі.
lorro

15

Моя порада щодо цієї конкретної ситуації полягає в тому, що якщо ви не хочете, щоб конструктор вийшов з ладу, оскільки якщо не вдається відкрити файл, то уникайте такої ситуації. Передайте вже відкритий файл конструктору, якщо це те, що ви хочете, тоді воно не може провалитися ...


3
А що, якщо вміст файлу порожній? Або містить недійсні дані?
CashCow

5
Плакат лише сказав, що хоче відкрити файл у конструкторі. Очевидно, що якщо він робить більше, тоді це може зазнати невдачі іншими способами, і з цим потрібно буде впоратися належним чином.
jcoder

4

Я хочу відкрити файл у конструкторі класів.

Майже напевно погана ідея. Дуже мало випадків, коли відкриття файлу під час будівництва доречно.

Не виключено, що отвір міг провалитися, тоді будівництво об’єкта не могло бути завершено. Як впоратися з цією невдачею? Викинути виняток?

Так, це був би спосіб.

Якщо це можливо, як це обробляти в конструкторі, що не кидає?

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


6
Чому відкриття файлу чи будь-якого іншого ресурсу в конструкторі - погана ідея?
Йорген Зігвардссон

3
@ Йорген Зігвардссон: Тому що було б краще писати свій клас з точки зору будь-якого даного istreamабо ostreamоб'єкта. Таким чином ви можете провести тестування, замінивши потік на рядок.
Billy ONeal

1
@ Йорген Сігвардссон: Це суперечить філософії ін'єкції залежності. Ваше питання краще читати навпаки: навіщо зав'язувати руку в спині? Явно використовуючи a openу своєму класі, ви запобігаєте будь-якому повторному використанню, скажімо, зіставленого з пам'яттю файлу, наприклад? З іншого боку, використовуючи базовий клас (подібний до потоку), ви можете передати все, що реалізує його інтерфейс; це полегшує тестування та повторне використання.
Matthieu M.

1
@Crazy: отже, стандартна бібліотека теж робить це неправильно? std::fstreamвідкриває файл у своєму конструкторі.
jalf

1
@Matthieu: Я не бачу, як обсяг роботи в конструкторах має якесь відношення до DI ...? Крім того, наявність "працьовитого" конструктора не виключає надання відкритого методу.
Йорген Зігвардссон

4

Один із способів - викинути виняток. Іншим є наявність функції 'bool is_open ()' або 'bool is_valid ()', яка повертає значення false, якщо в конструкторі щось пішло не так.

Деякі коментарі тут говорять про неправильність відкриття файлу в конструкторі. Я зазначу, що ifstream є частиною стандарту C ++, він має такий конструктор:

explicit ifstream ( const char * filename, ios_base::openmode mode = ios_base::in );

Він не створює виняток, але він має функцію is_open:

bool is_open ( );

1
Ну, ifstreamце об’єкт RAII, який управляє посиланням на файл. Це зовсім інший випадок, ніж у більшості "класів, які відкривають файли у своїх конструкторах" (які я вже бачив у будь-якому випадку) Хороший код C ++ теж не має явних deleteтверджень, але без нього неможливо реалізувати розумні вказівники.
Billy ONeal

Тільки щоб це згадати: багатьом людям не подобається підхід is_open () або is_valid () і вважає його поганим. Це тому, що користувачі класу можуть легко забути викликати цей метод, і в результаті ви отримаєте частково побудований клас і вам потрібно включити тест is_open () у безліч функцій-членів. Однак у деяких випадках це може бути варіантом.
sstn

4

Конструктор цілком може відкрити файл (що не обов'язково погана ідея) і може викинути, якщо відкрити файл не вдається, або якщо вхідний файл не містить сумісних даних.

Конструктор має розумну поведінку створювати виняток, однак тоді ви будете обмежені щодо його використання.

  • Ви не зможете створювати статичні (на рівні файлу на рівні компіляції) екземпляри цього класу, які будуються перед "main ()", оскільки конструктор повинен бути коли-небудь доданим лише до звичайного потоку.

  • Це може поширюватися і на пізнішу «першу» ледачу оцінку, де щось завантажується вперше, коли це потрібно, наприклад, у системі boost ::, коли побудована функція call_once ніколи не повинна кидати.

  • Ви можете використовувати його в середовищі IOC (Inversion of Control / Dependency Injection). Ось чому середовища МОК вигідні.

  • Будьте впевнені, що якщо ваш конструктор кине, ваш деструктор не буде викликаний. Отже, все, що ви ініціалізували в конструкторі до цього моменту, повинно міститися в об’єкті RAII.

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

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


0

Використовуйте завод.

Фабрика може бути " Factory<T>" цілим заводським класом " " для побудови ваших " T" об'єктів (це не повинен бути шаблон) або статичним загальнодоступним методом " T". Потім ви робите конструктор захищеним і залишаєте деструктор загальнодоступним. Це гарантує, що нові класи все ще можуть виходити з "T ", але жоден зовнішній код, крім них, не може безпосередньо викликати конструктор.

Заводськими методами (C ++ 17)

class Foo {
protected:            
   Foo() noexcept;                 // Default ctor that can't fail 
   virtual bool Initialize(..); // Parts of ctor that CAN fail 
public: 
   static std::optional<Foo>   Create(...) // 'Stack' or value-semantics version (no 'new')
   {
      Foo out();
      if(foo.Initialize(..)) return {out};
      return {};
   }
   static Foo* /*OR smart ptr*/ Create(...) // Heap version.
   {
      Foo* out = new Foo();
      if(foo->Initialize(...) return out;
      delete out;
      return nullptr;
   }
   virtual ~Foo() noexcept; // Keep public to allow normal inheritance
 };

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.