Exception Handling (Eure Konzepte, best practice,..)
-
Hallo!

Ich wurde hier im Forum mal "dezent darauf hingewiesen", dass ich zur Fehlerbehandlung mit Ausnahmen arbeiten sollte.
Seit dem habe ich mich eingelesen und schon viel probiert. Die Technik an sich ist klar.
Nun habe ich einige Fragen, wie Ihr Exceptions in Euren Projekten handhabt:
1.) Exceptions-Definitionen: Wo/wie macht Ihr diese? Alle Exception-Klassen von std::exception ableiten und in eine Header-Datei, die dann überall, wo Exceptions geworfen werden inkludiert wird? Oder bekommt jede Klasse "ihre eigene" Exception-Klasse, die in der Header-Datei der Klasse deklariert wird?
2.) Ausnahmen sollen i.d.R. geloggt werden. Wie realisiert ihr ein möglichst zentrales Logging, wenn Eure Applikation mehrere Threads benutzt?
3.) Wie geht Ihr mit den Standard-Exceptions aus der STL um? Fängt Ihr alle möglichen std::exceptions bei jeder Verwendung von streams, containern etc. um dann Eure eigenen Exceptions zu werfen. Z.B.: Eine Member-Funktion fängt intern alle möglichen Ausnahmen, um dann eine eine eigene Exception zu werfen, die z.B. die Information beinhaltet, welche Methode fehlgeschlagen ist. Oder gibt es Exceptions der STL die ihr als Unwahrscheinlich gar nicht fängt oder irgendwo außerhalb der Methode (z.B. std::ad_alloc: Ist egal wo es passiert ist, interessant wäre nur, dass es passiert ist, weil man eh nichts machen kann und jedesmal auf std::bad_alloc zu testen ist aufwendig)?
Ich bin gespannt auf Eure Antworten, da ich mir nicht sicher bin, wie ich in meinen Projekten das Exception-Handling am Besten umsetzten soll. Ich suche eine Art "Best practice" für Exception-Handling in Multithreading-Apps.
Wie ihr seht, stehe ich konzeptionell noch am Anfang, was Exception-Handling angeht.
-
#include "MyExceptions.h" void Klasse::MethodeA(){ try{ // viel STL-Code mit strings, streams, container etc... // viele potentielle std::exceptions sind hier verborgen } catch(std::out_of_range&){ throw MyExceptionClassA( "MethodeA", "Notiz zum Fehler"); } catch(std::was_so_passieren_könnte&){ throw NochEineMyExceptionClass("Notiz"); } catch(...){ throw UndefException(); //etwas vergessen? } } void Klasse::MethodeB(){ try{ // viel STL-Code mit strings, streams, container etc... // viele potentielle std::exceptions sind hier verborgen } catch(std::out_of_range&){ throw MyExceptionClassA( "MethodeB", "Notiz zum Fehler"); } catch(std::was_so_passieren_könnte&){ throw NochEineMyExceptionClass("Notiz"); } catch(...){ throw UndefException(); //etwas vergessen? } }#include "MyExceptions.h" #include "Klasse.h" ... void ZentraleKlasse::Hauptroutine(){ Klasse obj(); //wirft keine Exceptions try{ // jetzt aber... obj.MethodeA(); ... obj.MethodeB(); } catch(MyExceptionBase& e){ myLog << e; OnError(); } }
-
Die Frage ist meist zu allererst: "kann ich hier sinnvoll reagieren wenn eine Ausnahme kommt?" Denn wenn mans nicht kann reichts einfach, exceptionsicheren Code zu schreiben und die eventuell fliegenden Exceptions durchrauschen zu lassen bis zu einem Punkt, wo sie behandelt werden können.
Bei Code, wo viele verschiedene Exceptions vorkommen können macht es tatsächlich Sinn, sie in eine eigene Exception zu übersetzen, damit der aufrufende Code nicht zig verschiedene Exceptions zu behandeln hat.Die in deinem Code gezeigte Übersetzung von jeweils einer std::exception in eine andere exception halte ich für wenig sinnvoll, und vor allem der catchall ist gefährlich - wenn irgendwelche Exceptions von übergebenen Objekten kommen, dann könnte es sein dass der aufrufende Code diese Exceptions kennt, du aber nicht. Mit dem catchall übersetzt du diese exceptions aber in nichtssagende Allgemeinplätze. Catchalls sollten nur an Grenzen von Modulen auftreten, wo sich die Fehlerbehandlung ändert (z.B. Fehlercodes, andere Exception-Implementierung durch andere Compiler etc.)
-
Ich hoffe du hast folgende 3 Artikel gelesen:
http://magazin.c-plusplus.net/artikel/Exception-Handling
http://magazin.c-plusplus.net/artikel/Modernes Exception-Handling Teil 1 - Die Grundlagen
http://magazin.c-plusplus.net/artikel/Modernes Exception-Handling Teil 2 - Hinter den KulissenRoger Wilco schrieb:
1.) Exceptions-Definitionen: Wo/wie macht Ihr diese? Alle Exception-Klassen von std::exception ableiten und in eine Header-Datei, die dann überall, wo Exceptions geworfen werden inkludiert wird? Oder bekommt jede Klasse "ihre eigene" Exception-Klasse, die in der Header-Datei der Klasse deklariert wird?
Du darfst nicht Klassenweise denken. Es gibt keine string-exception oder fstream-exception. Es gibt nur Fehlertypen: OutOfMemory, FileNOtFound, FileAccessDenied, etc. Die Exception Hierachie verlaeuft also parallel zu deinerKlasen Hierachie. Und inkluden muss man immer nur das was notwendig ist.
2.) Ausnahmen sollen i.d.R. geloggt werden. Wie realisiert ihr ein möglichst zentrales Logging, wenn Eure Applikation mehrere Threads benutzt?
Exception muessen nicht geloggt werden. Es kann sinn machen gewissen Fehler zu loggen - aber prinzipiell muss nicht jede Exception geloggt werden. Und zentrales logging findet nicht an einer zentralen Stelle statt sondern ueber ein zentrales Objekt. zB ein Log-Objekt das als Singleton angelegt ist und sich um die notwendige Synchronisierung kuemmert.
3.) Wie geht Ihr mit den Standard-Exceptions aus der STL um? Fängt Ihr alle möglichen std::exceptions bei jeder Verwendung von streams, containern etc. um dann Eure eigenen Exceptions zu werfen. Z.B.: Eine Member-Funktion fängt intern alle möglichen Ausnahmen, um dann eine eine eigene Exception zu werfen, die z.B. die Information beinhaltet, welche Methode fehlgeschlagen ist. Oder gibt es Exceptions der STL die ihr als Unwahrscheinlich gar nicht fängt oder irgendwo außerhalb der Methode (z.B. std::ad_alloc: Ist egal wo es passiert ist, interessant wäre nur, dass es passiert ist, weil man eh nichts machen kann und jedesmal auf std::bad_alloc zu testen ist aufwendig)?
Du testest NIE auf exceptions.
Exception translation dagegen macht manchmal schon sinn - um dem Caller ein einheitliches Exception-Interface anzubieten. wenn man also FileNotFound wirft, waere es sinnvoll keineout_of_bounds exception durchreichen zu lassen sondern diese in eine SecurityException oder was auch immer umzuwandeln.Fehler wqernden aber an zentralen stellen behandelt und idR hast du fast keine try/catch Bloecke in deinem Code. Deshalb ist der Code von Roger Wilco auf ein absoluter Kaese.
-
Hallo pumuckl und Danke für Deine Antwort!
pumuckl schrieb:
Die Frage ist meist zu allererst: "kann ich hier sinnvoll reagieren wenn eine Ausnahme kommt?" Denn wenn mans nicht kann reichts einfach, exceptionsicheren Code zu schreiben und die eventuell fliegenden Exceptions durchrauschen zu lassen bis zu einem Punkt, wo sie behandelt werden können.
Neben diesen wichtigen Aspekt, gibt es aber auch den Aspekt: "Ist es wichtig, festzuhalten, wo der Fehler aufgetreten ist?" Wenn ich später nur weiß, da ist irgendwo ein out-of-range aufgetreten, nützt mir die Information nur wenig (z.B. beim Loging). Daher dachte ich an den Ansatz, grundsätzlich alle Exceptions zu fangen, um dann die Information "Entstehungsort" hinzuzufügen. Dann kann im Log nachvollziehn, "WAS" passiert ist und "WO" es passiert ist.
pumuckl schrieb:
Die in deinem Code gezeigte Übersetzung von jeweils einer std::exception in eine andere exception halte ich für wenig sinnvoll, ...
Mir geht es bei dem Verfahren darum, dass ich die std::exceptions auf eine eigene Exception-Klassen-Hierachie (nach Fehlerart) umsetze. In dieser kann ich dann die Informationen "WAS" und "WO" getrennt ablegen, um sie bei einer Fehlerausgabe auch getrennt und einheitlich abrufen zu können. Zusätzlich habe ich pro Exceptionklasse noch einen allgemeinen fehlertypischen Text (z.B. "Konfigurationsfehler").
pumuckl schrieb:
...und vor allem der catchall ist gefährlich - ... Catchalls sollten nur an Grenzen von Modulen auftreten, wo sich die Fehlerbehandlung ändert (z.B. Fehlercodes, andere Exception-Implementierung durch andere Compiler etc.)
Stimmt, das nutze ich bisher auch nur in solchen Fällen. In dem Beispiel hätte ich statt catch(...) catch(exceptions&) schreiben müssen.
-
Shade Of Mine schrieb:
Ich hoffe du hast folgende 3 Artikel gelesen:...
Ja, aber (noch) nicht komplett.
Shade Of Mine schrieb:
Du darfst nicht Klassenweise denken. Es gibt keine string-exception oder fstream-exception. Es gibt nur Fehlertypen: OutOfMemory, FileNOtFound, FileAccessDenied, etc. Die Exception Hierachie verlaeuft also parallel zu deinerKlasen Hierachie. Und inkluden muss man immer nur das was notwendig ist.
Ja, das denke ich auch. Nur sieht man oftmals Beispiele, wo die Exception-Deklaration mit in der Klassen-Headerdatei steht. Das liegt wahrscheinlich aber an dem "Vereinfachte aber teilweise unsinnige Code-Beispiele-Problem". Daher fragte ich, ob das gängig ist.
Also Exception-Klassenhierachie in mehrere eigene Header-Files und dann die verwendeten Exceptions inkludieren? Aus Gründen der Übersicht (oder Faulheit?) habe ich bisher alle in eine Datei getan und damit immer alle inkludiert.
Shade Of Mine schrieb:
Exception muessen nicht geloggt werden. Es kann sinn machen gewissen Fehler zu loggen - aber prinzipiell muss nicht jede Exception geloggt werden.
Gut, bei mir ist das i.d.R. so, dass ich die Loggen muss/will.

