Chronienie funkcji

Chronienie funkcji to ciekawe zastosowanie metody obsługi sytuacji wyjątkowych, dzięki któremu możemy w chwili wystąpienia błędu uzyskać dokładne informacje o miejscu awarii. Samą technikę podpatrzyłem dawno temu w publicznym kodzie gry Unreal Tournament. Przykładowy komunikat błędu prezentuje poniższy screen:

Komunikat o błędzie pokazujący stos wywołań.

Jak widać, komunikat zawiera historię wywołań funkcji, które doprowadziły do miejsca wystąpienia błędu (tak zwany call-stack). Napiszmy dwa pomocnicze makra oraz przykład użycia
#define GUARD(functionName) {static const char __GUARDED_FUNCTION_NAME__[] = #functionName; try{
#define UNGUARD } catch(...) { Framework->GetErrorManager()->AddCallHistory(__GUARDED_FUNCTION_NAME__); throw; } }
//przyklad uzycia
void funkcja_chroniona()
{
	GUARD(funkcja_chroniona);
	//jakies operacje
	UNGUARD(funkcja_chroniona);
}
Makra te ujmują kod zawarty między nimi w blok try-catch. Kiedy chcemy zasygnalizować wystąpienie bardzo poważnego błędu, który powinien spowodować przerwanie działania programu, wówczas rzucamy wyjątek. W chwili złapania wyjątku w makrze UNGUARD, nazwa podana do makra GUARD zostaje zapisana w systemie obsługi błędów ( to proszę napisać sobie samemu ;) ), po czym wyjątek zostaje rzucony dalej. Jeśli zabezpieczymy w ten sposób wiele funkcji, to rzucony wyjątek przeleci przez nie wszystkie aż do miejsca, w którym zostanie obsłużony i zatrzymany. Takim miejscem może być np. pętla główna gry. Przykład:
//punkt obslugi, fragment jakiejs funkcji
//...
try
{
	EnterMainLoop();
}
catch(std::exception& exc)
{
	errorSystem->DisplayException("STL exception", exc.what());
}
catch(CMyException& exc)
{
	errorSystem->DisplayException(exc.type(), exc.what());
}
catch(...)
{
	errorSystem->DisplayException("Unknown exception", "Unknown error");
}
//...
Z metodą tą wiążą się też pewne niedogodności:
  • Każdą chronioną funkcję / blok kodu trzeba zawierać w makrach GUARD/UNGUARD
  • Blok try/catch powoduje zwiększenie czasu wykonania funkcji
  • Utrudnione lub wręcz uniemożliwione jest używanie w kodzie wyjątków w celach innych niż obsługa błędów krytycznych, powodujących awaryjne zakończenie pracy programu
Równoważone są one jednak przez korzyść wynikającą ze znajomości dokładnej historii wywołań funkcji prowadzącej do miejsca wystąpienia błędu. Oczywiście makr tych nie stosujemy we wszystkich możliwych funkcjach; na przykład nie powinno się ich używać w prostych funkcjach wymagających bardzo dużej wydajności (np. iloczyn wektorowy). Możliwe usprawnienia podanego przykładu:
  • Zastosowanie dyrektyw #ifdef / #else / #endif do warunkowej kompilacji makr GUARD/UNGUARD, dzięki czemu można będzie je łatwo wyłączyć
  • Stworzenie dodatkowej pary makr GUARD_SLOW i UNGUARD_SLOW, które będą automatycznie wyłączane w wersji wydaniowej (patrz punkt poprzedni), a które stosowane będą w funkcjach wymagających możliwie wysokiej wydajności
  • Połączenie chronienia funkcji z własnymi makrami Assert/Verify, dzięki czemu przerywając pracę programu uzyskamy dokładne informacje o miejscu naruszenia asercji i drodze do tego miejsca. Własne makra Assert/Verify opiszę w przyszłości.
  • Połączenie makra GUARD z loggerem, dzięki czemu nazwa funkcji będzie automatycznie dopisywana do komunikatu. Jest to duże udogodnienie, jeśli ktoś zapisuje nazwy funkcji do loga.
  • Przerobienie makr, by system działał w oparciu o Unicode (w przypadku kodu, który w całości jest oparty o to kodowanie znaków)
Osobiście z powodzeniem stosuje koncepcję chronienia funkcji w kodzie od dość długiego czasu. Zintegrowała się ona na stałe z silnikami i frameworkami, których pisania się podejmowałem. Z wymienionych wyżej usprawnień nie udało mi się jedynie połączyć chronienia funkcji z loggerem.