Wo sollte man assert benutzen?



  • 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



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


Anmelden zum Antworten