Исключения в конструкторе и деструкторе

      Комментарии к записи Исключения в конструкторе и деструкторе отключены

Главная Форумы Программирование Программирование на С++ Учебные материалы по С++ Исключения в конструкторе и деструкторе

В этой теме 1 ответ, 2 участника, последнее обновление  Васильев Владимир Сергеевич 1 год назад.

  • Автор
    Сообщения
  • #2995

    questioner
    Участник

    Говорят, что конструктор конструктор и деструктор класса не должны вырабатывать исключения?
    В тоже время, я видел огромное количество кода и даже примеров из книг, где в конструкторе выделяется память при помощи оператора new, но ведь даже он может вырабатывать исключение bad_alloc — выходит, что весь этот код потенциально опасный? Как с этим бороться?

  • #2996

    Исключения, ровно как и оператор return прерывают поток выполнения команд функции, из системного стека выбираются объекты (такие как локальные переменные) и для них вызываются деструкторы. Однако, если при выполнении оператора return раскрутка стека прекратиться в точке где была вызвана завершенная функция, то при при выполнении throw объекты из стека будут уничтожаться до тех пор, пока управление не будет передано в блок try{}, содержащий обработчик, соответствующий типу выброшенного исключения. Читать подробнее про обработку исключений [1].

    Исключения в деструкторе класса

    В связи с этим, в большинстве случаев разрушение объектов созданных на стеке (без использования new/malloc) произойдет корректно — вызовом деструктора. Однако исключения в конструкторе или деструкторе могут приводить к нежелательным последствиям.

    Во-первых, программа не должна вырабатывать исключения во время обработки другого исключения (когда происходит раскрутка стека) — это приведет к аварийному завершению работы программы (фактически вызову abort()), которое уже не получится корректно обработать. Причина такой ошибки заключается в том, что один из деструкторов вырабатывает исключение или не обрабатывает исключение функции, которую вызывает:

    struct PrinterBusyException {};
    
    class Printer {
      std::string m_location, m_port;
    public:
      Printer(std::string location,
              std::string port) 
      : m_location(location),
        m_port(port) {
      }
      
      bool is_busy() {
        return false; // for example 
      }
      
      ~Printer() {
        if (is_busy()) {
          throw PrinterBusyException();
        }
      }
    };
    
    struct SomeException {};
    
    int main() { 
      try {
        Printer printer("localhost", "usb://Kyocera/FS-1020MFP?serial=LDA4322583");
        // some actions ...
        throw SomeException();
      }
      catch(SomeException exception) {
        std::cout << "SomeException handled\n";
      }
      catch(PrinterBusyException exception) {
        std::cout << "PrinterBusyException handled\n";
      }
    }

    В этом примере деструктор класса Printer вырабатывает исключение если принтер занят (печатает), однако в коде, который использует объект принтера вырабатывается исключение (это может быть что угодно — хоть bad_alloc от оператора new). При обработке SomeException раскручивается стек, вызываются деструкторы и если вдруг принтер окажется занят (is_busy() вернет true) — программа аварийно остановится. Ниже приведены варианты сообщений, выдаваемых программой в зависимости от результата is_busy():

    destructor_exception_abort

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

    Исключения в конструкторе класса

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

    Стандарт языка С++ гарантирует, что если исключение возникнет в конструкторе, то памяти из под членов-данных класса будет освобождена корректно вызовом деструктора — т.е. если вы используете идиому RAII [2], то проблем не будет. Часто для этого достаточно использовать std::vector/std::string вместо старых массивов и строк, и умные указатели вместо обычных [3]. Если же вы продолжите использовать сырые указатели и динамически выделять память — нужно будет очень тщательно следить за ней, например в следующем фрагменте кода нет утечки, т.к. исключение будет выработано только если память не будет выделена [4]:

    template <class ElementType>
    Array<ElementType>::Array() : m_realSize(Step), m_size(0), m_array(0) { 
      m_array = (ElementType*)malloc(sizeof(ElementType)*m_realSize);
      if (0 == m_array) {
        throw bad_allocation();
      }
    } 

    Вспомогательная литература по теме исключений в С++

    1. Обработка исключений — описание процессов, которые происходят при возникновении исключения в программе, сравнение их с возвратом кодом ошибки функцией и общие рекомендации по использованию исключений (без привязки к конкретному языку программирования);
    2. Идиома RAII. Объекты управления ресурсами в C++ — описание идиомы RAII, следование которой помогает безопасно управлять ресурсами при обработке исключений;
    3. описание класса unique_ptr являющегося простейшим умных указателем и реализующему идиому RAII;
    4. Реализация класса одномерного массива — содержит пример безопасного конструктора, вырабатывающего исключение.
    5. Мейерс С. Эффективное использование С++. 35 новых рекомендаций по улучшению ваших программ и проектов. – М.: ДМК Пресс, 2014. — третья глава книги полностью посвящена вопросам обработки исключений в С++.

Для ответа в этой теме необходимо авторизоваться.