ПрограммированиеПодсказкиОбщее

Трассировка стека вызовов при ошибке (с использованием исключений)

Автор:

Для начала определим класс трассирующего исключения со всеми его функциями-членами:

/*******************/
/*CFatalException.h*/
/*******************/
#pragma once

#include <list>

class CFatalException
{
public:
  CFatalException();
  CFatalException(const CFatalException &x);
  CFatalException(const std::string &msg);
  CFatalException(const CFatalException &x, const char *fn);
  ~CFatalException();

  const CFatalException &operator=(const CFatalException &x);

  void ShowErrorMessage() const;
private:
  struct _SExceptionInfo
  {
    std::string            Message;
    std::list<const char*> TracePoints;
    unsigned int           RefCount;
  };

  _SExceptionInfo *_exinfo;
};

/*********************/
/*CFatalException.cpp*/
/*********************/
#include <windows.h>
#include <list>
#include "CFatalException.h"

CFatalException::CFatalException()
{
  _exinfo = new _SExceptionInfo;
  _exinfo->RefCount = 1;
}

CFatalException::CFatalException(const CFatalException &x)
{
  _exinfo = x._exinfo;
  ++(_exinfo->RefCount);
}

CFatalException::CFatalException(const std::string &msg)
{
  _exinfo = new _SExceptionInfo;
  _exinfo->Message = msg;
  _exinfo->RefCount = 1;
}

CFatalException::CFatalException(const CFatalException &x, const char *fn)
{
  _exinfo = x._exinfo;
  ++(_exinfo->RefCount);
  _exinfo->TracePoints.push_front(fn);
}

CFatalException::~CFatalException()
{
  --(_exinfo->RefCount);
  if (!_exinfo->RefCount) delete _exinfo;
}

const CFatalException &CFatalException::operator=(const CFatalException &x)
{
  --(_exinfo->RefCount);
  if (!_exinfo->RefCount) delete _exinfo;
  _exinfo = x._exinfo;
  _exinfo->RefCount++;
  return *this;
}

void CFatalException::ShowErrorMessage() const
{
  std::list<const char*>::const_iterator s, e, i;
  std::string                            buf;
  
  buf = "Error message: ";
  buf += _exinfo->Message;
  buf += "\nCall stack: !root!";
  s = _exinfo->TracePoints.begin();
  e = _exinfo->TracePoints.end();
  for (i = s; i != e; ++i)
  {
    buf += " -> ";
    buf += *i;
  }
  MessageBox(0, buf.c_str(), "Error!", MB_ICONERROR);
}

Теперь пара слов о том, как это работает и как это заюзать. При возникновении супер-мега-фатальной ситуации, мы генерируем исключение нашего типа:

if (что_то_очень_страшное) throw(CFatalException("сообщение об ошибке"));

После этого исключение начнёт проваливаться через каждую функцию стека вызовов до самого main'а. Но это не совсем то, что нам нужно. Чтобы шла собственно трассировка, необходимо для кода каждой функции программы предусмотреть обёртку:

void Foo()
{
  try
  {
    /* ... */
    /* ... */
    /* ... */
    /*Здесь будет находиться самый обычный код функции Foo*/
    /* ... */
    /* ... */
    /* ... */
  }catch(const CFatalException &x){
    throw(CFatalException(x, __FUNCTION__));   //Бросаем исключение дальше
  }
}

Теперь в связанном списке _exinfo->TracePoints будут сохранены указатели на имена всех функций, участвовавших в фатальной цепочке вызовов. Осталось только отловить исключение и вывести сообщение об ошибке:

int main()
{
  try
  {
    /* ... */
    /* ... */
    /* ... */
    /*Код функции main, содержащий вызовы "опасных" функций*/
    /* ... */
    /* ... */
    /* ... */
  }catch(const CFatalException &x){
    x.ShowErrorMessage();
    exit(666);
  }
}

Для удобства можно написать пару макросов:

//Начало обёртки
#define enter try{

//Конец обёртки
#define leave }catch(const CFatalException &x){throw(CFatalException(x, __FUNCTION__));}

//Инициирование исключения
#define error(msg) throw(CFatalException(msg))

Вот пример использования:

#include <windows.h>
#include "CFatalException.h"   //Заголовочный файл с описанием класса нашего трассирующего
                               //исключения и макросами, упрощающими жизнь

void f3()
{
enter
  error("jopa!");
leave
}

void f2()
{
enter
  f3();
leave
}

void f1()
{
enter
  f2();
leave
}

int main()
{
  try
  {
    f1();
    return 0;
  }catch(const CFatalException &x){
    x.ShowErrorMessage();
    exit(666);
  }
}

При выполнении эта программа выдаст message box вот с таким сообщением:

Error message: jopa!
Call stack: !root! -> f1 -> f2 -> f3

Достоинства этого метода:
1) Более высокая скорость по сравнению с другими методами, так как трассировка активируется только при возникновении исключения. Можно не отключать в релизной версии программы.
2) Позволяет получить очень полезную для отладки информацию. Немного изменив функцию начального инициирования исключения можно сохранять номер конкретной строки в коде, где произошла ошибка.
3) Достаточно удобно использовать, если код изначально рассчитан на такую трассировку.
4) Не нужно создавать никаких лишних объектов.
Недостатки:
1) Трудно встроить в уже написанный код.
2) Исключения по стандарту ANSI не сочетаются с исключениями Win32 в рамках одной функции. Этот недостаток можно побороть, полностью перейдя на Win32-исключения. Правда при этом сильно усложнится код и скорее всего уменьшится быстродействие (сам я это делать не пробовал).
3) У некоторых программистов наблюдается неадекватная реакция на слово "исключение" :-).

#call stack

9 июня 2009