Введение

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

   if (блок будет работать) 
   { 
      блок;
   } else {
      сделать_что-то_другое:
   }                      

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

- Содержание -


Синтаксис инициирования исключений

Следующая функция шлепнет вас по рукам, если вызвать ее с неверным параметром. Вместо линейки она воспользуется секцией throw. В этой функции могут произойти две ошибки, представленные константами перечисления Gotcha.

   enum Gotcha { kTooLow, kTooHigh };
   void fndnt x) throw (Gotcha) { 
	if (x < 0) 
	   throw kTooLow; // Функция завершается здесь 
	if (x> 1000) 
	   throw kTooHigh; // Или здесь 
	   // Сделать что-то осмысленное 
   } 

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

- Содержание -

Объявления и определения

Спецификация исключений в объявлении функции должна точно совпадать со спецификацией в ее определении.

	void Fn() throwdnt); // Объявление 
	// Где-то в файле .срр 
	void Fn() throwdnt) {
			// Реализация 
	}

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

- Содержание -

Функции без спецификации исключений

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

	void fn(); // Может инициировать исключения любого типа 
- Содержание -

Функции, не инициирующие исключений

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

	void fn() throw(); // Не инициирует исключении
- Содержание -

Функции, инициирующие исключения нескольких типов

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

	void fn() throw(int, Excpet1on_Struct, char*);
- Содержание -

Передача исключений

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

	void fn() throw; 
- Содержание -

Исключения и сигнатуры функций

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

	void f1(int) throw();
	void f1(int) throw(Exception); // Повторяющаяся сигнатура!
- Содержание -

Непредусмотренные исключения

Если инициированное исключение отсутствует в спецификации исключений внешней функции, программа переформатирует ваш жесткий диск. Шутка. На самом деле она вызывает функцию с именем unexpected(). По умолчанию затем вызывается функция terminate(), о которой будет рассказано ниже, но вы можете сделать так, чтобы вызывалась ваша собственная функция. Соответствующие интерфейсы из заголовочного файла except.h выглядят так:

	typedef void (*unexpected_function)();
	unexpected_funct1on
		set_unexpected(unexpected_function unexpected__func);

В строке typedef... объявляется интерфейс к вашей функции. Функция set_unexpected() получает функцию этого типа и организует ее вызов вместо функции по умолчанию. Функция set_unexpected() возвращает текущий обработчик непредусмотренных исключений. Это позволяет временно установить свой обработчик таких исключений, а потом восстановить прежний. В следующем фрагменте показано, как используется этот прием.

	unexpected_function my_hand1er(void) { 
		// Обработать неожиданное исключение
	}
	{ // Готовимся сделать нечто страшное и устанавливаем свой обработчик
		unexpected_funct1on o1d_hand1er = 
			set_unexpected(my_hand1er);
		// Делаем страшное и возвращаем старый обработчик	
		set_unexpected(o1d_hand1er);
	}

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

- Содержание -


Синтаксис перехвата исключений

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

	try{                                              
		// Фрагмент, который может инициировать исключения
		} 
	catch (Except1оп_Туре t) {               
		// Восстановление после исключения типа Exception_Type)
		}
	// Произвольный список перехватчиков, за которым может следовать 
	catch (...){
		// Восстановление после исключений всех остальных типов
		}

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

- Содержание -

Выполнение программы после исключения

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

	catch(1nt exception) {
		// Сделать что-то, а затем 
		throw("Help!"); // Инициируется исключение типа char*
	}

Инициирование исключения из обработчика немедленно завершает выполнение вмещающей функции или блока.

- Содержание -

Если исключение не перехвачено

Если для исключения не найдется ни одного обработчика, по умолчанию вызывается глобальная функция ternrinate(). Как вы думаете, что она делает? По умолчанию ternrinate() в конечном счете вызывает библиотечную функцию abort(), и дело кончается аварийным завершением всей программы. Вы можете вмешаться и установить собственную функцию завершения с помощью библиотечной функции set_terminate(). Соответствующий фрагмент файла excepth выглядит так:

	typedef void (*terrmnate_functшon)();
	terminate_function
		set_term1nate(term1nate_function t_func);

В строке typedef... объявляется интерфейс к вашей функции завершения. Функция set_termnate() устанавливает функцию завершения, которую вместо функции abort() вызывает функция terminate(). Функция set__terininate() возвращает текущую функцию завершения, которую позднее можно восстановить повторным вызовом set_ternrinate().
Ваша функция завершения обязана завершить программу и не может инициировать другие исключения. Она может выполнить необходимые подготовительные действия, но никогда не возвращает управление вызывающей программе.

- Содержание -

Вложенная обработка исключений

Вложение блоков try/catch разрешается, хотя пользоваться этой возможностью следует как можно реже, если только вы хотите сохранить дружеские отношения с персоналом сопровождения вашей программы.

	{               
		try { 
			try { 
				try {
					// Ненадежный фрагмент
					} 
				catch(...) {
				} 
			}
			catch(...) {
				} 
		} 
		catch(...) {
			}
	}

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

- Содержание -

Внешние исключения не перехватываются!

