Wann Exception bzw. direkte Fehlerbehandlung?



  • Moin zusammen,

    ich habe eine Frage bezüglich Exceptions.
    Mir ist momentan noch nicht ganz klar, wann man Exceptions verwendet, oder wann man Fehler direkt in einer Funktion löst.
    In meinem Buch wird empfohlen, immer Fehler in einer Funktion ausfindig zu machen, die Behandlung aber der aufrufenden funktion zu überlassen.
    Hört sich für mich auch ganz logisch an und hab mich daher auch daran gehalten.
    Doch ist dies immer sinnvoll?

    Denn momentan schreibe ich mir ein kleines Programm, dass berechnet, wie viel Insulin man pro BE (Broteinheit) spritzen muss.
    Es werden anfangs drei double Variablen mittels einer Funktion initialisiert, die anschließend im ganzen Programm genutzt werden.
    Jeder dieser Variablen muss einen Wert enthalten der größer als 0.1 ist.
    Ansonsten wird eine Ausnahme geworfen, die einen darauf hinweist. Außerdem gibt es noch eine Ausnahme, wenn der Benutzer versucht, einen Buchstaben oder ein anderes Zeichen in einen double einzulesen.

    #include <iostream>
    
    using namespace std;
    
    void init(double &a, double &b, double &c); //initialisiert die Variablen, die zur Berechnung der Insulineinheiten nötig sind.
    
    int main()
    {
    	double mo = 0, mi = 0, ab = 0;
    	try
    	{
    		if (!mo || mi || ab)
    			init(mo, mi, ab);
    	}
    	catch (runtime_error&e)
    	{
    		cerr << "Fehler!\n" << e.what() << endl;
    		cin.clear();
    		cin.ignore();
    	}
    	system("pause");
    	return 0;
    }
    
    void init(double &a, double &b, double &c)
    {
    	cout << "Geben Sie die Einheiten ein, die zur entsprechenden Tageszeit, pro BE gespritzt werden.\n";
    	cout << "Morgens: ";
    	cin >> a;
    	cout << "\nMittags: ";
    	cin >> b;
    	cout << "\nAbends: ";
    	cin >> c;
    	cout << endl;
    
    	//Fehlerüberprüfung
    	if (!cin)
    		throw runtime_error("Einer oder mehrere Werte bzw. Zeichen sind nicht zulässig.");
    	if (a <= 0.1 || b <= 0.1 || c <= 0.1)
    		throw runtime_error("Der von Ihnen eingegebene Wert ist zu niedrig");
    }
    

    Nur wenn die Exception von Main gefangen wird, ist das Programm zuende.
    Wie kann ich es anstellen, dass der try Block nach falschen Eingaben wiederholt ausgeführt wird?

    Dies war meine erste Idee.
    Nur hätte ich mich dann nicht daran gehalten, den Fehler außerhalb der Funktion zu behandeln:

    #include <iostream>
    
    using namespace std;
    
    void init(double &a, double &b, double &c); //initialisiert die Variablen, die zur Berechnung der Insulineinheiten nötig sind.
    
    int main()
    {
    	double mo = 0, mi = 0, ab = 0;
    		if (!mo || mi || ab)
    			init(mo, mi, ab);
    	system("pause");
    	return 0;
    }
    
    void init(double &a, double &b, double &c)
    {
    	cout << "Geben Sie die Einheiten ein, die zur entsprechenden Tageszeit, pro BE gespritzt werden.\n";
    	cout << "Morgens: ";
    	cin >> a;
    	cout << "\nMittags: ";
    	cin >> b;
    	cout << "\nAbends: ";
    	cin >> c;
    	cout << endl;
    
    	//Fehlerüberprüfung
    	if (!cin)
    {
    cerr << "Fehler\n Es sind nur Zahlen als Eingabe zulässig.\n";
    cin.clear();
    cin.ignore();
    init(mo, mi, ab);
    }
    	if (a <= 0.1 || b <= 0.1 || c <= 0.1)
    {
    cerr << "Fehler!\n Die Werte müssen größer als 0.1 sein.\n";
    init(mo, mi, ab);
    }
    }
    

    Bin für jeden Tipp dankbar.



  • Wenn du ein Problem an einer Stelle lösen kannst, löse es dort, ansonsten werfe eine Exception. Prinzipiell ist deine Lösung in der init-Funktion an der richtigen Stelle, eine Schleife ist aber sicher besser als ein rekursiver Aufruf der Funktion.



  • Hallo Manni,

    dankd ir für die schnelle Antwort. Ich habe die Rekursion jetzt rausgenommen und das ganze so geregelt:

    void init(vector<double> &v)
    {
    	cout << "Geben Sie die Einheiten ein, die zur entsprechenden Tageszeit, pro BE gespritzt werden.\n";
    	do
    	{
    		cout << "Morgens: ";
    		cin >> v[0];
    		cout << "\nMittags: ";
    		cin >> v[1];
    		cout << "\nAbends: ";
    		cin >> v[2];
    		cout << endl;
    
    		//Fehlerüberprüfung
    		if (!cin)
    		{
    			cerr << "Es sind nur Zahlen als Eingabe erlaubt.\n";
    			cin.clear();
    			cin.ignore();
    		}
    		else if (v[0] <= 0.1 || v[1] <= 0.1 || v[2] <= 0.1)
    			cerr << "Alle eingegebenen Werte müssen größer als 0.1 sein.\n";
    	} while (v[0] <= 0.1 || v[1] <= 0.1 || v[2] <= 0.1);
    	}
    

    Funktioniert auf jeden Fall so wie ich es wollte.



  • Alles was mit direkten Benutzereingaben zu tun hat ohne Exceptions lösen.



  • faustregel schrieb:

    Alles was mit direkten Benutzereingaben zu tun hat ohne Exceptions lösen.

    Geht schon mal in allen Fällen nicht, wo du Objekte konstruierst, die direkt von Benutzereingaben abhängen.

    Beispiel: Datumsklasse. Willst du sie nur gültige Daten speichern lassen, musst du im Konstruktor bei einem Fehler eine Exception werfen. Das Datum vor dem Konstruktor ausserhalb zu prüfen macht keinen Sinn, da dazu ein beträchtlicher Teil der Implementierung aus der Klasse notwendig ist. Und Zombie-Objekte sind auch nicht unbedingt schön...



  • faustregel schrieb:

    Alles was mit direkten Benutzereingaben zu tun hat ohne Exceptions loesen.

    und wann sollte man man Exceptions einsetzten?



  • Nexus schrieb:

    Beispiel: Datumsklasse.

    Darum auch Faustregel.

    Wobei ich das mit Exceptions als unschön empfinde, aber es in C++ nicht viel besser geht. Haskell hätte Maybe DateTime, in C++ wäre das ein statischer Konstruktor mit expect<DateTime>. Würde ich persönlich so lösen, aber Exceptions sind da schon gangbar. Alternativ das Nullobject, Tag=Monat=Jahr=0.



  • Beispiel: Datumsklasse

    Zu pauschal. Format der Eingaben kann ausserhalb geprueft werden, genauso wie spezifizierte Vorbedingungen. Warum kann eine check-Funktion nicht ohne ein gueltiges Objekt auskommen?

    musst

    Nur sterben muss man man. Ansonsten fuehren viele Wege nach Rom. Drunter sind gibt es lang und beschwerliche neben leichten und angenehmen. Behauptung/Provokation: Fuer alle Probleme gibt es immer einen leichten und angenehmen Weg ohne Exceptions.

    und wann sollte man man Exceptions einsetzten?

    Ich setze Exceptions bei der Kommunikation mit "externen" Geraeten ein, die beispielsweise ueber USB oder Seriell angeschlossen ist. Was tun, wenn plaetzlich jemand drauf tritt.



  • knivil schrieb:

    Nur sterben muss man man. Ansonsten fuehren viele Wege nach Rom. Drunter sind gibt es lang und beschwerliche neben leichten und angenehmen.

    Sehr weise 🙂
    Aber es freut mich immer, wenn Leute einzelne Worte auseinandernehmen, statt die Idee zu verstehen versuchen.

    knivil schrieb:

    Behauptung/Provokation: Fuer alle Probleme gibt es immer einen leichten und angenehmen Weg ohne Exceptions.

    Lies mal Shade Of Mines Artikel zu modernem Exception-Handling. Die Diskussion haben wir schon zu oft geführt, als dass ich sie noch einmal beginnen würde.



  • Nexus schrieb:

    Lies mal Shade Of Mines Artikel zu modernem Exception-Handling. Die Diskussion haben wir schon zu oft geführt, als dass ich sie noch einmal beginnen würde.

    Link? Find ich jetzt auch nicht uninteressant.



  • Nexus schrieb:

    Aber es freut mich immer, wenn Leute einzelne Worte auseinandernehmen, statt die Idee zu verstehen versuchen.

    Und mich nervt es nur, wenn Meinungen als Gott gegebene Wahrheit dargestellt werden.

    Lies mal Shade Of Mines Artikel zu modernem Exception-Handling.

    Habe ich und mehr. Bitte stelle mich nicht als ... unerfahren dar.

    Ansonsten: http://magazin.c-plusplus.net/artikel/Modernes Exception-Handling Teil 1 - Die Grundlagen







  • Exceptions nimmt man bei außergewöhnlich fehlerträchtigen Fehlern.



  • Danke für die Tipps. Und danke Faustregel, für deine Faustregel. 😢
    Also wenn ich das hier so lese, ist die Realität in Sachen Nutzereingaben, wohl ein Mittelweg. Ich schätze mal, dass es besonders dort wichtig ist, wo Parameter durch mehrere Funktionen nacheinander geschleust werden, um mit ihnen zu rechnen, oder?
    Dort hat man dann als Nutzer ja keinen direkten Einfluss.



  • Nutz Exceptions auf jeden Fall für echte Ausnahmen: Nicht mehr genug Speicher, Window, Thread, etc. konnte nicht erstellt werden, OpenGL Kontext nciht geladen, etc.
    Dann ist es eine Streitfrage, ob du Exceptions für Dinge nutzt wie beispielsweise string::substr, wenn pos out-of-range. Ich tendiere bei meinen Klassen dazu, dass nur zu checken, wenn bestimmte Makros aktiv sind, ansonsten gibt es halt einen Segfault o.ä. Gleiches gilt für nullptr Übergabe an Funktion etc.
    Um interne Fehler zu checken, beispielsweise dass die berechnete id im Konstruktor eines Objektes nicht 0 ist, also bei Programmierfehlern von einer Person/einem Team, wo nichts externes drauf Zugriff hat, nutze assert.
    Ansonsten nutze Rückgabewerte, bpsw. Element not found in string via npos o.ä.



  • Naja.
    Ich habe mir mal das Video zur Going Native 2012 von Bjarne Stroustrup angeguckt.

    http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style

    Am Ende des Videos bei der Fragerunde spricht ihn ein Entwickler auf den Entwicklungsleitfaden fuer den Joint Strike Fighter an, den er mit anderen geschrieben hat. Time: 1:16:38

    Dort wurde die Verwendung von Exceptions strikt verboten, die Begruendung dazu liefert er dann auch gleich noch mit.

    Ich persoenlich bin noch sehr neu in C++ von daher ist meine Meinung nicht unbedingt valide.

    Aber Exceptions aus einem Konstruktor zu werfen "fuehlt" sich (fuer mich persoenlich) so an, als ob man als Pilot in einem Flugzeug erst 50m ueber dem Boden nachsieht ob man den richtigen bzw. ueberhaupt genug Treibstoff getankt hat.

    Vor allem was passiert, wenn das Objekt das ich erstelle von anderen Klassen erbt und deren Konstruktoren bereits aufgerufen und erstellt wurden und ich dann irgendwo eine Exception werfe und mit dem Schleudersitz aus dem Konstruktor aussteige?

    Das kann doch nicht mehr gecleaned werden, oder?

    Ich habe bis jetzt immer "Manager" gebaut die sichergestellt haben, dass die Informationen valide sind bevor das Objekt gebaut wurde.
    (User anlegen, Bestellung anlegen usw.)

    Bei Objekten die einen Service anbieten, also keinen Manager haben wird das Objekt normal erstellt und der Service ueber Funktionen angeboten, die dann den Fehler ueber ein "Return" mitteilen, so dass das erstellte Objekt wieder weggeschmissen werden kann.

    Wie gesagt ich bin ein Newbie und habe es bis jetzt so gemacht und diese Ansicht ist vielleicht nicht unbedingt richtig.



  • Dein Vergleich hinkt.
    Erst nachdem der (in C++11 ein) Konstruktor seine Arbeit getan hat, ist das Objekt erzeugt -> erst wenn der Pilot nachgeschaut hat, ob Treibstoff da ist, hebt er ab.
    Wenn ein derived Objektkonstruktor eine Exception wirft, ist der base Konstruktor schon abgeschlossen, der base-Teil gilt als erzeugt und der Destruktor von base wird aufgerufen, aber nicht der von derived
    (das die Destruktoren von nicht vollständig erzeugten Objekten nicht aufgerufen werden, kann auch zu Resourceleaks führen, wenn man manuell mit Speicher hantiert).
    Ruvi, die Designer von C++ denken schon an solche Sachen. 😉



  • Ruvi schrieb:

    Vor allem was passiert, wenn das Objekt das ich erstelle von anderen Klassen erbt und deren Konstruktoren bereits aufgerufen und erstellt wurden und ich dann irgendwo eine Exception werfe und mit dem Schleudersitz aus dem Konstruktor aussteige?

    Wenn du richtig programmierst (Stichworte RAII und Exceptionsicherheit), wird alles bereits erstellte ordnungsgemäss zerstört, und die Konstruktion wird abgebrochen. Danach bist du im gleichen Zustand wie vor dem Konstruktoraufruf.

    Wenn du im Konstruktor keine Exception wirfst, lässt du Objekte am Leben, obwohl sie nur halbwegs konstruiert sind. Sie befinden sich also in einem Zustand, in dem sie nicht richtig verwendbar sind. Klasseninvarianten können nicht mehr garantiert werden, als Folge muss man das Objekt als Benutzer auf Gültigkeit prüfen oder nachträglich initialisieren.

    Die Problematik ist ähnlich wie bei Zeigern. Leider hört man immer noch ab und zu die Empfehlung, Zeiger jeweils unmittelbar auf nullptr zu setzen und sie vor jedem Zugriff mit if (ptr) zu prüfen. Damit versteckt man aber nur Logikfehler, da im Fehlerfall einfach nichts passiert. Wenn du hingegen die Gültigkeit des Zeigers voraussetzt, kracht es im Fehlerfall (Nullzeiger-Dereferenzierung, sonst gibts auch assert ), ausserdem hast du einfacheren Code und keinen unnötigen Laufzeitoverhead.

    Ruvi schrieb:

    Ich habe bis jetzt immer "Manager" gebaut die sichergestellt haben, dass die Informationen valide sind bevor das Objekt gebaut wurde.

    Du kannst das nicht immer sinnvoll tun. Wenn ein Konstruktor ein Bild lädt, aber die Datei nicht findet, was tust du? Natürlich kannst du versuchen, das Bild im Voraus zu laden, aber damit trägst du nur Implementierungsdetails aus der Klasse. Die Anwendung der Klasse wird mühsam, weil man als Benutzer nicht einfach ein Objekt erstellen kann, sondern sich um interne Details kümmern muss.



  • Ich lese den Begriff Manager-Klasse das erste Mal. Ich habe bis jetzt nur Klassen benutzt wegen der Kapselung und dem automatischen Aufraeumen und fuer die Struktur. Design-Pattern oder sowas wie den angesprochenen Manager, kenne ich nur bedingt, da ich dafuer noch nie den grossen Bedarf hatte.

    Gibt es da ein gutes Buch oder ein Link zu Thema?


Anmelden zum Antworten