Axel Rogat
Objektorientiertes Programmieren mit C++ und JAVA
 
19.3: new-Handler Kapitel 19 20: STL 
 
  19.4 Typen der Fehlerobjekte  
 

Auch wenn man mit int bzw. enum-Typen relativ differenziert Fehler behandeln kann, sollte man fast immer spezielle Klassen für Exceptions definieren. Beim Zusammenspiel mehrerer, insbesondere fremder Klassen kommt es sonst sofort zu Überschneidungen.

Beispiel: In unserem Stack-Beispiel konnten drei Arten von Fehlern auftreten: Kein Speicher mehr für das Eintragen eines neuen Objekts, und Anwenden der Operationen top bzw. pop auf einen leeren Stack.

Es ist offensichtlich, daß im ersten Fall eine völlig andere Art von Fehlerbehandlung nötig ist als in den beiden anderen. Wenn nicht mehr genügend Speicher zur Verfügung steht, kann die größere Operation, die den Stack benutzt, eventuell nicht durchgeführt werden, oder es müssen Maßnahmen eingeleitet werden, Speicher wieder zu räumen. Die beiden anderen Fälle sind eventuell absichtlich nicht im normalen Programm abgefangen worden, um nicht durch zu viele Abfragen ineffizient zu werden, oder es handelt sich um einen Programmierfehler. Eine Fehlermeldung mit anschließendem Abbruch ist jedenfalls hier kein allgemeines Rezept.

class StackException { public: enum error { pop_empty, top_empty, out_of_memory }; const char *const message() { return errmsg[(int)code]; } StackException(error c) : code(c) { } private: error code; static char *errmsg[]; }; char* StackException::errmsg[]= { "pop with empty stack", "top with empty stack", "out of memory" };
Wir definieren einen neuen Fehlertyp classStackException. Als einzigen Datenmember enthält die Klasse einen Fehlercode code. Er steht deshalb nicht im Innern des stack-Templates, da seine Funktionsweise nicht von dem Typ abhängt, mit dem der Stack arbeitet. Es ist überflüssig, für jeden Stack-Typ Code für eine eigene StackException-Klasse zu erzeugen.

In StackException stellen wir drei Fehlercodes durch einen enum-Typ error zur Verfügung. Die Fehlermeldungen im Klartext sind static-Member der Klasse.

Es gibt zwei Funktionsmember, nämlich einen Konstruktor, der code initialisiert, und message, die die passende Fehlermeldung zurückgibt.

Das Erkennen von Speichermangel verlegen wir in die Klasse für die Knoten der zugrundeliegenden Liste. Statt einer Schleife im Destruktor von Stack arbeiten wir hier der Abwechslung halber mit Rekursion im Destruktor der Knoten. Die entsprechend angepaßten Klassen sehen wie folgt aus:

template <class T> class stack_node { public: T data; stack_node<T> *next; stack_node(T d, stack_node<T> *n) : data(d), next(n) { } ~stack_node() { if (next!=0) delete next; } void* operator new(size_t bytes) throw(StackException) { void *p=::new char[bytes]; if (p==0) throw StackException(StackException::out_of_memory); return p; } }; template <class T> class stack { private: stack_node<T> *first; public: stack() : first(0) throw() { } ~stack() { if (first!=0) delete first; } bool isempty() const throw() { return first==0; } stack<T>& push(const T &t) throw(StackException) { first=new stack_node<T>(t,first); return *this; } stack<T>& pop() throw(StackException) { if (isempty()) throw StackException(StackException::pop_empty); first=first->next; return *this; } T top() const throw(StackException) { if (isempty()) throw StackException(StackException::pop_empty); return first->data; } };
Folgendes Beispielprogramm wirft eine Exception und gibt die Fehlermeldung aus:
int main() { try { stack<int> s; s.push(1).push(2).push(3); cout << s.pop().pop().top() << endl; // liefert "1" cout << s.pop().top(); } catch (StackException &e) { cerr << "StackException: " << e.message() << endl; } }
In den Fehlerobjekten selbst kann auch sehr gut eine detailliertere Fehlermeldung produziert werden. Man profitiert dort davon, daß in der Fehlerklasse meist ein direkterer Zugriff auf die auslösende Klasse besteht.

Beispiel: Mit folgender Klasse kann man sinnvolle Exceptions in unsere Array-Templates einbauen:

class ArrayException { private: const char *const errmsg; const int value; public: ArrayException(const char *e, int v) : errmsg(e), value(v) { } friend ostream& operator << (ostream &o, const ArrayException &e) { return o << "ArrayException: " << e.errmsg << '(' << e.value << ')'; } };
Statt den Umweg über message zu gehen, können wir jetzt die Exception-Objekte direkt ausgeben. Sie liefern dann eine Fehlermeldung, die z.B. den fehlerhaften Index bei [] oder die unzulässige Array-Größe (0, negativ) im Konstruktor mit ausgibt.

Die relevanten Stellen von array und checked_array sind im folgenden aufgeführt:

template <class T> void array<T> :: allocate() throw(ArrayException) { if (num_elem<=0) throw ArrayException("illegal size",num_elem); data=new T[num_elem]; if (data==0) throw ArrayException("out of memory",num_elem); } template <class T> T& checked_array<T> :: operator [] (int i) const throw(ArrayException) { if (i<lower || i>upper) throw ArrayException("illegal index",i); return array<T>::operator[](i); }
Man kann auch eine konstante Referenz auf das betroffene Objekt (hier unser Array) in die Exception aufnehmen und bei der Fehlermeldung alle benötigten Informationen daraus herauslesen.

Gleichartige Fehler können meist an vielen verschiedenen Stellen des Benutzerprogramms auftreten. Es macht keinen Sinn, überall dort ähnlichen Code zu reproduzieren. Statt dessen kann man geschickterweise die Fehlerbehandlung in die Klasse des Fehlerobjekts einbauen, etwa in der Form einer Methode handle(). Bei einem allgemeinen Index-Fehler bietet sich allerdings keine Reparatur-Methode an.

 
19.3: new-Handler Startseite 20: STL 
 

© 1998 Axel Rogat