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

#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. – третья глава книги полностью посвящена вопросам обработки исключений в С++.