Seksowne raportowanie błędów w Cpp

cepa

Seksowne raportowanie błędów w C++

Wstęp

C++ jest jezykiem który daje programiście właściwie niczym nie ograniczoną swobodę. Niestestety jest to okupione zwiększeniem ryzyka błędów i zmniejszeniem produktywności (choć z tym można by polemizować).

Jednym z mechanizmów które java dostarcza natywnie jest wyłapywanie błędów, w C++ tzw wyjątki (exceptions) są tylko opcją która mimo tego że powoduje lekki spadek wydajności programu to może stać się bardzo przydatnym narzędziem do debugowania błędów w programie.

Standardowe wyjątki C++ mają jedną wadę, zgłaszają sam błąd, domyślnie nie ma w nich informacji o tym w ktorym dokładnie miejscu wystąpił wyjątek. Można temu zaradzić wykorzystująć tzw śledzenie stosu (stack tracing).

</p>

Stack Tracing

Celem tego artykułu jest pokazanie prostej metody która umożliwia zgłoszenie błędu ze stosownym komunikatem np: w takim stylu.

``` Fatal error in function 'void Klasa::metoda()' from [Main.cc:18]: to jest jakis komunikat o bledzie... Stack trace: #4: Function 'void Klasa::metoda()' from [Main.cc:17] #3: Function 'void funkcja3()' from [Main.cc:23] #2: Function 'void funkcja1()' from [Main.cc:33] #1: Function 'int main(int, char**)' from [Main.cc:39] ```

Jak widać poza samym ambitnym komunikatem dostaliśmy informacje o funkcji w której wystąpił błąd, pliku oraz lini. Dodatkowo poniżej mamy scieżkę jaką przebył program z funkcji początkowej (main) to funkcji która wywołała wyjątek (metoda).

Jak to działa?

Cała sztuczka polega na utworzeniu dodatkowego stosu na którym będziemy przechowywać informacje o tym w który punkt programu jest wykonywany w danej chwili. Tak więc przy uruchomieniu danej funkcji należy odłożyć o niej informacje na stos, a jak funkcja zakończy się bez błędów to należy tą informację zdjąć ze stosu tak aby to wszystko miało sens. Aby uprościć ten proces i ograniczyć się do jednego makra, możemy wykorzystać mechanizm obiektów lokalnych. Jak wiadomo w C++ kiedy tworzymy obiekt na początku funkcji jest uruchamiany jego konstruktor, kiedy funkcja się kończy uruchamiany jest destruktor. Dzieki temu możemy utworzyć instancję jakiejś prostej klasy której konstruktor odłoży na stos informacje o swoim położeniu, a destruktor ją zdejmię.

Wszystko pięknie tylko jest jedno ale.
Gdy w C++ wywoływany jest wyjątek uruchamiane są automatycznie destruktory obiektów lokalnych. Czyli jeżeli nic nie zrobimy to mimo wystąpienia wyjątku cały przebieg programu zostanie zdjęty ze stosu i na wyjściu nie otrzymamy żadnej informacji.
Aby to ominąć wystarczy dać klasie stosu możliwość blokowania - tzn: wywołanie wyjątku blokuje nasz stos przez co następnie wywoływane bez naszej wiedzy destruktory nie będą usuwały informacji o przebiegu programu, aż do momentu przechwycenia wyjątku.

Jeszcze słowo o informacji o punkcie programu. Tutaj z pomocą przychodzni nam preprocesor C++. Standardowo udostępnia on dwa makra: LINE (aktualna linia w pliku) FILE (nazwa aktualnego pliku), dodatkowo niektóre pakiety np: GCC udostępniają dodatkową informację o nazwie aktualnej funkcji: FUNCTION lub pełna deklaracje funkcji: PRETTY_FUNCTION.
Aby zachować te informacje na stosie wystarczy napisać proste makra które podadzą automatycznie te informacje do obiektu.

</p>

Kod

StackTrace.hh

```cpp #ifndef __STACKTRACE_HH__GUARD #define __STACKTRACE_HH__GUARD

#include <string>

namespace Debug {
namespace StackTrace {

struct TraceData_t
{
std::string fileName; // nazwa pliku
std::string functionName; // nazwa funkcji
int line; // linia w pliku
}; // struct TraceData_t

void Push( const TraceData_t &traceData ); // odklada trace point na stos
void Pop( void ); // zdejmuje ze stosu
void Lock( void ); // blokuje stos
void Print( void ); // wyswietla przebieg programu

class CTraceJoin
{

public:
CTraceJoin( const char *fileName, const char *functionName, int line )
{
m_traceData.fileName = fileName;
m_traceData.functionName = functionName;
m_traceData.line = line;
Push(m_traceData);
}

~CTraceJoin() 
{ 
	Pop();
}

private:
TraceData_t m_traceData;

}; // class CTraceJoin

// z tej klasy beda dziedziczyc wszystkie wyjatki w naszym programie
class CException
{

public:
CException() { Lock(); }
~CException() {}

}; // class CException

}; // namespace StackTrace
}; // namespace Debug

// makro ktore zachowuje przebieg programu
#ifdef NDEBUG
#define TRACE
#else
#define TRACE Debug::CTraceJoin ___localTraceJoin(FILE, PRETTY_FUNCTION, LINE);
#endif

#endif // __STACKTRACE_HH__GUARD


<h4>StackTrace.cc</h4>
```cpp
#include <StackTrace.hh>
#include <list>
#include <iostream>

