как справиться с ошибкой в конструкторе в c ++?

<p>я хочу открыть файл в конструкторе класса. возможно, что открытие не удастся, тогда строительство объекта не может быть завершено. как справиться с этой неудачей? выкинуть исключение? если это возможно, как справиться с этим в конструкторе без броска?</p>

<p>если не удается построить объект, выведите исключение.</p><p>альтернатива ужасна. вам нужно будет создать флаг, если конструкция прошла успешно, и проверить его в каждом методе.</p>

<blockquote> <p>я хочу открыть файл в конструкторе класса. возможно, что открытие не удастся, тогда строительство объекта не может быть завершено. как справиться с этой неудачей? выкинуть исключение?</p> </blockquote><p>да.</p><blockquote> <p>если это возможно, как справиться с этим в конструкторе без броска?</p> </blockquote><p>ваши варианты:</p><ul> <li><strong>перепроектировать приложение, чтобы оно не нуждалось в конструкторах, чтобы не выбрасывать</strong> - действительно, сделайте это, если это возможно</li> <li>добавить флаг и проверить успешность строительства <ul> <li>у вас может быть каждая функция-член, которая на законных основаниях может быть вызвана сразу после того, как конструктор проверит флаг, в идеале выбрасывает, если он установлен, но в противном случае возвращает код ошибки <ul> <li><em> это некрасиво и трудно сохранить правильность, если над кодом работает неустойчивая группа разработчиков. </em></li> <li>вы можете получить некоторую проверку во время компиляции, если полиморфно отложить объект до любой из двух реализаций: успешно сконструированной и всегда с ошибочной версией, но это приводит к затратам на использование кучи и производительности.</li> </ul> </li> <li>вы можете перенести бремя проверки флага с вызываемого кода на вызываемого, задокументировав требование, чтобы они вызывали некоторую функцию is\_valid () или аналогичную перед использованием объекта: опять-таки подверженный ошибкам и уродливый, но еще более распределенный, неисполнимый и вне контроля. <ul> <li>вы можете сделать это немного проще и более локализованным для вызывающей стороны, если вы поддерживаете что-то вроде: <code>if (x x) ...</code> (т.е. объект может быть оценен в логическом контексте, обычно с помощью <code>operator bool() const</code> или аналогичного интегрального преобразования), но тогда у вас нет <code>x</code> в области, чтобы запросить подробную информацию об ошибке. это может быть знакомо, например, из <code>if (std::ifstream f(filename)) { ... } else ...;</code></li> </ul> </li> </ul> </li> <li>пусть вызывающий абонент предоставит поток, за который он отвечает ... (известный как dependency injection или di) ... в некоторых случаях это работает не так хорошо: <ul> <li>у вас все еще могут быть ошибки при использовании потока внутри вашего конструктора, что тогда?</li> <li>сам файл может быть деталью реализации, которая должна быть приватной для вашего класса, а не предоставленной вызывающей стороне: что если вы захотите удалить это требование позже? например: вы, возможно, читали таблицу поиска предварительно рассчитанных результатов из файла, но сделали ваши вычисления настолько быстрыми, что нет необходимости в предварительном расчете - это болезненно (иногда даже нецелесообразно в корпоративной среде) удалять файл в каждой точке использование клиента, и требует гораздо больше перекомпиляции, чем просто повторное связывание.</li> </ul> </li> <li>вынудить вызывающую сторону предоставить буфер для переменной успеха/сбоя/условия ошибки, которую устанавливает конструктор: например, <code>bool worked; x x(&worked); if (worked) ...</code> <ul> <li>это бремя и многословие привлекают внимание и <em>, мы надеемся, </em> заставляют вызывающую сторону гораздо больше осознавать необходимость обращения к переменной после создания объекта</li> </ul> </li> <li>вынудить вызывающего объекта создать объект с помощью другой функции, которая может использовать коды возврата и/или исключения: <ul> <li><code>if (x* p = x\_factory()) ...</code></li> <li>smart\_ptr\_throws\_on\_null\_deref p\_x = x\_factory (); <code></li> <li></code>x x; // никогда не используется; if (init\_x (& x)) ... `</li> <li>так далее...</li> </ul> </li> </ul><p>короче говоря, c++ предназначен для предоставления элегантных решений для такого рода проблем: в данном случае исключения. если вы искусственно ограничиваете себя от их использования, то не ожидайте, что будет что-то еще, что наполовину менее эффективно.</p><p>(ps мне нравится передавать переменные, которые будут изменены указателем - согласно <code>worked</code> выше - я знаю, что faq lite обескураживает его, но не согласен с рассуждениями. не особенно заинтересован в обсуждении, если вы что-то не затронули по faq.)</p>

