Wann exceptions werfen?



  • Hallo Leute,

    habe mal eine Frage zu Exceptions. Wann und wie oft sollte man diese werfen? Ich hatte vor ein paar Tagen meinen Code angepasst und alle Stellen, an denen ich zuvor klassisch Fehlercodes gesetzt habe, durch das Werfen von exceptions ersetzt. Soweit so gut. In den höhrern Funktionsebenen äußern sich jetzt allerdings Probleme, da jede Subroutine potentiell exceptions werfen kann. Das heißt, ich müsste jetzt jede einzelne Zeile mit try{}-catch{} behandeln.
    ...so gesehen habe ich trotz guten Willen da etwas übertrieben.

    ...Aber wie regelt man soetwas am besten?

    Ich denke soetwas ist nicht das Gelbe vom Ei:

    ...
    try {Subfunktion_1();} catch(...){}
    try {Subfunktion_2();} catch(...){}
    try {Subfunktion_3();} catch(...){}
    ...
    

    Kann man in C++ eventuell exceptions für bestimmte Funktionsaufrufe ignorieren? Müsste ich eventuell einen Ausgleich durch Rückgabewerte schaffen, um die Anzahl der verwendeten exceptions zu reduzieren?

    viele Grüße,
    SBond



  • Eine Exception ist eine Ausnahme. Wenn also der Zugriff auf eine Datei fehlschlägt, dann ist das i.d.R. kein Grund für eine Exception sondern das potentiell erwartete Verhalten, etc.



  • was ist in C++ die beste Möglichkeit einfache Fehler zu behandeln? Möglicherweise könnte ich in diesen Fällen meine Exception-Klasse mit dem aufgetretenen Fehler als Rückgabewert senden. Die Hauptfunktion, welche die Subfunktion aufruft, könnte dann optional auf Fehler prüfen.
    ...allerdings habe ich sowas noch nie in einem Quellcode gesehen. ....ich weiß auch gar nicht, ob das so funktioniert. ...werde ich mal testen.





  • Kannst du das Video auch etwas kommentieren, lohnt sich das?



  • Mechanics schrieb:

    Kannst du das Video auch etwas kommentieren, lohnt sich das?

    Das Video ist ein sehr guter Talk, der exakt die hier angesprochenen Fragen beantwortet...



  • Danke für die Antworten. Ich schaue mir das Video gleich mal an 🙂

    ...habe meine Aussage mal getestet und es würde schonmal funktionieren.

    MyException subFunction()
    	{
    		...
    		return MyException (MyException::MEIN_FEHLERCODE);
    	}
    

    Aufruf:

    // Methode 1
    	MyException Status = subFunction();
    	if (Status.getError() == MyException::MEIN_FEHLERCODE)
    	{
    		...oh nein Fehler...
    	}
    
    	// Methode 2
    	if(subFunction().getError() == MyException::MEIN_FEHLERCODE)
    	{
    		...oh nein Fehler...
    	}
    


  • Ich seh jetzt nicht den Sinn deiner Konstruktion? Wo ist der Unterschied zu Fehlercodes außer, dass es jetzt ne Klasse ist die Exception im Namen hat? Vielleicht möchtest du so etwas wie Expected<T> wie Andrei Alexandrescu hier (ebenfalls ein sehr guter Vortrag) vorstellt? Kurz gesagt geht es dabei um eine Klasse die entweder einen Rückgabewert speichert oder eine Exception. Dabei kann man sich dann aussuchen ob man den Fehler explizit behandelt oder einfach versucht den Wert zu lesen. Falls kein Wert vorliegt sondern eine Exception dann wird diese geworfen, sodass man das testen auf einen Fehler nicht vergessen kann.

    Um nochmal grob auf deine Ausgangsfrage zurück zu kommen: Natürlich schreibt man nicht an jeden Funktionsaufruf try{}catch() dran. Eher sollte man versuchen auf einer möglichst hohen Ebene den Fehler zu behandeln.



  • vielen Dank 🙂

    ...ich habe mir die Videos mal angeschaut. Insbesondere im ersten Video zielt es auf das RAII Idiom. Für mich als C++ Anfänger ist es alles noch etwas schwer zu fassen, auch wenn ich den Zweck dahinter sehe. Hätte nicht gedacht, dass eine Fehlerbehandlung so komplex wird. Aus meinen alten C-Zeiten kannte ich nur Fehlercodes als Rückgabewerte oder eine globale Statusvariable. Alles war einfach, auch wenn man natürlich Fehler ignorieren konnte.

    Exceptions sind schon eine feine Sache, aber ich finde sie machen programmieren wesentlich schwerer und teilweise unübersichtlicher. Gerade in meinem Fall, wo nahezu jede Zeile eine Exception werfen könnte ist es ungünstig. ....was wohl auch mein Verschulden ist. Es wäre praktisch, wenn man nach dem Fangen einer Exception, welche man für unkritisch hält, wieder zurück in den Programmfluss zurückspringen könnte. Aber wahrscheinlich sehe ich das zu oberflächlich.

    Eventuell ist es sinnvoll Fehlercodes für kleine Funktionen einzusetzen, deren Versagen keine schweren Auswirkungen hat. ...Und exceptions bei Ausnahmefehlern. Nur ist hier wieder die Frage, in welchem Umfang.

    Wie geht Ihr dieses Thema an, wenn Ihr programmiert? Nutzt Ihr ausschließlich exceptions oder eine Kombination aus Fehlercodes und Exceptions. ....oder doch noch was ganz anderes?

    ...ich möchte es natülich gleich richtig machen, auch wenn es keine perfekte Lösung sein muss, da ich meine C++-Kenntnisse erst aufbauen muss. Nur hinterher den kompletten Quellcode anzupassen ist immer etwas....ungünstig.

    viele Grüße,
    SBond



  • SBond schrieb:

    Gerade in meinem Fall, wo nahezu jede Zeile eine Exception werfen könnte ist es ungünstig.

    Wieso das? Mit Fehlercodes ist es doch noch viel ungünstiger, weil man jede Funktion separat checken muss und den Fehler an die nächst höhere Funktion weitergeben. Der Vorteil von Exceptions ist ja, dass diese automatisch zu den nächst höheren Funktionen weiter gegeben werden bis ein try/catch Block gefunden wird. Nehmen wir mal das String to Int Beispiel aus dem Vortrag von Andrei. Angenommen du ließt eine Datei mit einem komplizierten Dateiformat ein, wo du sehr häufig diese Konvertierung brauchst. Dann hätte man einen try/catch Block um den gesamten Lesevorgang, da eine fehlerhafte Konvertierung dazu führt das die Datei überhaupt gar nicht vernünftig eingelesen werden kann.

    SBond schrieb:

    Es wäre praktisch, wenn man nach dem Fangen einer Exception, welche man für unkritisch hält, wieder zurück in den Programmfluss zurückspringen könnte. Aber wahrscheinlich sehe ich das zu oberflächlich.

    Was für eine Exception soll das sein? Man sollte Exceptions schon für echte Fehler verwenden, welche man nicht einfach mal ignorieren kann. Ich gebe aber zu, dass die Abgrenzung wann man eine Exception benutzt und wann besser nicht, nicht ganz klar ist.



  • Wieso wird diese Frage eigentlich nicht wie in Java beantwortet?
    Oder umgekehrt, wieso macht man es in Java einfach so und wirft bei jeder Kleinigkeit eine Exception?

    (Das ist keine Wertung für/gegen Java/C++, sondern eifnach eine Frage! Und vor allem soll kein Flamewar oder Shitstorm gestartet werden)



  • Skym0sh0 schrieb:

    Wieso wird diese Frage eigentlich nicht wie in Java beantwortet?
    Oder umgekehrt, wieso macht man es in Java einfach so und wirft bei jeder Kleinigkeit eine Exception?

    (Das ist keine Wertung für/gegen Java/C++, sondern eifnach eine Frage! Und vor allem soll kein Flamewar oder Shitstorm gestartet werden)

    Wo wirft man in Java eine Exception, wo man in C++ keine wirft?



  • Naja, Java wirft doch bei allem sofort.

    Nullpointer-Exception, IOException, FileNotFoundException, AssertionError, OutOfMemoryError, etc...



  • sebi707 schrieb:

    Mit Fehlercodes ist es doch noch viel ungünstiger, weil man jede Funktion separat checken muss und den Fehler an die nächst höhere Funktion weitergeben. Der Vorteil von Exceptions ist ja, dass diese automatisch zu den nächst höheren Funktionen weiter gegeben werden bis ein try/catch Block gefunden wird.

    Ich versuche mal mein Problem besser zu umschreiben. Folgender code:

    // ... sonstiger code ...
    Kopierfunktion_1(dest1, source1);
    Kopierfunktion_2(dest2, source2);
    Kopierfunktion_3(dest3, source3);
    // ... sonstiger code ...
    

    Diese beispielhaften Kopierfunktionen werden in einer Methode hintereinander aufgerufen. Jede Funktion kann exceptions werfen, wenn z.B. kein Speicher alloziert werden kann oder der gegebene Parameter 'source' leer ist. Alle Funktionen sind unabhängig und dürfen Fehlschlagen, wenn der 'source'-Parameter leer ist. Um mögliche exceptions abzufangen, könnte man nun folgendes machen:

    // ... sonstiger code ...
    try
    {
    	Kopierfunktion_1(dest1, source1);
    	Kopierfunktion_2(dest2, source2);
    	Kopierfunktion_3(dest3, source3);
    }
    catch(const MyException &e)
    {
    	int errorCode = e.getError();
    
    	if (errorCode != e.errorCodes::NO_DATA_AVAILABLE) // Fehler ignorieren, wenn keine Daten zum kopieren vorhanden sind.  ...ansonsten werfen.
    	{
    		throw;
    	}
    }
    // ... sonstiger code ...
    

    Der Catch-Block prüft, ob es ein kritischer Fehler ist und wirft ggf. eine exception. Sind nur keine Daten zum kopieren vorhanden, so wird der Fehler ignoriert. Problem ist, wenn die erste Kopierfunktion fehlschlägt, dann kommen die anderen beiden Zeilen nicht mehr zur Ausführung. Um das zu umgehen, könnte man nun das tun:

    // ... sonstiger code ...
    try
    {
    	Kopierfunktion_1(dest1, source1);
    }
    catch(const MyException &e)
    {
    	int errorCode = e.getError();
    
    	if (errorCode != e.errorCodes::NO_DATA_AVAILABLE) // Fehler ignorieren, wenn keine Daten zum kopieren vorhanden sind.  ...ansonsten werfen.
    	{
    		throw;
    	}
    }
    
    try
    {
    	Kopierfunktion_2(dest2, source2);
    }
    catch(const MyException &e)
    {
    	int errorCode = e.getError();
    
    	if (errorCode != e.errorCodes::NO_DATA_AVAILABLE) // Fehler ignorieren, wenn keine Daten zum kopieren vorhanden sind.  ...ansonsten werfen.
    	{
    		throw;
    	}
    }
    
    try
    {
    	Kopierfunktion_3(dest3, source3);
    }
    catch(const MyException &e)
    {
    	int errorCode = e.getError();
    
    	if (errorCode != e.errorCodes::NO_DATA_AVAILABLE) // Fehler ignorieren, wenn keine Daten zum kopieren vorhanden sind.  ...ansonsten werfen.
    	{
    		throw;
    	}
    }
    // ... sonstiger code ...
    

    Ja... das geht, aber das sieht irgendwie schrecklich aus. Würde ich Fehlercodes für unkritische Fehler und exceptions für kritische Fehler nehmen, dann sieht es schon besser aus:

    try
    {
    	// ... sonstiger code ...
    	if (Kopierfunktion_1(dest1, source1) == -1)
    	{
    		// unkritischer Fehler: naja nichts schlimmes und ggf. loggen (z.B. source ist leer und hat nichts zum Kopieren). Fatale Fehler würden Exceptions werfen und keinen Fehlercode zurückgeben
    	}
    	if (Kopierfunktion_2(dest2, source2) == -1)
    	{
    		//...
    	}
    	if (Kopierfunktion_3(dest3, source3) == -1)
    	{
    		//...
    	}
    	// ... sonstiger code ...
    }
    catch(const MyException &e)
    {
    	// irgendein kritischer Fehler
    }
    

    ist die letzte Variante ein schlechter Programmierstil? Ich lese immer wieder in Foren oder Tutorials, dass man Exceptions nur für Ausnahmen verwenden soll. Das halte ich auch für sinnvoll. Nur ist die frage, was man bei banalen (informativen) Fehlern/Warnungen macht. Wenn man hier keine Exception werfen sollte, muss es einen anderen Weg geben diese Fehler (optional) zu behandeln oder zu Prüfen.

    ....gut in meinem Beispiel könnte ich eine Methode schreiben (z.B. isSourceOccupied(source)), die vor dem Kopieren erstmal prüft, ob 'source' Daten enthält und einen bool zurückgibt, aber man muss es sich ja nicht unnötig komplizierter machen. ...bzw möglicherweise gibt es Fälle, in denem man das nicht so einfach prüfen kann.

    Was würdet Ihr machen? Eines von meinen Beispielcodes anwenden oder doch was ganz anderes?



  • Skym0sh0 schrieb:

    Wieso wird diese Frage eigentlich nicht wie in Java beantwortet?
    Oder umgekehrt, wieso macht man es in Java einfach so und wirft bei jeder Kleinigkeit eine Exception?

    Weil Java ursprünglich für ganz andere Ziele gemacht wurde: Damit extrem schlechte unausgebildete Progger für embedded systems extrem schlechte billige Programme schreiben. Türlich baut man dafür einen GC ein und versucht, Programmierfehler zu fangen.

    Skym0sh0 schrieb:

    Und vor allem soll kein Flamewar oder Shitstorm gestartet werden)

    Wenn Du die Antwort nicht verträgst, dann frag nicht. Man kann Java nicht ohne dessen Geschichte verstehen. https://en.wikipedia.org/wiki/Oak_(programming_language)



  • Im gezeigen Beispiel macht eine lokale Behandlung der Fehler ja auch Sinn (zumindest falls man mit nur einem Teil der Kopierten Dateien was anfangen kann). Da wäre ich auch eher für Rückgabewerte. Man rechnet bei sowas wie Datei kopieren ja schon fast mit Fehlern. fstream::open wirft auch keine Exception wenn es die Datei nicht öffnen kann.



  • es ist scheinbar nicht so trivial ein geeignetes Mittelmaß zu finden.



  • SBond schrieb:

    es ist scheinbar nicht so trivial ein geeignetes Mittelmaß zu finden.

    Das ist eben, wie ich finde, der Punkt. Die einen sagen "Keine Exceptions in C++ nutzen, weil langsam, weil unübersichtlich, weil Fehleranfällig, weil weil weil..." und die anderen sagen "Für alles Exceptions nutzen und werfen".

    Und dann sind da Praxisbeispiele, wo manchmal was geworfen wird und manchmal nicht.
    Dafür werden dann Assertions im Programm eingebaut, wo dann hier und da mal (im Production Betrieb ?!) das Programm einfach aussteigt ohne Fehlermeldung, ohne Stacktrace ohne alles.

    Ich blick da nicht mehr durch 😞
    Muss ich wohl zu Java rübergehen... :|



  • SBond schrieb:

    es ist scheinbar nicht so trivial ein geeignetes Mittelmaß zu finden.

    Das Video mit Expected<T> hast du gesehen? Ich habe es selbst noch nicht benutzt aber ich finde es sehr praktisch, dass dort beide Ansätze vereint werden. Wenn man möchte nutzt man den Rückgabewert und behandelt den Fehler lokal. Wenn nicht, dann wirft man eben eine Exception und lässt den Fehler zu höheren Funktionen propagieren. Vermutlich kann man die Idee auch für Funktionen ohne Rückgabewert, wie in deinem Beispiel, erweitern.



  • Skym0sh0 schrieb:

    Wieso wird diese Frage eigentlich nicht wie in Java beantwortet?
    Oder umgekehrt, wieso macht man es in Java einfach so und wirft bei jeder Kleinigkeit eine Exception?

    (Das ist keine Wertung für/gegen Java/C++, sondern eifnach eine Frage! Und vor allem soll kein Flamewar oder Shitstorm gestartet werden)

    Weil in C++ schnell alles explodiert mit RAII und Exceptions. Praktisch darf nichts was im Destruktor an Aufräumarbeiten gemacht wird eine Exception aus dem Destruktor werfen, weil sonst im Falle einer anderen Exception alles crasht.


Anmelden zum Antworten