Wo sollte man assert benutzen?



  • 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 Exception

    Also 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.



  • Optimizer schrieb:

    Lass mal die Performance aus dem Spiel, darum geht es doch jetzt gar nicht.

    Warum nicht? Ich habe begründet, warum Runtime-Checks gerade in ner Lib nur eingeschränkt vorhanden sein sollten. Oder läßt Du das nur nicht zählen, weil Dir nix dazu einfällt?



  • Optimizer schrieb:

    Lass mal die Performance aus dem Spiel, darum geht es doch jetzt gar nicht.

    Doch. Sonst kann man gleich Java nehmen. Dort werden auch Tausend sachen gecheckt, die nicht Fehlschlagen koennen, weil ich weiss, dass ich es richtig gemacht habe.

    Entweder etwas kann ungültig sein und du hast es nicht in der Hand, dann musst du es im Release-Build genauso prüfen

    Nein.
    Die Doku verlangt zB, dass nur folgende Werte erlaubt sind. Wozu in der Release Version checken? Der Caller ist dafuer verantwortlich mir die richtigen Daten zu geben. Es waere ja Bloedsinn wenn Caller und Callee, beide checken - oder noch schlimmer - es ueberall gecheckt wird, wo der wert verwendet wird.

    Du gehst ja auch davon aus, dass ein Objekt in einem gueltigen Zustand ist, und rufst nicht dauernd Invariants auf, um sicherzugehen.

    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.

    Dagegen spricht ja auch nichts. Du gehst sicher, dass der Nenner ungleich 0 ist - ist auf dich kein verlass, dass jeder das selber checken muss? Bei mir reciht es, wenn jeder Wert einmal auf gueltigkeit gecheckt wird - das muss nicht jede funktion selber machen.

    Und wenn mir einmal ein Wert entkommt, dann fange ich ihn mit assert in der Debug version ab - denn dort werden alle checks 100.000 mal gemacht.

    Niemand sagt: werte muessen nicht gecheckt werden.
    Sondern wir sagen: Werte muessen nur einmal gecheckt werden. Fuer jedes weitere mal nimmt man assert.



  • Seltsam... genau das habe ich auch gesagt.

    Ich habe gesagt, ich kann in meiner weiteren Verwendung davon ausgehen, dass der Nenner ungleich 0 ist. -> assert
    Ich habe gesagt, für private Hilfsmethoden, die nur ich aufrufe, die arbeiten mit Werten von mir, oder schon mal woanders gecheckt, da kann ich davon ausgehen, dass alles passt -> assert
    Ich habe auch gesagt, dass der Wert einmal (im Konstruktor und in Methoden, die den Bruch ändern) geprüft werden muss -> Exception

    Aber hier werden auf einmal Unterschiede zwischen Debug und Release aufgestellt, "hmmmmmmmm ich muss in der Release ja nicht prüfen, weil ich kann mich ja darauf verlassen, dass die anderen nicht so dumm sind, im Konstruktor 0 zu übergeben. 🙄

    Und wenn am Ende wirklich die Performance nicht passt, kann man aus den 2, 3 kritischen Methoden die Checks entfernen, aber nicht einfach mal von vornherein weglassen.
    Ich kann nicht einfach sagen, "der Parameter passt schon", wenn ich das nur mit einem assert abprüfe, was im Release-Build nicht mehr gemacht wird.



  • Optimizer schrieb:

    Und wenn am Ende wirklich die Performance nicht passt, kann man aus den 2, 3 kritischen Methoden die Checks entfernen, aber nicht einfach mal von vornherein weglassen.

    Ich hab doch erklärt, daß das nicht so einfach geht.

    Also Fazit: Lib nur mit unbedingt nötigen Checks, wer mehr braucht kann sich nen Wrapper bauen.



  • Optimizer schrieb:

    Aber hier werden auf einmal Unterschiede zwischen Debug und Release aufgestellt, "hmmmmmmmm ich muss in der Release ja nicht prüfen, weil ich kann mich ja darauf verlassen, dass die anderen nicht so dumm sind, im Konstruktor 0 zu übergeben. 🙄

    NEIN!

    Es wird jeder Wert EINMAL gecheckt - und das wars.
    strcpy checkt seine Parameter nur per assert ob sie passen - denn es ist ein Logikfehler wenn ein 0 Zeiger uebergeben wird. Der Caller uebernimmt hierbei die Arbeit des checkens, bzw. laesst es sein, wenn die Werte in Ordnung sind.

    Das betrifft somit nicht nur kleine Hilfsfunktionen, sondern auch ordentliche grosse Funktionen aus grossen Libraries. Nimm zB die C++ Standard Library. Dort findet keine unnoetige Pruefung statt - denn der Caller checkt die Werte. Das hat den Sinn, dass kein Wert 2mal gecheckt werden muss.

    Stell dir mal ein memcpy vor, dass checkt
    ob die beiden pointer ungleich 0 sind
    und
    ob die speicherbereiche nicht ueberlappen

    es wuerde vermutlich 50% der Zeit fuer diese Abfragen daruf gehen, obwohl der Caller weiss, dass die Werte in Ordnung sind.

    Nimm mal folgende Schleife:

    for(int i=0; i<size; ++i)
    {
      arr[i]=sqrt(abs(arr[i])) + sqrt(abs(arr[i]));
    }
    

    Wozu muss 3mal gecheckt werden ob arr[i] ein gueltiger Index ist?
    Wozu muss sqrt 2mal checken ob arr[i] negativ ist?
    Wozu dieses x-malige checken?

    Dies faellt nicht unter Premature Optimization - denn niemand wuerde ernsthaft:

    if(!p) throw "error";
    if(!p) throw "error";
    if(!p) throw "error";
    if(!p) throw "error";
    p->foo():
    

    schreiben, wenn ein if(!p) throw "error"; doch reicht...



  • Ok, bleiben wir mal bei strcpy. Wenn ein NULL-Pointer nicht zu einer Exception führen würde, sondern undefiniertes Verhalten verursachen würde (halt einfach den Wert bei 0x00000000 nehmen), würdest du dann immer noch ein assert verwenden?

    Shade Of Mine schrieb:

    Nimm mal folgende Schleife:
    [...]
    Wozu muss 3mal gecheckt werden ob arr[i] ein gueltiger Index ist?
    Wozu muss sqrt 2mal checken ob arr[i] negativ ist?
    Wozu dieses x-malige checken?

    Warum nicht? Das Berechnen der Wurzel nimmt doch sowieso 99% der Zeit ein. Ok, deine Wurzel ist ja nicht sicherheitskritisch. Dann kommt halt ein schwachsinniges Ergebnis raus.
    Aber Arrays verwendest du nicht nur in dieser Schleife und das ist eine verdammt üble Geschichte, wenn du einen falschen Index benutzt (stell dir mal vor, du zählst am Index nicht, sondern berechnest ihn selber erstmal).

    Und so schlimm ist es doch nicht, wenn du den Betrag, die Wurzel und vielleicht nochmal die Fakultät einer Zahl berechnest, dass du dann vorher die Zahl prüfst. :p
    Vor allem, weil so komplizierte Berechnung dann doch oft in kleine Hilfsmethoden ausgelagert werden, die rufst ja dann nur du auf, da würde ich dann auch nur asserts verwenden (wie oben beschrieben).

    Es geht mir doch nur um eins: Ich verstehe nicht, wie ihr die Entscheidung, ob ihr asserts oder exceptions verwendet, von Debug/Release - Build abhängig macht, da ist doch keine Logik dahinter.
    Asserts sind keine Prüfung, sondern zusätzliche Kontrollen, ob die Programmlogik korrekt abläuft.



  • Wie benutzt man eigentlich assert in schleifen bei arrays?



  • Optimizer schrieb:

    Ok, bleiben wir mal bei strcpy. Wenn ein NULL-Pointer nicht zu einer Exception führen würde, sondern undefiniertes Verhalten verursachen würde (halt einfach den Wert bei 0x00000000 nehmen), würdest du dann immer noch ein assert verwenden?

    Das führt nicht zu einer Exception, sondern zu undefiniertem verhalten.
    Man verwendet trotzdem assert. Denk mal drüber nach, warum die Profis, die sowas schreiben das so machen, wenn es so falsch ist.

    Optimizer schrieb:

    Es geht mir doch nur um eins: Ich verstehe nicht, wie ihr die Entscheidung, ob ihr asserts oder exceptions verwendet, von Debug/Release - Build abhängig macht, da ist doch keine Logik dahinter.
    Asserts sind keine Prüfung, sondern zusätzliche Kontrollen, ob die Programmlogik korrekt abläuft.

    Wir unterscheiden überhaupt nicht nach Debug/Release, mein Code läuft unverändert im Debug und im Release, nur daß mich die asserts im Release nichts kosten.
    Asserts sind keine Kontrollen für die Programmlogik, sie sind Kontrollen, ob meine Libs korrekt verwendet werden. Einfach nur, ob die Aufrufe im Prinzip okay sind. Wenn was in der Logik schief läuft, dan werf ich auch ne Exception. Aber das sich ein Nullpointer in strcpy verirrt ist kein Fehler in der Programmlogik, sondern ein Fehler im Programm. Und damit zwinge ich denjenigen meine Schnittstelle sauber zu benutzen, weil er nämlich sonst nicht weitermachen kann. Bei exceptions kann er ein try{}catch(...){} außenrumbauen und mich weiter zumüllen. Und wer nicht im Debugmodus entwickelt und testet, der ist irgendwie selber schuld, oder nicht?

    MfG Jester



  • Jester schrieb:

    Und damit zwinge ich denjenigen meine Schnittstelle sauber zu benutzen, weil er nämlich sonst nicht weitermachen kann. Bei exceptions kann er ein try{}catch(...){} außenrumbauen und mich weiter zumüllen. Und wer nicht im Debugmodus entwickelt und testet, der ist irgendwie selber schuld, oder nicht?

    Nein tust du nicht. Du zwingst ihn nur im Debug-Build dazu.

    Und wenn er ein try-catch macht, ist das seine Sache, das geht dich eigentlich nicht mal was an. Hauptsache, er bekommt nicht das, was er will (Bruch mit Nenner 0, Logarithmus von -563870465439865 (<- Integer Überlauf)). Das ist der entscheidende Punkt, deine Lib liefert keinen Schrott.

    Und asserts sind Kontrollen für die Programmlogik, nichts anderes (auch keine Prüfung von Benutzereingaben). Nur deshalb entfallen sie im Release, weil dann die Programmlogik stimmt.



  • Ähm, kann es sein, daß Du irgendwie davon ausgehst daß der Benutzer Mist bauen will? Ich zwinge ihn im Debug, und da er ein korrektes Programm schreiben will wird er dran arbeiten, bis es korrekt ist. Dann verwendet er die Lib korrekt und im Release funktioniert alles wunderbar, nur schneller.

    Und nochmal für Dich: asserts sind keine Kontrollen der Programmlogik. Es sind Kontrollen gegen Programmierfehler. Warum habe ich schon beschrieben und auch was sie von Kontrollen der Programmlogik unterscheidet. Deine Aussage wird nicht dadurch richtiger, daß Du irgendwelche Wörter fett machst. Eine Begründung muß dazu.

    edit:
    Wenn Du sie natürlich für die Kontrolle der Programmlogik verwendest ist das Dein Problem, dafür sollte man nämlich wirklich Exceptions nehmen.

    MfG Jester



  • Ja. Ich hab schon öfters mal eine DirectX Funktion mit 14 Parameter falsch aufgerufen (weil ich der DAU bin 😉 ).

    Ok, ich wiederhole die Begründung: Wenn die Programmlogik funktioniert, weil ich das Programm ausgiebig getestet habe, dann brauche ich da keine Prüfungen mehr. Deshalb asserts für die Programmlogik. Weil die Programmlogik meine Sache ist und ich weiss, das sie stimmt.

    Und wenn ich eine Lib verkaufe, weiss ich nicht, dass sie korrekt verwendet wird. Deshalb hier Exceptions, weil nicht aufgrund meiner Programmlogik sichergestellt ist, dass diese Bedingung nie eintritt. Also werf ich dem Aufrufer eine Exception zu, weil der dafür verantwortlich ist, dass ein Problem zustande gekommen ist und das kann er jetzt behandeln oder nicht.

    Was willst du denn mit einer Exception in der Programmlogik? Wenn du ein Datenbank Programm schreibst und irgendwann mal mit ner Prüfung feststellst, dass alle Noten der Schüler auf einmal gelöscht sind, was nützt dir dann deine Exception, willst du sie behandeln?

    Performancegründe sind doch kein Grund gleich mal generell alle Prüfungen mit Asserts durchzuführen, insbesondere solche, die von Benutzereingaben oder vom Aufrufer abhängen. Bei wirklich krass wichtigen Funktion muss man halt dann auf die Prüfung leider verzichten (also nur noch assert einbauen).



  • Optimizer schrieb:

    Performancegründe sind doch kein Grund gleich mal generell alle Prüfungen mit Asserts durchzuführen, insbesondere solche, die von Benutzereingaben oder vom Aufrufer abhängen.

    Langsam komm ich mir verarscht vor



  • Ich mir auch...

    @Optimizer: Kannst Du mal kurz Programmlogik definieren?
    Ich glaube wir verstehen da unterschiedliche Sachen drunter.

    Für mich hat es nichts mit Programmlogik zu tun, wenn meine Funktion einen ungültigen Parameter kriegt. Das ist ein Programmierfehler und kein Problem der Programmlogik. Und nur die will ich doch mit asserts abfangen.



  • Ja, und ich fange ungültige Parameter mit Exceptions ab. Das ist - wie du gesagt hast - kein Fehler in der Programmlogik.

    Du fängst sie mit asserts ab, damit sagst du meiner Meinung nach "Ok, also in meinem Programm wird meine Lib richtig benutzt, das hab ich jetzt ausgiebig getestet und im Release brauch ich die Prüfung nicht mehr".

    Aber wenn ein anderer deine (vielleicht schon compilierte) lib benutzt, dann verlässt du dich auch darauf, dass sie richtig benutzt wird. Das ist IMO ein Fehler, da du das nicht durch deine Programmlogik sicherstellen kannst, dass du gültige Argumente kriegst (außer in private Methoden). -> kein assert
    (Außerdem nimmst du dem Aufrufer die Möglichkeit, den Fehler zu behandeln.)

    Aber ich glaube, das wird heute nichts mehr. Halten wir einfach fest:

    Ich bin der Meinung, man sollte asserts zur Kontrolle der Programmlogik verwenden. Für Sachen, die niemals, unter gar keinen Umständen, passieren können. Die wirklich eigentlich unmöglich sind, egal was andere machen.

    Du bist der Meinung, man sollte sie zur Kontrolle von Programmierfehlern (auch durch andere) benutzen. Das schließt das Übergeben von ungültigen Argumenten, Indizes mit ein. Richtig so?



  • Ja, ich denke das kann man so stehen lassen.
    Selbstverständlich biete ich sowohl einen Debug, also auch einen Release-Build für den Benutzer zur Verfügung.



  • Optimizer schrieb:

    (Außerdem nimmst du dem Aufrufer die Möglichkeit, den Fehler zu behandeln.)

    Der Aufrufer muss den Fehler ja gar nicht erst machen. Wie ich schon sagte, wenn er nicht in der Lage ist, die Vorbedingungen zu überprüfen, warum dann eine Exception fangen, die nur seinen eigenen Programmierfehler anzeigt?

    double y = arbitrary_value(), z;
    try { // also irgendwas ist mit der Funktion, aber ich les die Doku vielleicht morgen, oder nächste Woche
      z = wurzel(y);
    } catch(RadicandNegativeException& e) {
      // Ich hab keinen blassen (ehrlich!) was hier schiefgegangen sein könnte!
    }
    

    Hältst du das für realistisch?


Anmelden zum Antworten