Shade Of Mine schrieb:
Und zentrales logging findet nicht an einer zentralen Stelle statt sondern ueber ein zentrales Objekt. zB ein Log-Objekt das als Singleton angelegt ist und sich um die notwendige Synchronisierung kuemmert.
Ja, da hast Du selbstverständlich recht. Streicht die Frage wieder.

Shade Of Mine schrieb:
Du testest NIE auf exceptions.
Exception translation dagegen macht manchmal schon sinn - um dem Caller ein einheitliches Exception-Interface anzubieten. wenn man also FileNotFound wirft, waere es sinnvoll keineout_of_bounds exception durchreichen zu lassen sondern diese in eine SecurityException oder was auch immer umzuwandeln.Fehler wqernden aber an zentralen stellen behandelt und idR hast du fast keine try/catch Bloecke in deinem Code. Deshalb ist der Code von Roger Wilco auf ein absoluter Kaese.
Mein Gedanke war es die Exceptions zu übersetzten, damit ich eben eine einheitliche Fehlerbehandlung und ein einheitliches Logging ermöglichen kann, die mehr und differenzierte Daten über den Fehler enthält. (s.o.) Mir ist es wichtig, mit festzuhalten, wo ein Fehler aufgetreten ist. Manchmal interessiert mich auch nicht genau welcher Fehler in einer Methode aufgetreten ist, sondern, dass ein Fehler in dieser Methode aufgetreten ist. Wie mache ich das am besten, um eben diese vielen try...catches zu vermeiden.
Aber vielen Dank für die Antworten, das hilft wirklich weiter!

