Исключения

Естественный порядок функционирования программ нарушают возникающие нештатные ситуации, в большинстве случаев связанные с ошибками времени выполнения (но не всегда).

  • нехватка оперативной памяти
  • попытка доступа к элементы коллекции по некорректному индексу
  • попытка недопустимого преобразования динамических типов и пр.

Обработка исключительных ситуаций носит невозвратный характер.

Носителями информации об аномальной ситуации (исключении) в C++ являются объекты заранее выбранные на эту роль типов (пользовательских или… базовых, например char*). Такие объекты называются **объектами-исключениями**. Жизненный цикл объектов-исключений начинается с возбуждения исключительной ситуации посредством оператора **throw**. При этом тип генерируемого исключения в общем случае *может* быть любым:

throw "Illegal cast";				// char*
throw IllegalCast();				// class IllegalCast
enum Status { Ok, BadIndex, IllegalCast};
throw IllegalCast;					// enum Status
throw NULL;							// int
throw nullptr;						// pointer

Пример:

int main() {
	try {
		cout << "Throwing an integer exception...\n"; 
		throw 42;    
	} catch (int i) {
		cout << " the integer exception was caught, with value: " << i << endl;    
	}
	return 0;
}

Try-блок и Catch-блок

В языке C++ мы используем ключевое слово try для определения блока стейтментов (так называемого блока try). Блок try действует как наблюдатель в поисках исключений, которые были выброшены каким-либо из операторов в этом же блоке try.

Ключевое слово catch используется для определения блока кода (так называемого блока catch), который обрабатывает исключения определенного типа данных.

Хоть в C++ можно сгенерировать исключаение любого типа, стандартом является исключения отнаследованные от типа std::exception. И здесь и далее должна будет использоваться именно этот стандартный подход

Так как в блоки try может быть сгенерированы разные исключения при определенных условиях, то и количество блоков catch не ограничено; в каждом из которых можно ловить свой тип исключений. Все блоки catch выполняются последовательно, и, если тип исключения соответсвует нужному блоку, то произойдет вход в него и после выход из блоков try-catch (то есть все последующие проигнорируются).

Общим типом исключений, как уже было описано является тип std::exception. Но, если, чтобы словить совсмем все типы исключений (даже те, что не отнаследованы от std::exception), то можно применить специальную форму - catch(…) - осуществляет перехват любых исключений.

Также тип исключений может быть как имеющий параметр исключение, так и без него. Все описанные формы представимы так:

// Именованный формальный параметр
try { /* */ } catch (const std::exception& e) { /* */ }
// Неименованный формальный параметр
try { /* */ } catch (const std::exception&) { /* */ }
// Особая форма блока-обработчика исключений осуществляет перехват любых исключений
try { /* */ } catch (...) { /* */ }

Пример:

try {
	string("abc").substr(10); // генерирует std::length_error
} catch (const std::exception& e) {
	cout << e.what(); // "invalid string position"
}


try {
	f();
} catch (const std::exception& e) {
	// будет выполнен если f() сгенерирует исключение	
} catch (const std::runtime_error& e) {
	// недостижимый код
}

Функциональные защищенные блоки

Защищенный блок может быть оформлен не только как часть функции, но и функции целиком (в том числе main() и конструкторы классов). В таком случае защищенный блок называют функциональным:

void foobar() try {
	// …
 }
 catch(/* … */) { /*… */ }
 catch(/* … */) { /*… */ }
 catch(/* … */) { /*… */ }

Раскрутка стека и уничтожение объектов

Поиск catсh-блока, пригодного для обработки исключения, приводит к раскрутке стека – последовательному выходу из составных операторов и определений функций. В ходе раскрутки стека происходит уничтожение локальных объектов, определенных в покидаемых областях видимости. При этом деструкторы локальных объектов вызываются штатным образом. Исключение, для обработки которого не найден catch-блок, инициирует запуск функции terminate(), передающей управление функции abort(), которая аварийно завершает программу.

Повторное возбуждение исключения.

Оператор throw без параметров помещается (только) в catch-блок и повторно генерирует исключение. При этом его копия не создается.

try {
	f();
} catch (const std::exception& e) {
	// будет выполнен если f() сгенерирует исключение и проброщено дальше по стеку
    throw;
} catch (const std::runtime_error& e) {
	// недостижимый код
}

Описание контракта исключений. Безопасные функции

Если мы заранее знаем, что конкретная функция может сгенерировать исключение мы можем помочь разработчику при вызове этих функций наверняка поместить их в try-catch блок. Достигается это добавлением к описанию функции слова throw(…) с указанием конкретных типов исключений:

 int foo(int &i) throw();
 bool bar(char* pc = 0) throw(IllegalCast);
 void foobar() throw(IllegalCast, BadIndex);

Более того, в процессе конструирования объекта исключение может быть сгенерировано в одном из базовых классов иерархии. C++ позволяет обработать данное исключение и надлежащим образом создать объект. Для обработки всех исключений, возникших при исполнении конструктора, его тело и список инициализации должны быть совместно помещены в функциональный защищенный блок.

Пример 1:

class Alpha {
public:
    Alpha(int value) { /*...*/ }
};
class Beta : public Alpha {
public:
    Beta(int value);
};


 Beta::Beta(int value)
 try : Alpha(foo(value)) {
	// ...
 } catch(...) {
	// ...
 }

Пример 2:

struct S {
	string m;
	S(const string& arg) try : m(arg) {
		cout << "constructed, m = " << m << endl;
	} catch(const std::exception& e) {
		cerr << "arg=" << arg << " failed: " << e.what() << endl;
	} // throw;
};

Пример 3:

int f(int n = 2) try {
	++n;
	throw n;
} catch(...) {
	++n; 
	return n;
}

Деструркторы и исключения

Деструкторы классов не должны возбуждать исключений.

Нейтральный код

От безопасности программного кода важно отличать нейтральность, под которой, согласно терминологии Г. Саттера (Herb Sutter), следует понимать способность в методах “пропускать сквозь себя” исключения, полученные ими на обработку.

Нейтральный метод:

  • может обрабатывать исключения
  • должен ретранслировать исключения (в неизменном виде или дополненном виде).

Иерархия классов исключений. Класс std::exception

 class exception {
 public:
	exception() throw();
	exception(const exception&) throw();
	exception& operator=(const exception&) throw();
	virtual ~exception() throw();
	virtual const char* what() const throw();
};

Существуют стандартные разновидности этого класса.

  1. Логические ошибки (std::logic_error):
    • std::invalid_argument - неверный аргумент
    • std::out_of_range - вне диапазона
    • std::length_error - неверная длина
  2. Ошибки времени выполнения (std::runtime_error):
    • std::range_error - ошибка диапазона
    • std::overflow_error - переполнение (в т. ч. структуры данных)
    • std::underflow_error - потеря порядка (в т. ч. попытка получить элемент из пустой коллекции)
  3. Общие
    • std::bad_alloc - ошибка выделения динамической памяти
    • std::bad_cast - ошибка приведения типа (dynamic_cast)