Вы можете перехватить любое исключение, инициированное посредством throw. Тем не менее, существуют и другие исключения, которые не удается перехватить переносимыми способами. Например, если пользователь применяет для завершения программы комбинацию клавиш с правым Ctrl, нет гарантии, что операционная система сгенерирует исключение, которое может быть перехвачено вашими обработчиками. Вообще говоря, обработка исключений относится только к исключениям, сгенерированным программой; все остальное непереносимо.

- Содержание -


Конструкторы и деструкторы

Одно из принципиальных достоинств стандартной схемы обработки исключений - раскрутка стека (unwinding the stack). При запуске исключения автоматически вызываются деструкторы всех стековых объектов между throw и catch.

	void fn() throw(int) {
		Foo aFoo;
		// Что-то не так! 
		throw(bad_news);
	}

Когда возникает исключение, до передачи стека соответствующему обработчику будет вызван деструктор aFoo. Тот же принцип действует и для try-блока вызывающей стороны.

   {
      try {
         Bar b;
	 fn(); // Вызывает исключение
      }
      catch(int exception) {
	 // Перед тем, как мы попадем сюда, будет вызван деструктор b
      }
   }  	

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

- Содержание -

Уничтожаемые объекты

Гарантируется вызов деструкторов всех стековых объектов, сконструированных с начала выполнения try-блока, но и только. Например, допустим, что к моменту возникновения исключения был сконструирован массив. Деструкторы вызываются лишь для тех объектов массива, которые были сконструированы до возникновения исключения.
Динамические объекты (то есть созданные посредством оператора new) - совсем другое дело. Вам придется самостоятельно следить за ними. Если в куче размещаются объекты, которые должны уничтожаться в результате исключения, обычно для них создается оболочка в виде вспомогательного стекового объекта.

	class TempFoo { 
	private:
		Foo* f;
	public:	
		TempFoo(Foo* aFoo): f(aFoo) {}
		~TempFoo() { delete f: }
	}; 
	try {	
		TempFoo tf(new Foo);
		// и т.д.
	}
	catch(...) {
		// Foo уничтожается деструктором tf
	}
- Содержание -

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

Рассмотрим следующий процесс конструирования:

	class Foo {...}
	class Bar : public Foo {
	private:
		A a;
		В b;
	public:
		Bar();
	};
	Bar::Bar()
	{
		X x;
		throw(bad_news);
		Y y;
	}

Если во время конструирования объекта произойдет исключение, деструкторы будут вызваны для тех компонентов (базовых классов и переменных), конструкторы которых были выполнены к моменту возникновения исключения. Конструирование Ваг к этому моменту еще не завершено. Тем не менее, конструкторы базовых классов (Foo) и переменных (а и b) уже отработали, поэтому их деструкторы будут вызваны до передачи исключения обработчику. По тем же причинам будет вызван деструктор локальной переменной x. Деструктор у не вызывается, поскольку переменная еще не сконструирована. Деструктор Ваг тоже не вызывается, поскольку конструирование объекта не завершилось к моменту инициирования исключения.
Предположим, конструктор b инициирует исключение. В этом случае вызываются деструкторы Foo и а, но не деструкторы b, Bar и у.
Одни и те же принципы действуют как для стековых, так и для динамических объектов. Если исключение возникает при конструировании динамического объекта, вызываются точно те же деструкторы, что и для стековых объектов.

- Содержание -

Порядок вызова деструкторов

Гарантируется, что порядок вызова деструкторов будет обратным порядку вызова конструкторов. Это относится как к локальным переменным, так и к переменным и базовым классам объектов.

- Содержание -


Нестандартная обработка исключений

Многие библиотеки и некоторые компиляторы обрабатывают исключения нестандартным образом. Большинство имитирует парадигму try/catch с помощью макросов, но не организует правильной раскрутки стека за счет вызова деструкторов сконструированных объектов. Некоторые реализации ориентированы на конкретные типы компьютеров и операционных систем.
К сожалению, многие компиляторы претендуют на стандартную обработку исключений, но не во всем следуют каноническим правилам. На своем горьком опыте я узнал, что обработку исключений желательно тестировать, если вы делаете нечто хоть скольконибудь нестандартное (даже если оно должно быть стандартным). Если вы окажетесь в подобной ситуации, окажите услугу всем нам: наймите разработчика такого компилятора якобы для серьезного проекта и заставьте его писать и отлаживать код обработки исключений для его собственного компилятора в течение ближайших пяти лет. А еще лучше, заставьте его перенести в свой компилятор код, идеально работающий на другом компиляторе.
Если по какой-либо причине вам придется заниматься нестандартной обработкой исключений, больше всего проблем вызовет освобождение памяти от динамических объектов, которые были созданы, до возникновения исключения и внезапно стали недоступными переменным. Именно для этой цели используется 99% обработки исключений в реальной жизни, да еще изредка требуется закрыть открытые файлы. Вы можете либо создать хитроумные структуры данных, которые перемещают новые динамические объекты на верх стека и, следовательно, сохраняют их доступность, либо воспользоваться методикой сборки мусора, описанной в последней части книги. Оба подхода выглядят в равной степени отталкивающе, так что выбирайте сами.

- Содержание -