-
Roger Wilco schrieb:
Hallo pumuckl und Danke für Deine Antwort!
pumuckl schrieb:
Die Frage ist meist zu allererst: "kann ich hier sinnvoll reagieren wenn eine Ausnahme kommt?" Denn wenn mans nicht kann reichts einfach, exceptionsicheren Code zu schreiben und die eventuell fliegenden Exceptions durchrauschen zu lassen bis zu einem Punkt, wo sie behandelt werden können.
Neben diesen wichtigen Aspekt, gibt es aber auch den Aspekt: "Ist es wichtig, festzuhalten, wo der Fehler aufgetreten ist?" Wenn ich später nur weiß, da ist irgendwo ein out-of-range aufgetreten, nützt mir die Information nur wenig (z.B. beim Loging). Daher dachte ich an den Ansatz, grundsätzlich alle Exceptions zu fangen, um dann die Information "Entstehungsort" hinzuzufügen. Dann kann im Log nachvollziehn, "WAS" passiert ist und "WO" es passiert ist.
An der Stelle, wo du die Exception behandelst, fängst du sie ja sowieso. Genau an der Stelle kannst du auch das Logging aufrufen und sagen "hier ist ein Problem aufgetreten". An einer tieferen Stelle einfach nur die "wo"-Info hinzuzufügen bringt dir nicht viel, denn wenn du die Exception auf einer höheren Ebene behandelst ist es nicht von Interesse, in welchem Teil der Unterroutinen sie aufgetreten ist, da du mit der Information nichts anfangen kannst (wenn es von Belang wäre ob die Exception in Unterfunktion f1 oder f2 aufgetreten ist, dann könntest du sie nach dem Aufruf von f1 bzw. f2 auch gleich behandeln).
pumuckl schrieb:
Die in deinem Code gezeigte Übersetzung von jeweils einer std::exception in eine andere exception halte ich für wenig sinnvoll, ...
Mir geht es bei dem Verfahren darum, dass ich die std::exceptions auf eine eigene Exception-Klassen-Hierachie (nach Fehlerart) umsetze. In dieser kann ich dann die Informationen "WAS" und "WO" getrennt ablegen, um sie bei einer Fehlerausgabe auch getrennt und einheitlich abrufen zu können. Zusätzlich habe ich pro Exceptionklasse noch einen allgemeinen fehlertypischen Text (z.B. "Konfigurationsfehler").
Wie gesagt, das "WO" gehört zum "WAS" dazu. Und der Typ der Exception besagt doch schon zu welchem allgemeinen Fehlertyp sie gehört, da ist der typische Fehlertext redundant. Sonst kommt am Ende an der Oberfläche eine Meldung wie "OutOfRange-Exception caught. Message: out_of_range exception (out of range)".
Was das Übersetzen angeht: wenn in deiner Routine foo eine outofrange oder filenotfound-exception auftaucht ists an höherer Stelle meist uninteressant was genau passiert ist, weil mans da dann eh kaum noch unterschiedlich behandeln kann -> eine fooFailureException reicht dann um die Fälle abzudecken.
-
Roger Wilco schrieb:
Shade Of Mine schrieb:
Du darfst nicht Klassenweise denken. Es gibt keine string-exception oder fstream-exception. Es gibt nur Fehlertypen: OutOfMemory, FileNOtFound, FileAccessDenied, etc. Die Exception Hierachie verlaeuft also parallel zu deinerKlasen Hierachie. Und inkluden muss man immer nur das was notwendig ist.
Ja, das denke ich auch. Nur sieht man oftmals Beispiele, wo die Exception-Deklaration mit in der Klassen-Headerdatei steht. Das liegt wahrscheinlich aber an dem "Vereinfachte aber teilweise unsinnige Code-Beispiele-Problem". Daher fragte ich, ob das gängig ist.
Also Exception-Klassenhierachie in mehrere eigene Header-Files und dann die verwendeten Exceptions inkludieren? Aus Gründen der Übersicht (oder Faulheit?) habe ich bisher alle in eine Datei getan und damit immer alle inkludiert.
Alle in einen Header zu packen ist unsinnig. Bei Exceptions die wirklich klassenspezifisch sind kann man sie durchaus mit in den Klassenheader packen, z.T. sogar eine Unterklasse daraus machen. In der Hinsicht muss ich Shade teilweise widersprechen: es gibt sowohl allgemeine Fehler wie die, die er angegeben hat, als auch kontextspezifische Fehler, die zu dem jeweiligen modul/der Klass gehören. Diese kontextspezifischen Exceptions gehören zum Interface der Klasse und können/sollten mit dieser zusammen geliefert werden. Sie können aber durchaus von allgemeineren Exceptions abgeleitet sein die nicht zu einer bestimmten Klasse gehören.
Beispielsweise könnte bei einer bestimmten Klasse Bar eine Funktion baz() nur mit bestimmten Werten aufgerufen werden dürfen - wird ein anderer Wert übergeben komt eine Bar::InvalidBazParamException, die abgeleitet ist von einem std::domain_error oder ähnlichem.
-
pumuckl schrieb:
An der Stelle, wo du die Exception behandelst, fängst du sie ja sowieso. Genau an der Stelle kannst du auch das Logging aufrufen und sagen "hier ist ein Problem aufgetreten". An einer tieferen Stelle einfach nur die "wo"-Info hinzuzufügen bringt dir nicht viel, denn wenn du die Exception auf einer höheren Ebene behandelst ist es nicht von Interesse, in welchem Teil der Unterroutinen sie aufgetreten ist, da du mit der Information nichts anfangen kannst (wenn es von Belang wäre ob die Exception in Unterfunktion f1 oder f2 aufgetreten ist, dann könntest du sie nach dem Aufruf von f1 bzw. f2 auch gleich behandeln).
Stimmt eigentlich. Das sagte auch schon Shade of mine, dass zentrales Logging ja nicht heißt, dass man an einer zentrale Stelle den logger aufruft. Ich war so von der Idee gefesselt, dass man mit Exceptions zentral (an einer Stelle im Code) Fehler behandeln und loggen kann. Aber wenn ich direkt nach der Entstehung logge, dann erübrigt sich die "Wo?"-Information natürlich.
pumuckl schrieb:
Was das Übersetzen angeht: wenn in deiner Routine foo eine outofrange oder filenotfound-exception auftaucht ists an höherer Stelle meist uninteressant was genau passiert ist, weil mans da dann eh kaum noch unterschiedlich behandeln kann -> eine fooFailureException reicht dann um die Fälle abzudecken.
Das wäre dann aber wieder eine Klassen-spezifische Exception, die nicht in die klassen-unabhängige Hierachie passt.
Aber setzten wir das mal beispielhaft um:
#include "MyException.h" class MethodeAException : public MyExceptionBase{...}; class MethodeBException : public MyExceptionBase{...}; void Klasse::MethodeA(){ try{ // viel STL-Code mit strings, streams, container etc... // viele potentielle std::exceptions sind hier verborgen } catch(std::exception& e){ myLog << e << "(Klasse::MethodeA)"; throw MethodeAException(); } } void Klasse::MethodeB(){ try{ // viel STL-Code mit strings, streams, container etc... // viele potentielle std::exceptions sind hier verborgen } catch(std::exception& e){ myLog << e << "(Klasse::MethodeB)"; throw MethodeBException(); } }#include "Klasse.h" ... void ZentraleKlasse::Hauptroutine(){ Klasse obj(); //wirft keine Exceptions try{ // jetzt aber... obj.MethodeA(); ... obj.MethodeB(); } catch(MyExceptionBase& e){ OnError(); } }
-
Oder mal anders gefragt:
Wie sieht bei Euch typisches Exception-Handling aus? Ein kleines Code-Beispiel wäre sehr hilfreich.
Natürlich variiert das je nach Anforderung und je nach Fall, aber ein sagen wir mal ein vereinfachter und "typischer" Fall von Exception-Handling in Euren Projekten (in dem viel mit STL gearbeitet wird z.B.).
-
Roger Wilco schrieb:
Wie sieht bei Euch typisches Exception-Handling aus? Ein kleines Code-Beispiel wäre sehr hilfreich.
In meinen Artikeln habe ich versucht einige Codes herzuzeigen.
Das Problem bei solchen Codes ist aber, du siehst nichts von den Exceptions - denn dank RAII räumst du automatisch alles auf und try/catch hat man nur an wenigen stellen wo man wirklich reagieren kann.guter exception sicherer code sieht nicht wirklich anders aus als anderer code, nur idR kürzer und klarer. der trick ist, dass man plötzlich keine fehlerbehandlung im code stehen hat.
-
Das mit dem Exception-sicheren Code ist mir auch klar (prinzipiell - Umsetzung ist ja nicht immer ganz einfach).
Aber ich habe da Problem, dass ich eigentlich grundsätzlich wissen möchte, wo ein Fehler aufgetreten ist. Das heißt für mich, dass ich eigentlich überall, wo ich Code einsetzte, der Exception werfen kann, dass ich viel try/catches habe, um einfach den Ort des Fehlers zu protokollieren.
Ich nutze sehr viel die Standard-Bibliothek und so ist mein ganzer Code voll von diesen try/catches, wobei ich die Exceptions entweder in meine Exception-Hierachie übersetzte oder einfach nur rethrow mache.
Das nervt mich ehrlich gesagt und macht den Code unübersichtlich. Daher wollte ich mal fragen, wie Ihr das so macht. Anscheinend interessiert Euch der Fehlerort aber i.d.R. nicht. Außerdem scheint Ihr i.d.R. die Exceptions der Standard-Bibliothek nicht in Eure Exception-Hierachie zu übersetzten, ist das richtig?
Exceptions fängt Ihr dort, wo Ihr reagieren könnt. Das ist klar. Aber das bedeutet, dass man dann Exceptions an solchen Stellen behandelt, wo man gar nicht weiß was genau oder wo es passiert ist. Z.B. ein bad_alloc eines std::strings oder ein ios_base::failure eines Streams irgendwo.
Mich würden ehrlich gesagt konkrete Beispiele (gerne auch nur prinzipieller Pseudo-Code) interessieren. Nicht so konstruierte main() ruft Funktion auf fängt Exception der Funktion. Das sind ja nur Beispiele, die die Funktionsweise an sich erklären, aber nicht der Einsatz in einem echten Projekt. Mein Ansatz scheint ja "falsch" zu sein. Nur wie mache ich es richtig?
-
Roger Wilco schrieb:
Das mit dem Exception-sicheren Code ist mir auch klar (prinzipiell - Umsetzung ist ja nicht immer ganz einfach).
Aber ich habe da Problem, dass ich eigentlich grundsätzlich wissen möchte, wo ein Fehler aufgetreten ist. Das heißt für mich, dass ich eigentlich überall, wo ich Code einsetzte, der Exception werfen kann, dass ich viel try/catches habe, um einfach den Ort des Fehlers zu protokollieren.
Was bringt dir das? Das Protokollieren von Fehlern direkt an Ort und Stelle ist im fertigen Programm absolut sinnfrei. Man denke nur an diese unsäglichen Windows-Fehlermeldungen "Unbekannter Fehler bei 0x056ad344 aufgetreten". Wenn im fertigen Programm etwas schief läuft ists dem Anwender herzlichst egal ob jetzt die Methode foo() oder bar() schiefgelaufen ist. Was ihn interessiert ist, dass beim Laden der Karte für sein Spiel ein Fehler aufgetreten ist.
Ich hoffe doch sehr, dass du Exceptions nicht benutzt um dein Programm während der Entwicklung zu debuggen. Dazu sind assertions da.Ich nutze sehr viel die Standard-Bibliothek und so ist mein ganzer Code voll von diesen try/catches, wobei ich die Exceptions entweder in meine Exception-Hierachie übersetzte oder einfach nur rethrow mache.
Was für zig Exceptions aus der Standardbibliothek erwartest du denn?
Außerdem scheint Ihr i.d.R. die Exceptions der Standard-Bibliothek nicht in Eure Exception-Hierachie zu übersetzten, ist das richtig?
Kommt auf die Exception an. Eine FileNotFound übersetze ich so, dass ich an der Stelle wo die entsprechende übergeordnete Routine die Datei lädt eine Fehlermeldugn ausgegeben wird, an welcher Stelle weiter unten im Programm ich den Stream versuche zu öffnen interessiert nicht.
Eine bad_alloc kann man andererseits kaum wirksam behandeln - da mache ich im Normalfall ein catch in der main, gebe noch eine Fehlermeldung aus, dass das Programm wegen Speichermangel in die Binsen gegangen ist und verlasse die Szene.Exceptions fängt Ihr dort, wo Ihr reagieren könnt. Das ist klar. Aber das bedeutet, dass man dann Exceptions an solchen Stellen behandelt, wo man gar nicht weiß was genau oder wo es passiert ist. Z.B. ein bad_alloc eines std::strings oder ein ios_base::failure eines Streams irgendwo.
Wen interessierts obs am String oder am Stream gelegen hat? In beiden Fällen kann ich nur die Schultern zucken, mir am Kopf kratzen, ggf. ein paar der Speicherfressenden Programme schließen und/oer mir zusätzlichen Speicher zulegen.
Mich würden ehrlich gesagt konkrete Beispiele (gerne auch nur prinzipieller Pseudo-Code) interessieren. Nicht so konstruierte main() ruft Funktion auf fängt Exception der Funktion. Das sind ja nur Beispiele, die die Funktionsweise an sich erklären, aber nicht der Einsatz in einem echten Projekt. Mein Ansatz scheint ja "falsch" zu sein. Nur wie mache ich es richtig?
Konkrete Beispiele siehst du oben. Dateizugriffsfehler behandle (und logge) ich dort wo ich die Datei öffne (was normalerweise ein paar Ebenen oberhalb des eigentlichen ftream::open ist). Bei Containerzugriffen gehe ich nicht davon aus dass ich Exceptions bekomme, sonst wäre die Programmlogik falsch an der Stelle. bad_alloc rauscht durch, weil da nichts mehr zu retten ist. Andere Exceptions fallen mir im Augenblick nicht ein.
-
pumuckl schrieb:
Was bringt dir das? Das Protokollieren von Fehlern direkt an Ort und Stelle ist im fertigen Programm absolut sinnfrei. Man denke nur an diese unsäglichen Windows-Fehlermeldungen "Unbekannter Fehler bei 0x056ad344 aufgetreten". Wenn im fertigen Programm etwas schief läuft ists dem Anwender herzlichst egal ob jetzt die Methode foo() oder bar() schiefgelaufen ist. Was ihn interessiert ist, dass beim Laden der Karte für sein Spiel ein Fehler aufgetreten ist.
Ich hoffe doch sehr, dass du Exceptions nicht benutzt um dein Programm während der Entwicklung zu debuggen. Dazu sind assertions da.Bei uns ist das so, dass die Software ein Log für uns hat (im fertigen Produkt aber nicht für den Kunden - zum Debuggen). Daher wollen wir wissen was (Fehlertyp) und wo (Klasse::Methode) passiert ist. Bisher läuft das über viele ifs, Returnwerte und fprintfs
Daher hatte ich jetzt mit Exceptions angefangen. Anhand der Exception war der Fehlertyp klar und in der Exception stand dann die Info Klasse::Methode. Geloggt wurde dann zentral.
Bisher wird das hier im alten C-Stil (Returnwerte, Ifs, fprintf,...) gemacht. Das versuche ich gerade 1:1 durch Exceptions zu lösen, was wohl mein falscher Ansatz ist. Aber hier kennt sich keiner mit Exceptions aus. Kommen mehr aus der C-Welt. Daher muss ich Euch löchern.Was für zig Exceptions aus der Standardbibliothek erwartest du denn?
Wenn etwas, dass ich verwende eine Exception werfen kann, muss ich sie doch (irgendwo) behandeln? Egal ob ich die nun "erwarte" oder nicht. ich erwarte eigentlich keine bad_allocs, reagiere aber trotzdem drauf. I.d.R. sind das bei mir immer 2-3 Exceptions, die auftreten könnten. Diese übersetzte ich dann in meine Exception.
an welcher Stelle weiter unten im Programm ich den Stream versuche zu öffnen interessiert nicht.
Aber wenn der Kunde dann kommt, sagt "Deine Software kann manchmal eine Datei nicht öffnen!" Wie findest Du den Fehler, woran es liegt - Angenommen es ist ein Bug in der Anwendung?
Wen interessierts obs am String oder am Stream gelegen hat?
Stimmt, das interessiert nicht. Aber der grundsätzliche Fehlertyp und der grobe Fehlerort interessiert schon.
Bei Containerzugriffen gehe ich nicht davon aus dass ich Exceptions bekomme, sonst wäre die Programmlogik falsch an der Stelle. bad_alloc rauscht durch, weil da nichts mehr zu retten ist. Andere Exceptions fallen mir im Augenblick nicht ein.
Das heißt Container-Fehler wie out_of_range behandelst Du gar nicht und lässt das Programm sich nur beenden? Was ist mit (internen/temporären) Streams? Fragst Du die Fehler-Bits ab oder nutzt Du die exceptions?
Ich möchte mich jedenfalls recht Herzlich für die Antworten und Eure Geduld bedanken!
-
Roger Wilco schrieb:
Wenn etwas, dass ich verwende eine Exception werfen kann, muss ich sie doch (irgendwo) behandeln? Egal ob ich die nun "erwarte" oder nicht. ich erwarte eigentlich keine bad_allocs, reagiere aber trotzdem drauf. I.d.R. sind das bei mir immer 2-3 Exceptions, die auftreten könnten. Diese übersetzte ich dann in meine Exception.
Du musst sie nicht dann behandeln, wenn sie auch laut Programmlogik auftauchen können, nicht wenn sie laut Interface der verwendeten Funktion auftauchen können. Wenn du z.B. durch die Programmlogik sicherstellst dass ein Containerzugriff gültig ist (z.B. index < size()), dann brauchst du keine Exception mehr abfangen.
an welcher Stelle weiter unten im Programm ich den Stream versuche zu öffnen interessiert nicht.
Aber wenn der Kunde dann kommt, sagt "Deine Software kann manchmal eine Datei nicht öffnen!" Wie findest Du den Fehler, woran es liegt - Angenommen es ist ein Bug in der Anwendung?
Indem ich das Programm debugge und den Fehler nachstelle. Und wie gesagt, ich benutze Exceptions nicht zum Debuggen.
Wen interessierts obs am String oder am Stream gelegen hat?
Stimmt, das interessiert nicht. Aber der grundsätzliche Fehlertyp und der grobe Fehlerort interessiert schon.
Der grundsätzliche Fehlertyp ist der Typ der Exception (std::bad_alloc in dem Fall), der grobe Fehlerort ist der Ort wo ich die Exception behandle (im Fall von bad_alloc ists ein programmweiter oder gar systemweiter Fehler).
Bei Containerzugriffen gehe ich nicht davon aus dass ich Exceptions bekomme, sonst wäre die Programmlogik falsch an der Stelle. bad_alloc rauscht durch, weil da nichts mehr zu retten ist. Andere Exceptions fallen mir im Augenblick nicht ein.
Das heißt Container-Fehler wie out_of_range behandelst Du gar nicht und lässt das Programm sich nur beenden?
Wenn ein out_of_range auftritt, dann tritt er auf weil ich einen Index nicht vorher auf Gültigkeit überprüft habe, wo es nötig gewesen wäre. Und dann tritt er nicht erst auf wenn die Software beim Kunden ist sondern in den Unit-Tests unmittelbar nachdem ich ihn eingebaut habe. Und dann weiß ich sehr genau wo er auftritt und wie ich ihn eingebaut hab. Wenn er erst beim Kunden auftritt dann heißt das dass meine Testabdeckung Mist ist und dann hab ich ein noch viel größeres Problem als nur festzustellen in welcher Funktion genau der Fehler aufgetreten ist.
Was ist mit (internen/temporären) Streams? Fragst Du die Fehler-Bits ab oder nutzt Du die exceptions?
Ich frag die Fehler-Bits ab, wenn es im normalen Programmverlauf zu ungültigen Streams kommen kann (z.B. bei fehlerhaften Usereingaben).
Exceptions sind wirklich Ausnahme-Fehler. Sie treten nicht am laufenden Band auf, wenn das Programm korrekt geschrieben wurde.
Wenn wie bei dir zu Debug-Zwecken die Exceptions mitgeloggt werden sollen, kann man sich überlegen im Debugmodus eine entsprechende Behandlung einuzbauen, und nur dort. Im Releasemodus wo das Programm zu 99% der Fälle ordentlich laufen sollte sind ständige try/catch Blöcke eine Performanceeinbuße, die nicht enthalten sein sollte. Schließlich ist das Programm fertig debugged und ausgeliefert, also brauchen die Exceptions nicht mehr mitgeloggt und nur selten übersetzt werden.
Die C-Style if-else-kaskaden-Fehlerlogging-Politik deiner Firma ist ein wenig out of date in C++-Code. Exzessive Fehlercodeabfragen und Funktionalitätstests über den gesamten Code verstreut sind abgelöst durch Exceptions (eben an Stelle der if-else-Kaskaden) und Unit-tests. Wenn du die C-Style "if(fehlercode1) else if (fehlercode2...)" ablöst durch ein "try catch(exception1) catch(exception2)", hast du nichts gewonnen. Exceptionhandling ist eben was anderes als Fehlercodeabfragen.
Genauso wird richtige Funktionsweise nicht mehr durch "if (hatfunktioniert)" im Code getestet sondern durch Unit-Tests, die außerhalb des Codes stehen.
Dadurch wird der Code nochmal deutlich strukturierter und lesbarer, weil er sich aufs Wesentliche (die Programmlogik) konzentriert und nicht zu 60% aus fehlerbehandlung, zu 30% aus Funktionalitätstest und zu 10% aus Programmlogik besteht, alles schön verquirlt mit einem Haufen Conditionals.
-
Ahh... danke, es wird immer heller bei mir!