namespace Debug {
namespace StackTrace {

using std::list;
using std::cerr;
using std::endl;

// mozna uzyc std::stack ale lista jest dwukierunkowa :)
list<TraceData_t> g_backTrace;
bool g_stackLock = false;
int g_traceCounter = 0;

void Push( const TraceData_t &traceData )
{
	if (!g_stackLock) {
		g_backTrace.push_back(traceData);
		g_traceCounter++;
	}
}

void Pop( void )
{
	if (!g_stackLock) {
		g_backTrace.pop_back();
		g_traceCounter--;
	}
}

void Lock( void )
{
	g_stackLock = true;
}

void Print( void )
{
#ifndef NDEBUG
	cerr << "Stack trace:" << endl;
	int i = g_traceCounter;
	list<TraceData_t>::const_reverse_iterator iter = g_backTrace.rbegin();
	for (; iter != g_backTrace.rend(); ++iter, --i)
		cerr << '#' << i <<  ": Function '" << (*iter).functionName << "' from [" << (*iter).fileName << ':' << (*iter).line << ']' << endl;
#endif
}

} // namespace StackTrace
} // namespace Debug

CException.hh

```cpp #ifndef __CEXCEPTION_HH__GUARD #define __CEXCEPTION_HH__GUARD

#include <Debug.hh>

class CException : public Debug::CException
{

public:
explicit CException(
const char *msg,
const char *fileName = "",
const char *funcName = "",
int line = 0)
: m_msg(msg),
m_fileName(fileName),
m_funcName(funcName),
m_line(line)
{}

~CException() {}

const char *getMessage() const { return m_msg; }
const char *getFileName() const { return m_fileName; }
const char *getFuncName() const { return m_funcName; }
int getLine() const { return m_line; }

private:
const char *m_msg;
const char *m_fileName;
const char *m_funcName;
int m_line;

}; // class CException

// dzieki temu makru mamy dokladna informacje o punkcie wywolania wyjatku.
#define THROW(msg) throw CException(msg, FILE, PRETTY_FUNCTION, LINE);

#endif // __CEXCEPTION_HH__GUARD


<h4>Main.cc</h4>
```cpp
#include <CException.hh>
#include <iostream>

class Klasa 
{
public:
	void metoda()
	{ TRACE
		THROW("to jest jakis komunikat o bledzie...");
	}
};

void funkcja3()
{ TRACE
	Klasa k;
	k.metoda();
}

void funkcja2()
{ TRACE
}

void funkcja1()
{ TRACE
	funkcja2();
	funkcja3();
}

int main( int argc, char **argv )
{ TRACE
	try {
		funkcja1();

	} catch (CException &exc) {
		std::cerr 
			<< "Fatal error in function '" 
			<< exc.getFuncName() 
			<< "' from [" 
			<< exc.getFileName()
			<< ':'
			<< exc.getLine()
			<< "]: " << exc.getMessage() << std::endl;
		Debug::StackTrace::Print();
		return -1;

	} catch (...) {
		std::cerr << "Fatal error: Uncaught exception!" << std::endl;
		Debug::StackTrace::Print();
		return -1;
	}
	return 0;
}

Jak widać kod programu nie jest specjalnie skomplikowany a samo używanie śledzenia stosu jest wręcz banalne, jedyny mankament to potrzeba wywoływania makra TRACE na poczatku każdej funkcji która może zgłosic wyjątek, oraz wyrzucanie wyjątkow za pomoca makra THROW zamiast normalnego throw.

Pozdrawiam wszystkich dżawowców! :D

2 komentarzy

Ale to jest rozwiązanie tylko dla aplikacji jednowątkowych, prawda? Czy jestem w błędzie?
Jeśli nie jestem, to dałoby rade bez większego wysiłku przerobić to tak, żeby współpracowało z aplikacjami wielowątkowymi?

Nareszcie jakiś rzeczowy i pożyteczny artykuł się pojawił na tym portalu. Narządko w prawdzie całkiem sexy, choć do tych celów normalnie używa się memory dumpów i całej związanej z nim otoczki.
Nie mniej, do jakichś mniejszych programów pisanych na styl kontraktowy, całkiem, całkiem w cipkę.