Исключения традиционно относятся к сложной для понимания части C++. В форумах
часто возникают вопросы на эту тему.. в общем – статья назрела. Я не
претендую на авторство высказанных здесь мыслей, я просто собрал всё
вместе.
Исключение – это явление, которое происходит при ненормальном
развитие событий в программе и требует особой логики обработки. В идеале
правильно спроектированная программа не нуждается в обработке исключений. Дело в
том, что ситуации, ведущие к исключениям, можно отлавливать на ранних стадиях,
анализируя все возвращаемые функциями коды ошибок. Если в вашей программе вы
хотите отгородится исключением от попыток деления на 0 или выделения –200 байт
(читается «минус двухсот байт»), то вы находитесь далеко от правильного пути. И
в этом случае лучше доработать алгоритм работы. Заранее проверить «Сколько байт
я хочу выделить?». Заранее проверить, «а на что же я собираюсь поделить?».
Стандарт С (ISO/IEC 9899) не содержит обработку исключений. Это ещё один довод в
пользу того, что правильно спроектированная программа должна правильно работать
и без них.
Живём мы в реальном мире, поэтому обработка исключений нам
нужна. Операционная система не смогла выделить те 500 Мб, которые вы попросили.
Драйвер не оказался загруженным. Файла не оказалось в нужное время в нужном
месте. Да мало ли ещё что произошло! И вот тут нас спасёт обработка исключений.
Кроме того, иногда исключения упрощают вывод сообщений об ошибках в программе,
например, при использовании MFC исключений. Об этом - впереди. В общем,
исключения – это дешёвый способ существенно повысить устойчивость вашего
(особенно системного или серверного) программного обеспечения.
Я не буду
рассказывать про обработку исключений подробно, про это гораздо лучше описано в
учебной литературе, а отвечу лишь на некоторые часто задаваемые
вопросы.
Вопрос. Есть ли в С обработка
исключений? Обработка исключений есть только в C++. Стандарт С (ISO/IEC
9899) её не содержит.
Вопрос. Кто берёт на себя ответственность за
обработку исключений? CRTL – C-Run-Time-Library.
Вопрос. Что
такое SEH? SEH – Structured Exception Handling – в операционной системе
Windows включена обработка исключений на уровне операционной системы. Блоки SEH
оформляются с помощью операторов __try, __finally, __except. Если SEH-исключение
не перехвачено, то произойдет появление хорошо известного окна с предложением
впаять разработчику и остановка процесса.
Вопрос. Чем
плохо использовать операторы __try, __except и т.д. Плохая совместимость
с программами, написанными на «чистом» С++. В пределах одной функции невозможно
пользоваться CRTL и SEH исключениями. Если вы работаете с Visual Studio 6,
CRTL преобразует стандартное исключение в SEH исключения в случае, если при
сборке проекта указан ключ /EHa или (эквивалентный) /GX, и установки галочки
Enable Exception Handling в состояние No. В Visual Studio 7: заходим на вкладку
свойств проекта C/C++ Code Generation, находим строчку Enable C++ exception,
ставим в этой строчке No. Дальше двигаемся в конец к секции Command Line. В ней
есть окошко Additional Options. Надо прописать /EHac. Недостаток этого
подхода - невозможно определить тип исключения, и ,что более серьёзно, возникают
проблемы с плавающей арифметикой (последнее утверждение не проверял, но поверил
одному товарищу). Имеется более усовершенствованный метод. Заключается он в
использовании так называемого se транслятора. Вот примерный код (Visual
Studio).
Смысл
кода приблизительно в следующем. Вызовом функции _set_se_translator можно
установить функцию, которая будет получать управление в случае возникновения в
текущем потоке SEH-исключения. Главное назначение этой функции - получить код
SEH-исключения, завернуть в подходящую обёртку и выбросить нормальное C++
исключение, которое в дальше можно поймать обычным catch(). Коды этих исключений
можно получить из windows.h, а описание - в MSDN в статье про EXCEPTION_RECORD,
либо в прикрепленном файле. Среди этих кодов есть семейство особо важных,
связанных с плавающей точкой. При получении одного из этих кодов, надо делать
маленькую дополнительную обработку. А именно, нужно командой fninit сбросить
сопроцессор в нормальное состояние и загрузить подходящее слово управление.
Иначе флаги исключений по-прежнему будут висеть в сопроцессоре, что вызовет
возбуждение нового исключения при попытке его снова использовать – Вам оно
нужно? Вообще, использование исключений сопроцессора -- это отдельный
нетривиальный вопрос.
Вопрос. Я работаю с STL, очень часто использую
операцию push_back(), при этом не знаю, как контролировать ситуацию, когда
память push_back – ом не выделена, потому что push_back не возвращает код
ошибки. Как мне быть?
Всё нормально – вам необходимо ловить
исключение std::bad_alloc – именно оно генерируется в случае неудачного
проведения операции push_back. И не только push_back() – но и вообще везде, где
STL перераспределяет память – например, resize().
Вопрос. Я пытаюсь
поймать исключение std::bad_alloc при выделении памяти оператором new, но у меня
ничего не получается. Помогите! Тут возможны несколько причин. 1)
Генерация стандартного исключения std::bad_alloc возможна только стандартным
оператором new. То есть для начала необходимо сделать как минимум #include
<new>. 2) Стандарт гарантирует, что в памяти сможет расположиться
std::bad_alloc. Если вы напишете catch(std::bad_alloc){}, то при этом CRTL будет
пытаться расположить в памяти не только сам bad_alloc, но и его копию. Про копию
Стандарт C++ ничего не говорит, поэтому CRTL может игнорировать копии
bad_alloc-а. Правильнее писать: catch(std::bad_alloc &){}. 3) Вы не
загрузили std::bad_alloc в качестве new handler-а. Вот как лучше всего это
сделать.
//код приведён для Visual C++, в иных компиляторах возможны изменения// #include <new> #include <new.h>
//функция установки new handler-a. int _cdecl my_new_handler(size_t) { throw std::bad_alloc(); return 0; }
Обязательно
ли возвращать old_new_handler на место – не знаю, скорее всего необязательно. По
моему - лучше всего это сделать один раз в самом начале программы, а по
завершении – вернуть old_new_handler. С другой стороны производительность
стандартного оператора new (как и всего остального стандартного) немного
хромает, если вы желаете добиться экстра производительности – то old_new_handler
лучше вернуть на место. В общем – я предупредил – остальное на вашей
совести.
4) Вы работаете с MFC. В этом случае вы можете поймать только
указатель на исключение CException либо производный от него. В этом случае, если
вы будете, например, пытаться выделить большое количество памяти, то MFC будет
упорно кидать сообщение «Out of memory». И с этим ничего поделать нельзя –
придётся ловить MFC исключения (не помогает даже ручная установка new
handler-а), это видимо сделано под девизом «Мы в Майкрософт, всегда считаем, что
стандарт можно улучшить» (Copyright кто то из MS, но не Билл
Гейтс);
Вопрос. У меня что то случилось с размером контейнера при
вызове исключения std::bad_alloc – size() вернул одно, а перечисление с помощью
итератора – на один элемент больше. Такое бывает если исключение кидает
конструктор копии – size() не учитывает недоконструированный элемент, а при
перечислении он может и остаться, это касается контейнеров std::list, std::dequе
и других. Это – «особенность дизайна» некотрых реализаций STL, например, той,
что поставляется с Visual C++. Exception safety контейнеров стандартной
библиотеки была добавлена в последний момент процесса стандартизации, поэтому
далеко не все реализации контейнеров правильно ведут себя в присутствии
исключений. Так версия STL от Dinkumware, что поставляется с VC 6 тянется ещё со
времен VC 4.2, т.е. года 1994 - последняя версия стандарта C++ вышла в 1998 году
(комментировать нужно?). Бороться с этим можно путём обновления STL на более
свежую реализацию (например, от STLPort – www.stlport.com). Либо не бросать
исключения в конструкторах.
Вопрос. Перечислите плюсы и минусы
использования SEH по сравнению с обычными CRTL исключениями. Плюсы: 1)
позволяет ловить больший спектр исключений, к которым относится деление на 0,
переполнение стека, и т.д. 2) обработка исключений ведётся на уровне ядра
операционной системы (в WinNT образных ОС); 3) возможность использовать
исключения без CRTL. Часто для уменьшения размера программы её собирают без
CTRL. В этом случае использовать «стандартные» C++ исключения невозможно. SEH
можно будет воспользоваться, если загрузить kernel32.dll.
Минусы: 1)
плохая совместимость с С++. SEH исключения реализованы на уровне ядра ОС,
которое ничего не знает про С++, например про классы. Если произошла
исключительная ситуация, то SEH не гарантирует, что уберёт за собой весь мусор,
потому что не будут вызваны деструкторы пользовательских классов. Это связано с
тем, что если компилятор не видит генерации C++-исключений, то он и не создает
код, который отвечает за размотку стека при исключениях (только при
использовании слов __try, __except и т.д.). 2) невозможность (в пределах
одной функции) пользоваться SEH и стандартными исключениями
одновременно.
__except(EXCEPTION_EXECUTE_HANDLER) //подставить нужное слово { //... }
__try { //.... } __finaly { //... }
GetExceptionCode()
– возвращает код возникшего исключения – можно использовать для вывода
диагностического сообщения. Если Вы используете слово __finaly, то этот блок
будет выполнен в любом случае, даже если попытаться выйти из блока __try с
помощью return; В одном блоке __except и __finaly одновременно быть не
могут.
Кроме того, можно получить машинно-независимую информацию об
исключении, при помощи функции GetExceptionInformation().
Структурная
обработка особых ситуаций средствами Win32 API
Вопрос. Как ловить MFC
исключения?
Я приведу пример, как можно ловить исключения при работе
с файлами, а за подробностями отправлю к MSDN.
CFile f; CFileException *pE = new CFileException; TCHAR szErrorString[255];
if (f.Open(m_sDraftName, CFile::modeRead | CFile::shareDenyWrite, pE) == FALSE) { pE->ReportError(MB_OK | MB_ICONSTOP); pE->GetErrorMessage(szErrorString, 255); WriteErrorInLogFile(szErrorString); //функция записи в лог (пользовательская). pE->Delete(); return FALSE; } delete pE;
У
класса CException и его производных имеется метод ReportError – который выводит
на экран сообщение об ошибке. Так же из этого сообщения можно просто
сформировать строку, например, для вывода в log файл. Для этого есть метод
GetErrorMessage(); Так же MFC исключения можно ловить дедовским способом
try/catch.
Вопрос. В Visual C++ я видел операторы try и TRY. В чём
отличие и чем лучше пользоваться?
Макросы
TRY/CATCH/AND_CATCH/END_CATCH/THROW/THROW_LAST тянутся из тех времен, когда
компилятор C++ от MS еще не поддерживал стандартную обработку исключений.
Пользоваться ли ими – это уже ваш выбор, но в свете сказанного ранее – не
советую.
Вопрос. Как насчёт быстродействия кода получаемого при
использовании исключений? Быстродействие его практически не страдает, но
вот объём существенно возрастает. И всё из за добавления кода «для
отката».
Вопрос. Как насчёт исключений в UNIX-like
системах? В UNIX-ах при возникновении исключений система шлёт сигналы,
например, при возникновении ошибки с плавающей точкой FreeBSD шлёт сигнал SIGFPE
– Floating Point Exception.
Вопрос. Нестандартное использование
исключений. Естественно, можно использовать исключения в нестандартных
ситуациях. Например, для выхода из многоступенчатого цикла – т.е. там, где break
не сработает.
Более
подробную информацию по ловле исключений читайте в прикрепленном файле. (40 927
байт в zip архиве, всё написано русским по белому – кто не испугался – срочно
качаем!).
1) Exception handling в DOS, Win9x,
UNIX. 2) Exception handling в компиляторах Borland (я слышал, что им не нужно
изгаляться с преобразование C++ исключения в SEH, а что Borland кидает
исключительно SEH исключения? – Borland не претендует на универсальность, и в
данном случае это просто прекрасно!). 3) Exception handling в ИмяРек
компиляторах. 4) Exception handling в OLE/COM. 5) Ещё я слышал, что
появилась VEH обработка исключений. Пользовались? Я нет. Поделитесь опытом