Man sollte also folgendermaßen vorgehen:
Logik-Fehler/Programmfehler werden durch Debugging(z.B. Unit-Tests) beseitigt und nach dem Test als "nicht möglich" nicht weiter behandelt (z.B. out_of_range).
Exceptions nutzt man für Fehler, die man beim Debuggen in Unit-Test etc. nicht finden kann oder man nicht kontrollieren kann. Also eher Laufzeitfehler wie, Dateizugriffsfehler, fehlender Speicher, missglückte Win-API-Aufrufe z.B..
Exceptions werden dort gefangen, wo man entsprechend Handeln kann (nochmal Probieren, Alternative einschlagen oder sinnvollen Fehler generieren (wie "Datei konnte nicht geöffnet werden!" -Warum genau ist nicht relevant) ).
-
Roger Wilco schrieb:
Exceptions werden dort gefangen, wo man entsprechend Handeln kann (nochmal Probieren, Alternative einschlagen oder sinnvollen Fehler generieren (wie "Datei konnte nicht geöffnet werden!" -Warum genau ist nicht relevant) ).
Warum genau steht mit etwas Glück sogar in der Exception (std::exception::what()) - allerdings nicht immer, manche Implementierungen sind da mit Infos eher knauserig.
Sonst denke ich ists hell genug geworden

