Wo sollte man assert benutzen?
-
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?
-
Ja mein Gott, im äußerst einfachen Falle einer Wurzelfunktion bietet es sich halt nicht an, die Exception zu behandeln. Mir ist doch sowieso egal, was mit der Exception passiert.
Hauptsache, meine Funktion liefert niemals irgendeinen Müll zurück.
-
Die Wurzel ist nur ein Beispiel. Das gilt doch für alles. Warum sollte ich, statt es richtig zu machen, eine Exception fangen? Exceptions setze ich ein, wenn die Fehlerursachen unkontrollierbar sind.
-
Ja, das ist deine Philosophie. Für mich sind Fehlerursachen auch unkontrollierbar, wenn ich mich darauf verlassen muss, dass andere meine Funktion richtig füttern.
Das mit der Performance ist eigentlich nur bei sowas simplen ein Problem, wenn ich bei einer richtig fetten Klasse mal einen Wert von außen annehme, wird er einmal durch die Methode geprüft und alle Methoden (die in der Regel private sind), die danach damit arbeiten, haben nur noch asserts, weil das dann etwas ist, wo ich durch meine Programmlogik sicherstellen kann, dass der Wert nicht ungültig ist.
Und wenn ich lauter fürchterlich komplizierte Berechnungen und vielleicht noch Grafikausgaben mache, dann ist doch das einmalige Prüfen wirklich kein Flaschenhals.Aber diese Diskussion führt wirklich zu nichts mehr jetzt, fürchte ich.
Das sind einfach unterschiedliche Design-Philosophien, meine ich.
-
Optimizer schrieb:
Ja, das ist deine Philosophie. Für mich sind Fehlerursachen auch unkontrollierbar, wenn ich mich darauf verlassen muss, dass andere meine Funktion richtig füttern.
Diese anderen (was eigentlich, Teammitglieder, oder Benutzer deiner Library?) arbeiten doch nicht im Release-Modus bzw. mit der Release-Version deiner Library. Die kriegen genauso die Assertions um die Ohren wie du.
-
Das machen aber scheinbar alle größeren Bibliotheken (DirectX zum Beispiel) so. DX wirft halt keine Exception, sondern gibt einen FAILED-Wert zurück (Managed DirectX throwt aber auch, wahrscheinlich ist der Rückgabewert nur wegen C noch so), aber der Punkt ist, dass in jedem Build die Argumente geprüft werden.
Ich verstehe auch nicht, was dagegen spricht. Argumente sind ja nicht immer statisch, die werden auch mal berechnet (vielleicht sogar aufgrund von Benutzereingaben). Sonst wär die Sache ja einfach.
-
Du setzt also deine Klassen mit System-APIs wie DX gleich.
-
Ich wende dasselbe Prinzip an, so wie es große Libs machen, wie ich es gelernt habe und wie ich es selber richtig finde, weil ich der Meinung bin, dass auch im Release-Build solche Prüfung stattfinden sollten.
Weil ich nicht will, dass meine Lib Unsinn macht, Unsinn zurückliefert oder undefiniertes Verhalten erzeugt. Das ist wie wenn ich ne Fernbedienung verkaufe und die geht nach einem Tastendruck kaputt. Und ich sag dann "haben sie die Bedienungsanleitung nicht gelesen? Sie müssen immer von unten dagegen drücken." Da bau ich das Ding lieber stabiler.Das sind zwei verschiedene Ansichten, ihr bevorzugt Performance, ich bevorzuge Sicherheit und locker die Sicherheit vielleicht dann nachträglich nochmal ein bisschen, wenn eine bestimmte Prüfung zu sehr auf die Performance drückt.
-
Dann prüfst du sicherlich auch alle Referenzen, die du bekommst, ob sie nicht verschleierte null Zeiger sind?
Verwendest niemals nicht konstante Referenzen, denn der Benutzer könnte ja mit einem const_cast undefiniertes Verhalten heraufbeschwören, oder wie?
Wenn jemand undefiniertes Verhalten will, dann kriegt er es auch, egal wie gut du deine lib absicherst.
-
Dann bau dir doch ein eigenes abschaltbares assert, was im Fehlerfall Exceptions wirft, anstatt das Programm abzubrechen. Und schalte ASSERTIONS_ON auch im Release nicht ab.
# ifdef ASSERTIONS_ON # define my_assert(COND) ((COND)?(void)0:throw AssertionViolation(__FILE__, __LINE__, #COND)) # else # define my_assert(COND) # endif class AssertionViolation : public std::logic_error { public: AssertionViolation(const char * file, unsigned int line, const char * condition); };
-
davie schrieb:
Dann prüfst du sicherlich auch alle Referenzen, die du bekommst, ob sie nicht verschleierte null Zeiger sind?
Nein, das prüfen die Betriebssysteme für die ich programmiere.
Ihr glaubt immer, ich geb mich mit der halben Performance zufrieden, nur damit ich die Prüfungen habe, aber ihr überschätzt die paar Tests ganz extrem. Das sind ein paar Methoden, die man von außen benutzen kann und die rechnen jetzt irgendein kompliziertes Zeug aus und rufen weitere interne Methoden auf - da macht doch eine simple if-Abfrage nichts mehr aus (sagt zumindest auch mein Profiler und QueryPerformanceCounter()).
Also, was zum Geier spricht jetzt gegen ein paar Abfragen?@Bashar, das hat nichts mit Exceptions an sich zu tun, sondern ich verwende sie deshalb, weil die Bedingung immer geprüft werden soll. Ich könnte auch mit Rückgabewerten arbeiten, aber das mach ich aus verständlichen Gründen nicht. Es geht darum, dass die Prüfung gemacht wird, nicht um die Art der Fehlerbehandlung.
davie schrieb:
Wenn jemand undefiniertes Verhalten will, dann kriegt er es auch, egal wie gut du deine lib absicherst.
Nein, kriegt er nicht. Außer wir bleiben jetzt wirklich bei so einfachen Sachen wie eine Wurzelfunktion, wo ich dann auch keine Prüfung machen würde.
-
Hmm. Hier wird immer gesagt, assert ist im Release-Modus deaktiviert. Das ist natürlich nicht ganz richtig. Ob Assertions geprüft werden, geht danach ob NDEBUG definiert ist. Lass doch NDEBUG einfach aus, auch im Release.
-
Optimizer schrieb:
Also, was zum Geier spricht jetzt gegen ein paar Abfragen?
Nichts, aber es spricht etwas gegen Redundanz. Wenn eine Funktion einen falschen Wert bekommt, weil der User diesen nicht überprüft dann reicht ein assert. Wenn ein Programmierer so blöd ist und logische Fehler macht (wie z.B. divide mit 0 oder sqrt mit einem negativen double-Wert) dann ist er schuld. Netterweise fliegt ihm ein assert um die Ohren das auch noch nichts kostet, weil der Compiler es im Release-Build ignoriert (wenn NDEBUG deaktiviert ist). Exceptions sind für andere Sachen gedacht.
-
Um jetzt nochmal bei der Funktion, die den const char * erwartet zu bleiben.
Wenn ich spezifiziere, daß der nicht 0 sein darf, dann ist es ein Programmierfehler, wenn er 0 ist, oder?
Und daran ändert es auch nichts, wenn der string eine Eingabe des Benutzers ist. Wenn er da was blödes eingibt (oder nix eben), dann kommt ein Pointer auf "" rein und nicht 0, das ist ein kleiner aber feiner Unterschied.Btw.: Wir überprüft Dein OS denn, das eine Referenz gültig ist:
int * x=0;
int & y = *x;
// und los gehtsMfG Jester
-
Jester schrieb:
Btw.: Wir überprüft Dein OS denn, das eine Referenz gültig ist:
int * x=0;
int & y = *x;
// und los gehtsMfG Jester
Da geht gar nichts los. Das führt zu einer Acces Violation unter Windows und Linux. Ich habe es jetzt sogar extra ausprobiert.
@Bashar: Natürlich kann ich auch asserts im Release haben. Finde ich auch voll ok. Mir geht es ja nur darum, dass die Prüfung stattfindet.
Und ich nehme halt jetzt Exceptions, weil ich ein anderes Verständnis von asserts habe (siehe oben) und mische mich nicht in die #defines ein, die sich schlaue Leute so ausgedacht haben.
-
Optimizer schrieb:
Jester schrieb:
Btw.: Wir überprüft Dein OS denn, das eine Referenz gültig ist:
int * x=0;
int & y = *x;
// und los gehtsMfG Jester
Da geht gar nichts los. Das führt zu einer Acces Violation unter Windows und Linux. Ich habe es jetzt sogar extra ausprobiert.
Bei mir weder unter Windows noch unter Linux
.
int main() { int* x = 0; int& y = *x; }
-
int * x=0; int & y = *x;
dass das alleine schon eine access violation auslöst?
vielmehr ein späteres y = ...
Allerdings ist das *x schon undefiniert.was ist mit deinen klassen?
überprüfst du jedesmal ob nicht vielleicht this == 0?
das wäre nämlich auch denkbar. (wenn auch vom standard undefiniert)warum sagst du dann nicht selbst einfach:
das verhalten der funktion ist undefiniert, falls argument1 == 0.
ebenso wie du vermutest, dass das verhalten einer memberfunktion undefiniert ist,
falls this==0 (bzw. ein nullzeiger sich eben nicht dereferenzieren lässt) - ich nehme nämlich nicht an, dass du in jeder memberfunktion this==0 prüfst.