it-swarm-ru.tech

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

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

62
Thomson

Если не удается построить объект, выведите исключение.

Альтернатива ужасна. Вам нужно будет создать флаг, если конструкция прошла успешно, и проверить его в каждом методе.

36
BЈовић

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

Да.

Если это возможно, как справиться с этим в конструкторе без броска?

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

  • перепроектировать приложение, чтобы оно не нуждалось в конструкторах, чтобы не выбрасывать - действительно, сделайте это, если это возможно
  • добавить флаг и проверить успешность строительства
    • у вас может быть каждая функция-член, которая на законных основаниях может быть вызвана сразу после того, как конструктор проверит флаг, в идеале выбрасывает, если он установлен, но в противном случае возвращает код ошибки
      • Это некрасиво и трудно сохранить правильность, если над кодом работает неустойчивая группа разработчиков.
      • Вы можете получить некоторую проверку во время компиляции, если полиморфно отложить объект до любой из двух реализаций: успешно сконструированной и всегда с ошибочной версией, но это приводит к затратам на использование кучи и производительности.
    • Вы можете перенести бремя проверки флага с вызываемого кода на вызываемого, задокументировав требование, чтобы они вызывали некоторую функцию is_valid () или аналогичную перед использованием объекта: опять-таки подверженный ошибкам и уродливый, но еще более распределенный, неисполнимый и вне контроля.
      • Вы можете сделать это немного проще и более локализованным для вызывающей стороны, если вы поддерживаете что-то вроде: if (X x) ... (т.е. объект может быть оценен в логическом контексте, обычно с помощью operator bool() const или аналогичного интегрального преобразования), но тогда у вас нет x в области, чтобы запросить подробную информацию об ошибке. Это может быть знакомо, например, из if (std::ifstream f(filename)) { ... } else ...;
  • пусть вызывающий абонент предоставит поток, за который он отвечает ... (известный как Dependency Injection или 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 lite обескураживает его, но не согласен с рассуждениями. Не особенно заинтересован в обсуждении, если вы что-то не затронули по FAQ.)

27
Tony Delroy

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

15
jcoder

Новый стандарт C++ переопределяет это во многих отношениях, поэтому пришло время вернуться к этому вопросу.

Лучший выбор:

  • Именовано необязательно: иметь минимальный закрытый конструктор и именованный конструктор: static std::experimental::optional<T> construct(...). Последний пытается настроить поля-члены, обеспечивает инвариант и вызывает закрытый конструктор, только если это обязательно произойдет. Закрытый конструктор заполняет только поля участников. Это опционально легко протестировать, и это недорого (даже в хорошей реализации можно сэкономить даже копию).

  • Функциональный стиль: Хорошая новость в том, что (неименованные) конструкторы никогда не являются виртуальными. Таким образом, вы можете заменить их статической функцией-членом шаблона, которая, помимо параметров конструктора, принимает две (или более) лямбда-выражения: одну, если она была успешной, одну, если она не удалась. "Настоящий" конструктор все еще закрыт и не может потерпеть неудачу. Это может показаться излишним, но лямбды прекрасно оптимизируются компиляторами. Вы можете даже сэкономить if опционального таким образом.

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

  • Исключение: Если ничего не помогает, используйте исключение - но учтите, что вы не можете перехватить исключение во время статической инициализации. Возможный обходной путь - в этом случае возвращаемое значение функции инициализирует объект.

  • Класс Builder: Если конструкция сложная, есть класс, который выполняет проверку и, возможно, некоторую предварительную обработку до такой степени, что операция не может завершиться неудачей. Пусть у него есть способ вернуть статус (да, функция ошибки). Я лично сделал бы это только для стека, чтобы люди не передавали его; затем пусть у него есть метод .build(), который создает другой класс. Если строитель - друг, конструктор может быть приватным. Может даже потребоваться что-то, что может сконструировать только сборщик, так что задокументировано, что этот конструктор должен вызываться только сборщиком.

Неудачный выбор: (но видел много раз)

  • Flag: Не путайте свой инвариант класса с состоянием 'invalid'. Именно поэтому у нас есть optional<>. Подумайте о optional<T>, который может быть недействительным, T, который не может. Функция (членская или глобальная), которая работает только с действительными объектами, работает с T. Тот, который обязательно возвращает действительные, работает на T. Тот, который может вернуть недопустимый объект, возвращает optional<T>. Тот, который может сделать недействительным объект, принимает неконстантный optional<T>& или optional<T>*. Таким образом, вам не нужно будет проверять каждую функцию на предмет правильности вашего объекта (и эти ifs могут стать немного дороже), но и в конструкторе тоже не отказывайте.

  • Конструкция и сеттеры по умолчанию: Это в основном то же самое, что и Flag, только на этот раз вы вынуждены иметь изменяемый шаблон. Забудьте сеттеры, они излишне усложняют ваш инвариант класса. Помните, чтобы ваш класс был простым, а не простым.

  • Конструкция по умолчанию и 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;
    });
}
9
lorro

Конструктор может хорошо открыть файл (не обязательно плохая идея) и может выдать, если открытие файла не удается или если входной файл не содержит совместимых данных.

Разумное поведение для конструктора - генерировать исключение, однако тогда вы будете ограничены в его использовании.

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

  • Это может распространиться на более поздние "ленивые" вычисления "в первый раз", когда что-то загружается в первый раз, когда это требуется, например, в конструкции boost :: Once функция call_once никогда не должна выдавать.

  • Вы можете использовать его в среде IOC (Inversion of Control/Dependency Injection). Вот почему IOC среды являются выгодными.

  • Будьте уверены, что если ваш конструктор сгенерирует, ваш деструктор не будет вызван. Поэтому все, что вы инициализировали в конструкторе до этого момента, должно содержаться в объекте RAII.

  • Кстати, более опасным может быть закрытие файла в деструкторе, если это очищает буфер записи. Ни в коем случае нельзя правильно обрабатывать ошибки, которые могут возникнуть в этой точке.

Вы можете справиться с этим без исключения, оставив объект в состоянии "сбой". Именно так вы должны делать это в тех случаях, когда выбрасывание запрещено, но, конечно, ваш код должен проверять наличие ошибок.

4
CashCow

Я хочу открыть файл в конструкторе класса.

Почти наверняка плохая идея. Очень мало случаев, когда уместно открывать файл во время строительства.

Возможно, что открытие не удастся, тогда строительство объекта не может быть завершено. Как справиться с этой неудачей? Выкинуть исключение?

Да, это был бы способ.

Если это возможно, как справиться с этим в конструкторе без броска?

Сделайте возможным, чтобы полностью сконструированный объект вашего класса мог быть недействительным. Это означает предоставление процедур проверки, их использования и т.д.

4
Crazy Eddie

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

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

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

Он не генерирует исключение, но имеет функцию is_open:

bool is_open ( );
4
sashoalm