-
Dann eine Frage zum Schnittstellen-Design bezüglich Exceptions:
So wie ich das Verstanden habe, lasst Ihr die Methoden Eurer Klassen prinzipiell sowohl std::exceptions (oder lässt Sie durch) als auch eigene Exceptions werfen, richtig?
-
Roger Wilco schrieb:
So wie ich das Verstanden habe, lasst Ihr die Methoden Eurer Klassen prinzipiell sowohl std::exceptions (oder lässt Sie durch) als auch eigene Exceptions werfen, richtig?
Es fliegen quasi nie std::exceptions. Mir fällt jetzt ausser at() keine methode ein wo eine exception fliegt.
aber natürlich kann es sinn machen solche exceptions in higher level exceptions umzuwandeln.
was das loggen betrifft: du kannst uU uncaught_exception() in einem dtor verwenden um exceptions zu loggen. stackguards können für sowas ganz gut verwendet werden.
oder natürlich du kannst __FILE__ und __LINE__ ja an die exception übergeben. ganz selten sieht man auch unwind-code ala:
#define STACKGUARD_START try { #define STACKGUARD_END } catch(tracable_exception& e) { e.add_trace(__FILE__, __LINE__); throw; } void foo() { STACKGUARD_START; throw bar(); STACKGUARD_END; }
-
Shade Of Mine schrieb:
Es fliegen quasi nie std::exceptions. Mir fällt jetzt ausser at() keine methode ein wo eine exception fliegt.
Stimmt eigentlich, wenn man die std::bad_alloc weg lässt, weil man da eh nichts tun kann sind es eigentlich nur noch std::out_of_range und das theoretische std::lenght_error.
Shade Of Mine schrieb:
aber natürlich kann es sinn machen solche exceptions in higher level exceptions umzuwandeln.
Ja, wie das Beispiel Dateioperation. Aber grundsätzlich fängt man nicht alle std::esceptions und wirft was eigenes, nur um eine einheitliche oder std-freie Schnittstelle bezüglich Exceptions zu besitzen, oder? Damit gehören dann z.B. std::bad_alloc oder std::lenght_error mit zur Schnittstelle der eigenen Klassen.