Я хочу відкрити файл у конструкторі класів. Не виключено, що отвір міг провалитися, тоді будівництво об’єкта не могло бути завершено. Як впоратися з цією невдачею? Викинути виняток? Якщо це можливо, як це обробляти в конструкторі, що не кидає?
Я хочу відкрити файл у конструкторі класів. Не виключено, що отвір міг провалитися, тоді будівництво об’єкта не могло бути завершено. Як впоратися з цією невдачею? Викинути виняток? Якщо це можливо, як це обробляти в конструкторі, що не кидає?
Відповіді:
Якщо конструкція об’єкта не вдається, видаліть виняток.
Альтернатива жахлива. Вам довелося б створити прапор, якщо побудова вдалася, і перевіряти його у кожному методі.
istream&параметра :)
Я хочу відкрити файл у конструкторі класів. Не виключено, що отвір міг провалитися, тоді будівництво об’єкта не могло бути завершено. Як впоратися з цією невдачею? Викинути виняток?
Так.
Якщо це можливо, як це обробляти в конструкторі, що не кидає?
Ваші варіанти:
if (X x) ...(тобто об’єкт може бути оцінений у логічному контексті, як правило, шляхом надання operator bool() constабо подібного інтегрального перетворення), але тоді у вас немає xможливості для запит про деталі помилки. Це може бути знайомо з напрif (std::ifstream f(filename)) { ... } else ...;bool worked; X x(&worked); if (worked) ...
if (X* p = x_factory()) ...</li>
<li>X x; // ніколи не придатний для використання; if (init_x (& x)) ... `Коротше кажучи, C ++ розроблений для надання елегантних рішень таких питань: у цьому випадку винятки. Якщо ви штучно обмежуєтесь у їх використанні, то не сподівайтесь, що буде щось інше, що робить вдвічі меншу роботу.
(PS Мені подобається передавати змінні, які будуть модифіковані покажчиком - як workedзазначено вище - я знаю, що заданий FAQ задає це питання, але я не погоджуюся з аргументацією. Не особливо зацікавлений в обговоренні цього питання, якщо у вас щось не охоплюється частими запитаннями.)
Новий стандарт 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.
std::variant<ValueType, ErrorInfo>- також не потрібно змінювати введення. Будьте готові, boost::variantякщо у вас його ще немає у вашому компіляторі.
Моя порада щодо цієї конкретної ситуації полягає в тому, що якщо ви не хочете, щоб конструктор вийшов з ладу, оскільки якщо не вдається відкрити файл, то уникайте такої ситуації. Передайте вже відкритий файл конструктору, якщо це те, що ви хочете, тоді воно не може провалитися ...
Я хочу відкрити файл у конструкторі класів.
Майже напевно погана ідея. Дуже мало випадків, коли відкриття файлу під час будівництва доречно.
Не виключено, що отвір міг провалитися, тоді будівництво об’єкта не могло бути завершено. Як впоратися з цією невдачею? Викинути виняток?
Так, це був би спосіб.
Якщо це можливо, як це обробляти в конструкторі, що не кидає?
Зробіть можливим, що повністю побудований об’єкт вашого класу може бути недійсним. Це означає надання процедур перевірки, їх використання тощо ... ick
istreamабо ostreamоб'єкта. Таким чином ви можете провести тестування, замінивши потік на рядок.
openу своєму класі, ви запобігаєте будь-якому повторному використанню, скажімо, зіставленого з пам'яттю файлу, наприклад? З іншого боку, використовуючи базовий клас (подібний до потоку), ви можете передати все, що реалізує його інтерфейс; це полегшує тестування та повторне використання.
std::fstreamвідкриває файл у своєму конструкторі.
Один із способів - викинути виняток. Іншим є наявність функції '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 ( );
ifstreamце об’єкт RAII, який управляє посиланням на файл. Це зовсім інший випадок, ніж у більшості "класів, які відкривають файли у своїх конструкторах" (які я вже бачив у будь-якому випадку) Хороший код C ++ теж не має явних deleteтверджень, але без нього неможливо реалізувати розумні вказівники.
Конструктор цілком може відкрити файл (що не обов'язково погана ідея) і може викинути, якщо відкрити файл не вдається, або якщо вхідний файл не містить сумісних даних.
Конструктор має розумну поведінку створювати виняток, однак тоді ви будете обмежені щодо його використання.
Ви не зможете створювати статичні (на рівні файлу на рівні компіляції) екземпляри цього класу, які будуються перед "main ()", оскільки конструктор повинен бути коли-небудь доданим лише до звичайного потоку.
Це може поширюватися і на пізнішу «першу» ледачу оцінку, де щось завантажується вперше, коли це потрібно, наприклад, у системі boost ::, коли побудована функція call_once ніколи не повинна кидати.
Ви можете використовувати його в середовищі IOC (Inversion of Control / Dependency Injection). Ось чому середовища МОК вигідні.
Будьте впевнені, що якщо ваш конструктор кине, ваш деструктор не буде викликаний. Отже, все, що ви ініціалізували в конструкторі до цього моменту, повинно міститися в об’єкті RAII.
До речі, більш небезпечним може бути закриття файлу в деструкторі, якщо це змиє буфер запису. Зовсім неможливо правильно обробити будь-яку помилку, яка може виникнути в цей момент.
Ви можете обробляти це без винятку, залишаючи об'єкт у "невдалому" стані. Це потрібно робити в тих випадках, коли метання заборонено, але, звичайно, ваш код повинен перевірити наявність помилки.
Використовуйте завод.
Фабрика може бути " 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
};
На відміну від встановлення "дійсних" бітів чи інших хаків, це відносно чисто і розширювано. Правильно зроблено, це гарантує, що жодні недійсні предмети ніколи не втечуть у дику природу, а написання похідних "Фу" все ще просто. А оскільки заводські функції - це звичайні функції, ви можете робити з ними багато інших речей, яких не можуть конструктори.
На мою скромну думку, ви ніколи не повинні вкладати в конструктор будь-який код, який може реально вийти з ладу. Це майже означає все, що виконує введення-виведення чи іншу `` справжню роботу ''. Конструктори - це особливий куточок мови, і в основному їм не вистачає можливості обробляти помилки.