Wo sollte man assert benutzen?
-
Hi,
ja, der Meinung bin ich auch. Eine Lib sollte z.B. auch im Releasebuild (also Lib im Releasebuild, so wie sie ja normalerweise ausgeliefert wird) alle Eingaben die von außen kommen als ungültig behandeln, bis sie ausdrücklich validiert wurden. Dazu gehört strenggenommen sogar, dass man nicht nur p != 0 prüft, sondern auch, dass p auf einen gültigen Speicherbereich zeigt, was IIRC aber nur mit plattformspezifischen Funktionen geht.
ChrisM
-
edit: habt ihr meinen Beitrag überhaupt gelesen? Es geht da nicht um eine Lib und die dort angebotenen High-Level-Operationen, sondern um die verschiedenen Abstraktionsebenen. Nur die oberste muß wirklich die Konsistenz prüfen.
@Optimizer: Du bist also für folgende Variante:
// unterste Abstraktionsebene void myfunc(const char * p) { if(!p) { //Fehlerbehandlung } } // mittlere Abstraktionsebene void performOperation(const Part & part) { if(! part konsistent) { Fehlerbehandlung } myfunc(part.pointer); // ... } // höchste Abstraktionsebene void complexOperation(const Object & o) { if(! o konsistent) { Fehlerbehandlung } performOperation(o.part); //... }
Die schönen redundanten Tests machen den Code sicher sehr performant.
Ansonsten verweise ich an dieser Stelle einfach mal auf Large Scale C++ Software Design von John Lakos, an dessen Beispiel obiges (das aus dem vorigen Beitrag) angelehnt ist.MfG Jester
-
Optimizer: Du hast Jesters Posting missverstanden. Wenn du in einem Programm eine Benutzereingabe machst, dann sollte idealerweise nur an einer einzigen Stelle geprüft werden, ob die Eingabe gültig ist. Nicht in jeder Funktion, die irgendwann mal die Eingabe bearbeitet. Nimm z.b. das:
double wurzel(double x) { if (x < 0) throw "Wurzel aus negativer Zahl??!"; else return sqrt(x); } int main() { double x; do { cout << "x >= 0 eingeben: "; cin >> x; } while (x < 0); cout << "Wurzel: " << wurzel(x); }
Wozu die zusätzliche Prüfung in wurzel()? Ein assert würde es auch tun, denn wurzel hat eben die Vorbedingung, dass das übergebene Argument nicht-negativ ist, und die Eingaberoutine sorgt auch dafür, dass nur gültige Eingaben angenommen werden. Ein assert würde in aggressiver Weise dafür sorgen, dass die Prüfung der Eingabe an einer Stelle konzentriert ist.
-
Moment mal, ich kann nicht hellsehen, ich weiss nicht, wo das her ist.
Wenn das jetzt so gemeint war, dass das nur ne kleine, private Hilfsmethode ist, die garantiert kein anderer füttert als ich, dann werd ich da sicher auch keine Exceptions benutzen.
Aber dann ist ja auch Kraft eigener Programmlogik sichergestellt, dass nichts passiert. Exceptions sollte man benutzen, sobald man keinen Einfluss darauf hat, was man bekommt und für mich hat das Stück Code so ausgeschaut, als wird da irgendwo mal ein String eingelesen.
-
@Optimizer
Benutzereingaben, Lesefehler oder sonstiges kommen im Release-Build auch vor, sowas sollte man mit Exceptions behandeln.
dagegen hat auch niemand etwas gesagt. Nur wenn ich eine Library oder allgemeine Funktion schreibe, weiss ich ja nicht, ob es sich um die Benutzereingabe etc. handeln könnte, deswegen hat hier dann der Programmierer selber vorsorge zu treffen.
assert dient eben nur dazu, dass dem Programmierer die Fehlersuche erleichtert wird und nicht irgend wo das Programm zusammen bricht plötzlich.
-
@Bashar: Ok, darauf können wir uns einigen.
-
Hi,
mag sein, ich bleibe trotzdem bei meiner Meinung, dass eine Libs alle Eingaben validieren sollte, auch wenn sie natürlich nicht wissen kann, ob das nicht schon von der ansteuernden Anwendung geschehen ist. Für mich ist die Lib ein eigenes Modul, das in sich geschlossen stabil laufen muss, auch wenn es ungültig angesteuert wird.
ChrisM
-
@ChrisM: Ich glaube, wir meinen alle das selbe. (Und das beinhaltet auch, dass alle Eingaben geprüft werden).
Mich hat nur Jesters Beispiel etwas verwirrt.
-
Jo, so war's auch gemeint. Wichtige Voraussetzung ist natürlich, daß die Vorbedingungen spezifiziert und dokumentiert sind. Und dann stelle ich mich irgendwie schon auf den Standpunkt, daß der Benutzer (damit meine ich den, der meine Funktion benutzt) dafür verantwortlich ist, daß er meine Funktion korrekt benutzt. Und um ihm das Leben etwas leichter zu machen helfe ich mit dem assert nach, damit er auch gleich merkt, wenn er sich nicht dran hält.
MfG Jester
-
Hi,
nein, ich habe Jester schon verstanden.
Trotzdem bin ich der Meinung, dass die Funktion ein definierbares Verhalten (z.B. Exception) zeigen sollte, wenn z.B. 0 übergeben wird, obwohl in der Dokumentation bzw. einem Kommentar steht, dass p nicht 0 sein darf.
EDIT: Eine andere Möglichkeit wäre es natürlich, es so zu machen, wie Microsoft bei DirectX und gleich noch Debugversionen der DLLs im SDK mitauszuliefern, die dann natürlich viele zusätzliche Sicherheitschecks beinhalten.
ChrisM
-
Wenn ich bestimmte Bedingungen fordere, dann muß ich auch davon ausgehen können, daß sie eingehalten werden, sonst habe ich in jedem Fall das Problem, daß ich redundante Prüfungen habe. Und mit den asserts wird die Eingabe ja wunderbar validiert, wer's falsch benutzt kriegt richtig eins auf den Deckel und muß sich drum kümmern es richtig zu machen.
MfG Jester
-
Hi,
ja, aber das assert() ist im Auslieferungsbuild ("release") ja nicht drin.
Lies mal oben mein Edit, das kam nach deinem Post eben. Können wir uns darauf einigen?
ChrisM
-
Jester schrieb:
Und dann stelle ich mich irgendwie schon auf den Standpunkt, daß der Benutzer (damit meine ich den, der meine Funktion benutzt) dafür verantwortlich ist, daß er meine Funktion korrekt benutzt.
Dann haben wir uns doch nicht richtig verstanden
Ich sag nochmal, wie ich es meine:
public: int bla(int x) { if (Argument ist ungültig) throw IllegalArgumentException(); assert(Ergebnis ist korrekt); // optional } private: int blubb() { assert(Argument ist gültig); }
-
Vielleicht sollten wir mal unterscheiden, welche Fehler es da geben kann.
Einmal die, das ein Pointer zum Beispiel nicht 0 sein darf. Das würde ich mit nem assert machen. Das ist sozusagen der Teil über den der Programmierer ne gute Kontrolle hat. Es handelt sich dabei eindeutig um einen Programmierfehler.
Es gibt aber auch semantische Fehler, das Objekt das übergeben wird erfüllt einige Eigenschaften nicht, die es aber haben muß, die sind aber semantischer Natur, nicht sowas einfaches wie Nullpointer... Dann werf ich ne Exception.
Denn der aufrufende Code weiß vielleicht garnicht, was er da weitergibt und außen werden möglicherweise verschiedene Teile produziert, von denen einige mit meiner Funktion zusammen arbeiten können, ander nicht.@ChrisM:
Wir sind hier aber bei Standard C++, da gibt's keine Dlls.
Jedenfalls ist meine Guideline: In den höheren Abstraktionsebenen Exceptions, weiter unten asserts.
-
Kann es sein, dass der springende Punkt die unterschiedliche Bedeutung von Benutzer ist? Für Jester ist der Benutzer der, der die Funktion aufruft und die Dokumentation (damit auch Vor- und Nachbedingungen) kennt. Für Optimizer scheint der Benutzer Joe, der dressierte Affe, zu sein, der immer erstmal jeglichen Nonsens eingibt ...
Es muss eine Instanz geben, die Joe's Dumm-Eingaben abfängt. Aber der Rest des Programms darf sich (per assert) drauf verlassen dürfen, dass die Eingabevalidierung richtig arbeitet.
-
Nein, der Benutzer ist ein anderer Programmierer.
Vielleicht ist ein Konstruktor ein gutes Beispiel, denn den benutzen andere und ich hab das nicht in der Hand. Ein Konstruktor für die Rationalklasse darf keinen Nenner mit dem Wert 0 akzeptieren. Jeder, der es versucht, kriegt als Strafe ein Exception zurückgeschmissen, aber (das ist IMO das wichtichtigste) er bekommt kein Rational-Objekt. Auch nicht im Release-Build meiner Rational-Lib.
Ich kann also davon ausgehen, dass kein Rational den Nenner 0 (außer Brüche können ihren Wert ändern) hat. Das ist Kraft eigener Programmlogik sichergestellt und in allen weiteren Methoden, die mit dem Bruch irgendwas sinnvolles machen könnten nochmal mit assert das prüfen.Meine Guideline: Etwas was ich nicht selber in der Hand habe mit Exceptions prüfen.
-
Ein Konstruktor ist aber auch genau eine solche High-Level Funktion. Sie sorgt dafür, das eine komplette Arbeitseinheit in einen gültigen Zustand versetzt wird. Da ist das auch angebracht, ein Konstruktor darf es niemals zulassen, daß ein inkonsistentes Objekt erstellt wird.
Aber meine Beispiele waren immer freie Funktionen.
Wie sieht's da aus?Als Beispiele:
strcmp: ich asserte, daß beide Zeiger nicht 0 sind, keine Exception
sqrt: ich erwarte, daß x>=0, keine ExceptionAlso ich finde eine Library sollte zwar ein paar Sachen abprüfen, aber sie muß auch davon ausgehen können, daß versucht wird ihre Spezifikation einzuhalten. Sonst habe ich so viel Error-Checking drin, daß es für performance-kritische Geschichten völlig unbrauchbar wird. Ein assert dagegen kostet nix.
Ich weiß, Du sagst jetzt, kein Problem, wenn's zu langsam ist nehm ich die Checks halt raus. Willkommen in der echten Welt: Du schreibst das Programm und benutzt die Lib von jemand anderem, kein Source verfügbar, also mußt Du die Lib wechseln, weil sie zu langsam ist oder Du schreibst die Lib und jemand anderes will sie nutzen, der kann nicht einfach mal kurz am Source rumpfuschen, weil er ihn entweder nicht hat, oder eine andere Software sich darauf verläßt, daß die Checks drin sind... er wird sich ne andere Lib suchen müssen.
Okay, als Privatmensch ist das alles kein Problem. Aber in ner Firma ist sowas vielleicht ein bißchen anders.MfG jester
-
Entweder er weiß, dass Rational keine 0 als Nenner akzeptiert, und sorgt dafür, dass das nicht passiert. Oder er weiß es nicht. Im zweiten Fall seh ich aber nicht ein, wieso er dann ausgerechnet wissen wollte, dass er eine Exception fangen sollte. Das wird er also nicht tun, du bekommst also im Release an dieser Stelle einen Abbruch wegen einer ungefangenen Exception ... entweder terminate(), oder ein prophylaktischer catch(...)-Handler.
Du argumentierst also IMHO im Kernn eigentlich dafür, dass man auch im Release-Code Checks haben sollte?
-
Ja, natürlich sollte man das. Klar, deine sqrt Funktion ist jetzt ein krasses Beispiel, man muss schon komplexere Sachen betrachten. Lass mal die Performance aus dem Spiel, darum geht es doch jetzt gar nicht. Entweder etwas kann ungültig sein und du hast es nicht in der Hand, dann musst du es im Release-Build genauso prüfen, oder willst du dann dort bei deinem Delete-Programm versehentlich die falsche Datei löschen, weil der Zeiger auf was falsches zeigt, weil undefiniertes Verhalten aufgetreten ist, weil ... du irgendwo eine Abfrage nicht gemacht hast?
Bashar schrieb:
Entweder er weiß, dass Rational keine 0 als Nenner akzeptiert, und sorgt dafür, dass das nicht passiert. Oder er weiß es nicht. Im zweiten Fall seh ich aber nicht ein, wieso er dann ausgerechnet wissen wollte, dass er eine Exception fangen sollte.
Gut, dann stürzt das Ding halt ab. Das wichtigste ist doch, dass kein Bruch einen Nenner 0 hat, das fangen der Exception interressiert mich nicht, weil es nicht in meinem Verantwortungsbereich liegt. Ich kann für meinem Code davon ausgehen, dass der Nenner nicht 0 ist (aber nur wenn ich die Prüfung gemacht habe). Das ist der Punkt.
-
Entweder etwas kann ungültig sein und du hast es nicht in der Hand, dann musst du es im Release-Build genauso prüfen, oder willst du dann dort bei deinem Delete-Programm versehentlich die falsche Datei löschen, weil der Zeiger auf was falsches zeigt, weil undefiniertes Verhalten aufgetreten ist, weil ... du irgendwo eine Abfrage nicht gemacht hast?
Mein Reden. Die Sinnhaftigkeit der ganzen Eingabe wird einmal geprüft. Fehler, die eigentlich nicht sein können, werden im Debug-Mode mit assert ausgeschlossen. Das Programm wird freigegeben, wenn man mit einiger Sicherheit ausschließen kann, dass noch asserts fliegen würden.
Diese Sicherheit zu verbessern ist dein Job als Programmierer.