<p>я предлагаю для этой конкретной ситуации, что если вы не хотите, чтобы конструктор выходил из строя, потому что, если не удается открыть файл, то избегайте такой ситуации. передайте уже открытый файл в конструктор, если это то, что вы хотите, то это не может дать сбой ...</p>

<p>новый стандарт c++ переопределяет это во многих отношениях, поэтому пришло время вернуться к этому вопросу.</p><p>лучший выбор:</p><ul> <li> <p><em>именовано необязательно</em>: иметь минимальный закрытый конструктор и именованный конструктор: <code>static std::experimental::optional<t> construct(...)</code>. последний пытается настроить поля-члены, обеспечивает инвариант и вызывает закрытый конструктор, только если это обязательно произойдет. закрытый конструктор заполняет только поля участников. это опционально легко протестировать, и это недорого (даже в хорошей реализации можно сэкономить даже копию).</p> </li> <li> <p><em>функциональный стиль</em>: хорошая новость в том, что (неименованные) конструкторы никогда не являются виртуальными. таким образом, вы можете заменить их статической функцией-членом шаблона, которая, помимо параметров конструктора, принимает две (или более) лямбда-выражения: одну, если она была успешной, одну, если она не удалась. "настоящий" конструктор все еще закрыт и не может потерпеть неудачу. это может показаться излишним, но лямбды прекрасно оптимизируются компиляторами. вы можете даже сэкономить <code>if</code> опционального таким образом.</p> </li> </ul><p>хороший выбор:</p><ul> <li> <p><em>исключение</em>: если ничего не помогает, используйте исключение - но учтите, что вы не можете перехватить исключение во время статической инициализации. возможный обходной путь - в этом случае возвращаемое значение функции инициализирует объект.</p> </li> <li> <p><em>класс builder</em>: если конструкция сложная, есть класс, который выполняет проверку и, возможно, некоторую предварительную обработку до такой степени, что операция не может завершиться неудачей. пусть у него есть способ вернуть статус (да, функция ошибки). я лично сделал бы это только для стека, чтобы люди не передавали его; затем пусть у него есть метод <code>.build()</code>, который создает другой класс. если строитель - друг, конструктор может быть приватным. может даже потребоваться что-то, что может сконструировать только сборщик, так что задокументировано, что этот конструктор должен вызываться только сборщиком.</p> </li> </ul><p>неудачный выбор: (но видел много раз)</p><ul> <li> <p><em>flag</em>: не путайте свой инвариант класса с состоянием 'invalid'. именно поэтому у нас есть <code>optional<></code>. подумайте о <code>optional<t></code>, который может быть недействительным, <code>t</code>, который не может. функция (членская или глобальная), которая работает только с действительными объектами, работает с <code>t</code>. тот, который обязательно возвращает действительные, работает на <code>t</code>. тот, который может вернуть недопустимый объект, возвращает <code>optional<t></code>. тот, который может сделать недействительным объект, принимает неконстантный <code>optional<t>&</code> или <code>optional<t>*</code>. таким образом, вам не нужно будет проверять каждую функцию на предмет правильности вашего объекта (и эти <code>if</code>s могут стать немного дороже), но и в конструкторе тоже не отказывайте.</p> </li> <li> <p><em>конструкция и сеттеры по умолчанию</em>: это в основном то же самое, что и flag, только на этот раз вы вынуждены иметь изменяемый шаблон. забудьте сеттеры, они излишне усложняют ваш инвариант класса. помните, чтобы ваш класс был простым, а не простым.</p> </li> <li> <p><em>конструкция по умолчанию и <code>init()</code>, которая принимает параметры ctor</em>: это не что иное, как функция, которая возвращает <code>optional<></code>, но требует двух конструкций и портит ваш инвариант.</p> </li> <li> <p><em>взять <code>bool& succeed</code></em>: это было то, что мы делали до <code>optional<></code>. причина <code>optional<></code> выше, вы не можете по ошибке (или небрежно!) игнорировать флаг <code>succeed</code> и продолжать использовать частично созданный объект.</p> </li> <li> <p><em>фабрика, которая возвращает указатель</em>: это менее общий принцип, поскольку он вызывает динамическое выделение объекта. либо вы возвращаете заданный тип управляемого указателя (и, следовательно, ограничиваете схему выделения/определения области действия), либо возвращаете незаполненный ptr и риск утечки клиентов. кроме того, с точки зрения производительности схемы перемещения это может стать менее желательным (локальные локальные хранилища очень быстрые и удобные для кэширования).</p> </li> </ul><p>пример:</p><pre><code>#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; }); } </code></pre>

