C++: обработка исключений

В C++ под Windows есть два способа обработки исключений - традиционный для C++ с пом. try/catch и т.н. структурная обработка исключений или SEH. Основная разница между ними в том, что с помощью try/catch можно реализовывать для разных типов исключений C++ различную реакцию (поведение), а структурная обработка  исключений способна отловить ситуации, которые с пом. catch не ловятся, например, Divide By Zero или Access Violation. SEH - это механизм, предоставляемый операционной системой и на самом деле try/catch реализуется также через него.

Одновременно в одной и той же процедуре использовать оба метода обработки исключений (try/catch и SEH) нельзя. Но можно использовать в одной процедуре try/cath, а в другой SEH.

Оба метода обработки исключений не идеальны и имеют свои достоинства и недостатки.

Основной недостаток try/catch заключается в том, что он не позволяет отловить и обработать все ошибки, а также не показывает место возникновения ошибки. Впрочем, в книжке Джеффри Рихтера (Windows для профессионалов. Создание эффективных Win32-пpилoжeний с учетом специфики 64-разрядной версии Windows) приводится способ перехвата структурных исключений:

1. Нужно создать класс CSE для идентификации структурного исключения:

#include <eh.h>
#include <stdexcept>

class CSE
{
public:
static void MapSEToCE(){ _set_se_translator(TranslateSEToCE); }
operator DWORD() { return (m_er.ExceptionCode);}

private:
CSE(PEXCEPTION_POINTERS pep)
{
m_er = *pep->ExceptionRecord;
m_context = *pep->ContextRecord;
if (m_er.ExceptionCode == 0xe06d7363)
{
printf("C++ Exception: Code 0x%x, Address 0x%x\n", m_er.ExceptionCode, m_er.ExceptionAddress);
}
else
{
printf("SEH Exception: Code 0x%x, Address 0x%x\n", m_er.ExceptionCode, m_er.ExceptionAddress);
}
}
static void _cdecl TranslateSEToCE(UINT deEC,PEXCEPTION_POINTERS pep)
{
throw CSE(pep);
}

private:
EXCEPTION_RECORD m_er;
CONTEXT m_context;
};

2. Для каждого потока надо вызвать один раз CSE::MapSEToCE(), после чего можно обрабатывать структурные исключения как обычные исключения C++:

int DivideByZero()
{
int x = 100;
int y = 0;
return x / y;
}

void ThrowException()
{
throw std::out_of_range("out_of_range");
}

void TestCSE()
{

CSE::MapSEToCE();
try
{
//DivideByZero();
ThrowException();
}
catch (CSE se)
{
switch(se)
{
case EXCEPTION_INT_DIVIDE_BY_ZERO:
printf("Divide by zero!\n");
break;
}
}
catch (std::exception & e)
{
printf("std::exception: %s\n", e.what());
}
}

3. Компилировать нужно с опцией /EHa. В противном случае _set_se_translator не сработает и исключения SEH с помощью catch (…) отлавливаться не будут, а компилятор (Visual Studio 2010) выдаст следующее предупреждение:
warning C4535: calling _set_se_translator() requires /EHa

SEH способен отлавливать все исключения и, что важно на мой взгляд, показывать место возникновения ошибки (ExceptionAddress), а также позволяет продолжить выполнение программы с прерванного места. Но, к сожалению, при возникновении исключения C++ в обработчике (фильтре) SEH документированными способами нельзя узнать тип исключения C++ и получить его текст ошибки. Ниже описан недокументированный способ.

Все исключения C++ в фильтре SEH имеют один и тот же код 0xe06d7363. При этом поле NumberParameters содержит 3 (для 32-разрядных приложений), ExceptionInformation[1] указывает на объект-исключение C++, а ExceptionInformation[2] указывает на структуру _ThrowInfo (ExceptionInformation[0] не интересно). _ThrowInfo относится к так называемым Predefined C++ Types. Для использования предопределенных типов не нужно подключать никаких .h файлов. При редактировании IDE Visual Studio подчеркивает их красным цветом и пишет: Error: identifier “_ThrowInfo” is undefined. Но при этом все успешно компилируется и выполняется :-)

Ниже приведен SEH-фильтр, который способен анализировать исключения C++ и выводить информацию об исключении (what()). Стоит отметить, однако, что хотя эта процедура успешно работает в Visual Studio 2010, но нет никаких гарантий, что в будущем Microsoft не поменяет структуру _ThrowInfo.

#include <eh.h>
#include <stdexcept>

LONG ExceptionFilter(PEXCEPTION_POINTERS pEP, const char * file, const char * function, int line)
{
PEXCEPTION_RECORD pER = pEP->ExceptionRecord;
if (pER->ExceptionCode == 0xe06d7363)
{
if (pER->NumberParameters == 3)
{
const _ThrowInfo * pThrowInfo = (_ThrowInfo*)pER->ExceptionInformation[2];
if (pThrowInfo != NULL)
{
const _CatchableTypeArray * pTypeArray = pThrowInfo->pCatchableTypeArray;
if (pTypeArray != NULL)
{
const _TypeDescriptor * pExceptionDesc = (_TypeDescriptor*)&typeid(std::exception);
for (int i = 0; i < pTypeArray->nCatchableTypes; i++)
{
const _CatchableType * pCatchableType = pTypeArray->arrayOfCatchableTypes[i];
if (pCatchableType != NULL && pExceptionDesc == pCatchableType->pType)
{
std::exception * e = (std::exception*)pER->ExceptionInformation[1];
printf("File: %s\nFunction: %s\nLine: %i\nC++ Exception: %s\n", file, function, line, e->what());
return EXCEPTION_EXECUTE_HANDLER;
}
}
}
}
}
printf("File: %s\nFunction: %s\nLine: %i\nC++ Exception (code 0x%x at 0x%x)\n", file, function, line, pER->ExceptionCode, pER->ExceptionAddress);
return EXCEPTION_EXECUTE_HANDLER;
}
else
{
printf("File: %s\nFunction: %s\nLine: %i\nSEH Exception (code 0x%x at 0x%x)\n", file, function, line, pER->ExceptionCode, pER->ExceptionAddress);
return EXCEPTION_EXECUTE_HANDLER;
}
}

int main(int argc, char* argv[])
{
__try
{
//DivideByZero();
ThrowException();
}
__except(ExceptionFilter(GetExceptionInformation(), __FILE__, __FUNCTION__, __LINE__))
{
//handle exception here
}

return 0;
}

В заключении хочу отметить, что ExceptionAddress для исключений C++ не имеет особого смысла, потому что указывает всегда на один и тот же адрес внутри функции __CxxThrowException(), которая выполняется каждый раз, когда вызывается исключение с помощью throw.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru