воскресенье, 24 марта 2013 г.

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 для идентификации структурного исключения:

  1. #include <eh.h>
  2. #include <stdexcept>
  3.  
  4. class CSE
  5. {
  6. public:
  7.     static void MapSEToCE(){ _set_se_translator(TranslateSEToCE); }
  8.     operator DWORD() { return (m_er.ExceptionCode);}
  9.  
  10. private:
  11.     CSE(PEXCEPTION_POINTERS pep)
  12.     {
  13.         m_er = *pep->ExceptionRecord;
  14.         m_context = *pep->ContextRecord;
  15.         if (m_er.ExceptionCode == 0xe06d7363)
  16.         {
  17.             printf("C++ Exception: Code 0x%x, Address 0x%x\n", m_er.ExceptionCode, m_er.ExceptionAddress);
  18.         }
  19.         else
  20.         {
  21.             printf("SEH Exception: Code 0x%x, Address 0x%x\n", m_er.ExceptionCode, m_er.ExceptionAddress);
  22.         }
  23.     }
  24.     static void _cdecl TranslateSEToCE(UINT deEC,PEXCEPTION_POINTERS pep)
  25.     {
  26.         throw CSE(pep);
  27.     }
  28.  
  29. private:
  30.     EXCEPTION_RECORD m_er;
  31.     CONTEXT m_context;
  32. };

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

  1. int DivideByZero()
  2. {
  3.     int x = 100;
  4.     int y = 0;
  5.     return x / y;
  6. }
  7.  
  8. void ThrowException()
  9. {
  10.     throw std::out_of_range("out_of_range");
  11. }
  12.  
  13. void TestCSE()
  14. {
  15.  
  16.     CSE::MapSEToCE();
  17.     try
  18.     {
  19.         //DivideByZero();
  20.         ThrowException();
  21.     }
  22.     catch (CSE se)
  23.     {
  24.         switch(se)
  25.         {
  26.             case EXCEPTION_INT_DIVIDE_BY_ZERO:
  27.                 printf("Divide by zero!\n");
  28.                 break;
  29.         }
  30.     }
  31.     catch (std::exception & e)
  32.     {
  33.         printf("std::exception: %s\n", e.what());
  34.     }
  35. }

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.

  1. #include <eh.h>
  2. #include <stdexcept>
  3.  
  4. LONG ExceptionFilter(PEXCEPTION_POINTERS pEP, const char * file, const char * function, int line)
  5. {
  6.     PEXCEPTION_RECORD pER = pEP->ExceptionRecord;
  7.     if (pER->ExceptionCode == 0xe06d7363)
  8.     {
  9.         if (pER->NumberParameters == 3)
  10.         {
  11.             const _ThrowInfo * pThrowInfo = (_ThrowInfo*)pER->ExceptionInformation[2];
  12.             if (pThrowInfo != NULL)
  13.             {
  14.                 const _CatchableTypeArray * pTypeArray = pThrowInfo->pCatchableTypeArray;
  15.                 if (pTypeArray != NULL)
  16.                 {
  17.                     const _TypeDescriptor * pExceptionDesc = (_TypeDescriptor*)&typeid(std::exception);  
  18.                     for (int i = 0; i < pTypeArray->nCatchableTypes; i++)
  19.                     {
  20.                         const _CatchableType * pCatchableType = pTypeArray->arrayOfCatchableTypes[i];
  21.                         if (pCatchableType != NULL && pExceptionDesc == pCatchableType->pType)
  22.                         {
  23.                             std::exception * e = (std::exception*)pER->ExceptionInformation[1];
  24.                             printf("File: %s\nFunction: %s\nLine: %i\nC++ Exception: %s\n", file, function, line, e->what());
  25.                             return EXCEPTION_EXECUTE_HANDLER;
  26.                         }
  27.                     }
  28.                 }
  29.             }
  30.         }
  31.         printf("File: %s\nFunction: %s\nLine: %i\nC++ Exception (code 0x%x at 0x%x)\n", file, function, line, pER->ExceptionCode, pER->ExceptionAddress);
  32.         return EXCEPTION_EXECUTE_HANDLER;
  33.     }
  34.     else
  35.     {
  36.         printf("File: %s\nFunction: %s\nLine: %i\nSEH Exception (code 0x%x at 0x%x)\n", file, function, line, pER->ExceptionCode, pER->ExceptionAddress);
  37.         return EXCEPTION_EXECUTE_HANDLER;
  38.     }
  39. }
  40.  
  41. int main(int argc, char* argv[])
  42. {
  43.     __try
  44.     {
  45.         //DivideByZero();
  46.         ThrowException();
  47.     }
  48.     __except(ExceptionFilter(GetExceptionInformation(), __FILE__, __FUNCTION__, __LINE__))
  49.     {
  50.         //handle exception here
  51.     }
  52.  
  53.     return 0;
  54. }

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

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

суббота, 23 марта 2013 г.

C++: конструкторы и виртуальные функции

Как говорится, век - живи, век учись.

Захотелось мне вынести инициализацию данных класса в виртуальную функцию init(), с тем, чтобы классы-наследники могли переопределить ее и добавить в инициализацию что-то своё. Примерно так:

  1. class C1
  2. {
  3. protected:
  4.     int m_count;
  5.     virtual void init() { m_count = 1; }
  6. public:
  7.     C1() { init(); }
  8.     inline int count() const { return m_count; }
  9. };
  10.  
  11. class C2: public C1
  12. {
  13. protected:
  14.     virtual void init() { C1::init(); m_count = m_count+1; }
  15. public:
  16.     C2() : C1() {}
  17. };
  18.  
  19. int _tmain(int argc, _TCHAR* argv[])
  20. {
  21.     C1 c1;
  22.     _tprintf(_T("C1 count: %i\n"), c1.count());
  23.     C2 c2;
  24.     _tprintf(_T("C2 count: %i\n"), c2.count());
  25.     return 0;
  26. }

Результат получается такой:

C1 count: 1
C2 count: 1

Опа! Оказывается переопределенная в классе C2 функция init() не вызывается. Получается, что если вызвать из конструктора класса виртуальную функцию этого же класса, то обычный метод вызова виртуальных функций через vtable задействован не будет - функция будет вызвана как если бы они была не-виртуальной. По-моему такое поведение не выглядит логичным, поэтому от вызова виртуальных функций из конструктора лучше отказаться.

PS. Из деструктора вызывать виртуальные функции тоже не следует.

По поводу того, что при вызове виртуальных функций из конструктора (и деструктора) не используется vtable, - тут я, похоже, был не прав. vtable используется, только в конструкторе класса C1 (см. пример) используется vtable класса C1, а не класса-потомка C2 - вот в чем дело. Аналогично, в деструкторе C1 также используется vtable класса С1.

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