<p>конструктор может хорошо открыть файл (не обязательно плохая идея) и может выдать, если открытие файла не удается или если входной файл не содержит совместимых данных.</p><p>разумное поведение для конструктора - генерировать исключение, однако тогда вы будете ограничены в его использовании.</p><ul> <li> <p>вы не сможете создавать статические (на уровне файла модуля компиляции) экземпляры этого класса, которые создаются до "main ()", так как конструктор должен только быть брошен в обычный поток.</p> </li> <li> <p>это может распространиться на более поздние "ленивые" вычисления "в первый раз", когда что-то загружается в первый раз, когда это требуется, например, в конструкции boost :: once функция call\_once никогда не должна выдавать.</p> </li> <li> <p>вы можете использовать его в среде ioc (inversion of control/dependency injection). вот почему ioc среды являются выгодными.</p> </li> <li> <p>будьте уверены, что если ваш конструктор сгенерирует, ваш деструктор не будет вызван. поэтому все, что вы инициализировали в конструкторе до этого момента, должно содержаться в объекте raii.</p> </li> <li> <p>кстати, более опасным может быть закрытие файла в деструкторе, если это очищает буфер записи. ни в коем случае нельзя правильно обрабатывать ошибки, которые могут возникнуть в этой точке.</p> </li> </ul><p>вы можете справиться с этим без исключения, оставив объект в состоянии "сбой". именно так вы должны делать это в тех случаях, когда выбрасывание запрещено, но, конечно, ваш код должен проверять наличие ошибок.</p>

<p><i>я хочу открыть файл в конструкторе класса.</i></p><p>почти наверняка плохая идея. очень мало случаев, когда уместно открывать файл во время строительства.</p><p><i>возможно, что открытие не удастся, тогда строительство объекта не может быть завершено. как справиться с этой неудачей? выкинуть исключение?</i></p><p>да, это был бы способ.</p><p><i>если это возможно, как справиться с этим в конструкторе без броска?</i></p><p>сделайте возможным, чтобы полностью сконструированный объект вашего класса мог быть недействительным. это означает предоставление процедур проверки, их использования и т.д.</p>

<p>одним из способов является создание исключения. другой - иметь функцию 'bool is\_open ()' или 'bool is\_valid ()', которая возвращает false, если что-то пошло не так в конструкторе.</p><p>некоторые комментарии здесь говорят, что открывать файл в конструкторе неправильно. я укажу, что если поток является частью стандарта c++, он имеет следующий конструктор:</p><pre><code>explicit ifstream ( const char * filename, ios\_base::openmode mode = ios\_base::in ); </code></pre><p>он не генерирует исключение, но имеет функцию is\_open:</p><pre><code>bool is\_open ( ); </code></pre>