Exception oder nullptr?



  • Bastelst du also bei jeder deiner Klassen dieses setstate-Zeugs drumherum?



  • 314159265358979 schrieb:

    Bastelst du also bei jeder deiner Klassen dieses setstate-Zeugs drumherum?

    Solange ich nicht einschätzen kann wie die Klasse genutzt wird, mache ich es wie oben vorgeschlagen. Es mag sicher Klassen geben bei denen es sinnvoll ist immer zu werfen, aber das ist hier ja nicht der Fall.



  • So ein Flag bei allen Funktionen verschandelt die ganze API! Wenn jemand lieber mit exceptions arbeitet, muss er immer ein "true" mitgeben. Wenn der letzte ordentliche Parameter ein bool ist, wirds auch noch ganz verzwickt, wenn man das letzte "true" für das "throwException-Flag" hält. Braucht man dafür ein false und geht später über den Code, ist die Versuchung groß, den Code anzupassen, und das false in ein true zu ändern, obwohl es gar nix mehr mit dem exception-flag zu tun hat.
    Außerdem muss jede Funktion das Flag prüfen und entsprechend den einen oder anderen Codepfad ausführen - das bläht unnötig auf.

    Da jemand entweder volles Rohr auf Exceptions steht (wo sie auch Sinn machen!) oder sie abgrundtief hasst (mir ist noch kaum jemand dazwischen begegnet), ist die einzige saubere Lösung, wenn man wirklich beides anbieten will, ein Präprozesor-Flag, das exceptions anmacht oder auf returncodes ausweicht.



  • cooky451 schrieb:

    314159265358979 schrieb:

    Bastelst du also bei jeder deiner Klassen dieses setstate-Zeugs drumherum?

    Solange ich nicht einschätzen kann wie die Klasse genutzt wird, mache ich es wie oben vorgeschlagen. Es mag sicher Klassen geben bei denen es sinnvoll ist immer zu werfen, aber das ist hier ja nicht der Fall.

    Schlechte Idee.
    Ein sinnvoller Stil ist immer besser als alle Stile die man findet zu vermischen.

    Entweder man wirft Exception oder man wirft keine. Umschalten ist dämlich. Gibt auch genug Diskussionen warum es in der C++ Library mit den Filestreams ziemlich kacke ist.

    Das Flag ist halt furchtbar, weil man nie weiss was jetzt passiert. Ist der Code Exception sicher oder nicht? Man weiss es nicht weil das Ding vielleicht wirft, vielleicht auch nicht. Also muss ich immer alles checken. Das ist einfach nicht gut.



  • destroying angel schrieb:

    ..obwohl es gar nix mehr mit dem exception-flag zu tun hat.

    Ja, das könnte passieren. Aber nicht bei drei Funktionen mit 1-2 Parametern. Eventuell müsste man dann doch noch auf eine flag-Funktion ausweichen.

    destroying angel schrieb:

    Außerdem muss jede Funktion das Flag prüfen und entsprechend den einen oder anderen Codepfad ausführen - das bläht unnötig auf.

    Nein. Die Bedingung die die Exception auslösen soll muss eh geprüft werden, da wird halt noch ein " && throw_exception" angehangen. Was den Code aufbläht, sind #ifdefs in jeder Funktion. 😉

    Shade Of Mine schrieb:

    Ein sinnvoller Stil ist immer besser als alle Stile die man findet zu vermischen.

    Aber nicht wenn man den Stil mit dem die Klasse genutzt werden soll gar nicht kennt.

    Shade Of Mine schrieb:

    Das Flag ist halt furchtbar, weil man nie weiss was jetzt passiert.

    Kannst du mir dafür mal ein Beispiel nennen?



  • sboc schrieb:

    void set( std::size_t index, const Foo& value ); // 1
    const Foo& get( std::size_t index ) const; // 2
    void create( std::size_t index ); // 3
    

    Es hängt natürlich vom Anwendungsfall ab, aber ich würd im Allgemeinen da eher in allen 3 Fällen wenn überhaupt dann ein assert() verwenden. Alternativ könnte man dem Container-Template eine Range-Checking-Policy als Argument geben, die dann wahlweise eine Exception wirft. Oder man macht es so wie die std:: Container und bietet zwei Methoden an, eine die eine Exception wirft und eine die's nicht tut.
    Die Methode create() könnte den alten Wert zurückgeben, oder einen bool der sagt ob der Wert schon vorhanden war und überschrieben wurde.
    Rückgabewerte für Index of out Bounds wären da imo jedenfalls ziemlich fehl am Platz.



  • cooky451: Glaub es einfach, mindestens 3 Leute sagen schon, dass es schrecklich ist.

    sboc: Warum brauchst man überhaupt eine create und eine set Methode? Die create Methode hat nicht mal Zusatzparameter, wie etwas erstellt werden soll. Das sieht alles sehr komisch aus. Was soll das am Ende tun (Beispiel).



  • Zunächst vielen Dank für die Antworten. Da habe ich ja fast vermutet, dass ein kleiner Glaubenskrieg ausbricht. :xmas1:

    Smilies in diesem Beitrag schrieb:

    Warum brauchst man überhaupt eine create und eine set Methode?

    Das ist nur wg. des Beispiels so. Diese drei Operationen sind mir prompt eingefallen, bei denen ich mir unsicher bin, wie ich das handeln soll. Tatsächlich würden z.B. nur "set" und "get" gemeinsam in einer Klasse auftauchen, "create" dann separat in einer anderen.

    dot schrieb:

    Es hängt natürlich vom Anwendungsfall ab, aber ich würd im Allgemeinen da eher in allen 3 Fällen wenn überhaupt dann ein assert() verwenden.

    Das würde mir (technisch) auch völlig ausreichen. Blöderweise verträgt sich das assert() mit Boost.Test, das ich einsetze, nicht so gut. Auf Exceptions testen ist einfacher.

    Im Falle der "get"-Methode tendiere ich mittlerweile zu einer Exception/nullptr-"Kombination". Exception werfen, wenn der Parameter unsinnig ist, d.h. wirklich ein Programmierfehler vorliegt und nullptr (bzw. Fehlerwert) liefern, wenn zwar der Parameter gültig ist, an geeigneter Stelle aber nichts gefunden wird.

    Ich möchte euch aber gerne noch mit einem konkreteren Beispiel verwöhnen, der vorige Code scheint teilweise mehr verwirrt als geholfen zu haben:

    class Map {
    	public:
    		static const unsigned int INVALID_ID = std::numeric_limits<unsigned int>::max();
    
    		Map( unsigned int size ) :
    			m_tiles( size * size, INVALID_ID ),
    			m_size( size )
    		{
    		}
    
    		void set( unsigned int x, unsigned int y, unsigned int id ) {
    			if( x >= m_size || y >= m_size ) {
    				throw std::invalid_argument( "x/y doof." );
    			}
    			else if( id == INVALID_ID ) {
    				throw std::invalid_argument( "id doof." );
    			}
    
    			m_tiles[y * m_size + x] = id;
    		}
    
    		void reset( unsigned int x, unsigned int y ) {
    			if( x >= m_size || y >= m_size ) {
    				throw std::invalid_argument( "x/y doof." );
    			}
    
    			m_tiles[y * m_size + x] = INVALID_ID;
    		}
    
    		unsigned int get( unsigned int x, unsigned int y ) const {
    			if( x >= m_size || y >= m_size ) {
    				throw std::invalid_argument( "x/y doof." );
    			}
    
    			return m_tiles[y * m_size + x];
    		}
    
    	private:
    		std::vector<unsigned int> m_tiles;
    		unsigned int m_size;
    };
    

    std::vector<unsigned int> ist der Einfachheit halber gewählt, es gibt den Fall auch mit größeren Ressourcen, die dann über Pointer rausgehen.
    Prinzipiell habe ich im Beispiel nun statt assert()s Exceptions genutzt und mich im get-Fall entschieden, einen Fehlerwert zurück zu geben, falls zwar die Koordinate korrekt ist, dort aber kein wirklicher Inhalt zu finden ist (INVALID_ID). Und falls wer meckert, dass das ja fast ein purer vector-Wrapper ist: Es gibt selbstverfreilich weitere Operationen. 😉

    Was haltet ihr davon?



  • Ich würde bei diesen Settern nur in der Debugversion mit asserts den Bereich und die IDs prüfen.

    In der Releaseversion haben derlei Überprüfungen imho nichts zu suchen.



  • Dann bau dir ein assert() dass eine Exception wirft?
    Wenn dein Programmdesign vom Testsystem bestimmt wird, dann ist das Testsystem Mist oder du verwendest es falsch, wenn du mich fragst.



  • sboc schrieb:

    Blöderweise verträgt sich das assert() mit Boost.Test, das ich einsetze, nicht so gut. Auf Exceptions testen ist einfacher.

    Du brauchst auch nicht testen, was passiert, wenn man falsch programmiert.



  • dot schrieb:

    Dann bau dir ein assert() dass eine Exception wirft?
    Wenn dein Programmdesign vom Testsystem bestimmt wird, dann ist das Testsystem Mist oder du verwendest es falsch, wenn du mich fragst.

    Könnte es nicht auch ein Hinweis darauf sein, dass das Design kein anständiges Testen zulässt? Ich schreibe zuerst Unittests, bevor ich den Produktiv-Code schreibe, und dort möchte ich halt auch Fehler provozieren, wo sie auftauchen sollen.

    Du brauchst auch nicht testen, was passiert, wenn man falsch programmiert.

    Ich denke schon, dass das sinnvoll ist. Ich möchte ja z.B. durch einen Testfall sicherstellen, dass eine Funktion bei falschen Werten fehlerhaft reagiert. Das wäre vergleichbar damit, als würde man nur testen, ob 15 / 5 = 3, aber nicht ob 15 / 0 = Fehler.

    Dass etwas bei anständigen Werten funktioniert, ist ja nur die halbe Miete. Oder aber ich mache es tatsächlich falsch, bin noch recht frisch beim test-driven development.



  • sboc schrieb:

    dot schrieb:

    Dann bau dir ein assert() dass eine Exception wirft?
    Wenn dein Programmdesign vom Testsystem bestimmt wird, dann ist das Testsystem Mist oder du verwendest es falsch, wenn du mich fragst.

    Könnte es nicht auch ein Hinweis darauf sein, dass das Design kein anständiges Testen zulässt? Ich schreibe zuerst Unittests, bevor ich den Produktiv-Code schreibe, und dort möchte ich halt auch Fehler provozieren, wo sie auftauchen sollen.

    Wenn du mich fragst soll das Testsystem sich dem Programm anpassen und nicht umgekehrt. Denn ich schreib das Programm ja nicht damit ich was zu testen hab, sondern aus anderen Gründen...
    Im konkreten Fall würd ich mal ganz definitiv sagen, dass mit Testsystem grob was im argen liegt, wenn es so grundlegende Entscheidungen wie das Verhalten einer Containerklasse beeinflusst oO



  • dot schrieb:

    Wenn du mich fragst soll das Testsystem sich dem Programm anpassen und nicht umgekehrt. Denn ich schreib das Programm ja nicht damit ich was zu testen hab, sondern aus anderen Gründen...

    Mit den Tests möchte ich auch nur sicherstellen, dass die Klasse sich genau so verhält wie gedacht. Das heißt, wenn ich die Tests schreibe, überlege ich, welche Fehler ich bei falschen Eingaben erwarte (neben den viel wichtigeren Erwartungen, was korrekterweise herauskommen soll, natürlich). Erst danach implementiere ich die Klasse nach diesem Schema.

    Im konkreten Fall würd ich mal ganz definitiv sagen, dass mit Testsystem grob was im argen liegt, wenn es so grundlegende Entscheidungen wie das Verhalten einer Containerklasse beeinflusst oO

    Ich weiß nicht genau, was du mit Beeinflussen meinst. Die Tests geben in meinem Fall das Design vor, da die Klasse so funktionieren soll wie die Tests sie nutzen (TDD, falls noch nicht bekannt).



  • sboc schrieb:

    dot schrieb:

    Wenn du mich fragst soll das Testsystem sich dem Programm anpassen und nicht umgekehrt. Denn ich schreib das Programm ja nicht damit ich was zu testen hab, sondern aus anderen Gründen...

    Mit den Tests möchte ich auch nur sicherstellen, dass die Klasse sich genau so verhält wie gedacht. Das heißt, wenn ich die Tests schreibe, überlege ich, welche Fehler ich bei falschen Eingaben erwarte (neben den viel wichtigeren Erwartungen, was korrekterweise herauskommen soll, natürlich). Erst danach implementiere ich die Klasse nach diesem Schema.

    Kann die Testumgebung denn nicht erwarten und testen, daß bei einem falschen Index ein assert zündet?
    Das wäre doch ok, dann könntest Du, wie es sich gehört, Programmierfehler mit assert begrenzen.



  • cooky451 schrieb:

    Shade Of Mine schrieb:

    Das Flag ist halt furchtbar, weil man nie weiss was jetzt passiert.

    Kannst du mir dafür mal ein Beispiel nennen?

    Klar:

    void foo(Klasse& obj) {
       obj.bar();
    }
    
    int main() {
       Klasse a;
       a.enable_exceptions(true);
       foo(a);
    
       Klasse b;
       b.enable_exceptions(false);
       foo(b);
    }
    

    Wie soll foo jetzt wissen wie es auf einen Fehler bei bar() reagieren soll? Jedesmal checken ob Exceptions aktiviert sind? Und wenn Exceptions aktiviert sind, wie ist dann isGood() oder failed() etc. definiert wenn eine Exception fliegt? Wird immer das fail bit immer gesetzt? Was wenn ich zwischen einem Fehler umschalte?

    Enorme Komplexität für keinen Mehrwert. Wenn du einfach sagst bar() wirft eine Exception, dann ist alles klar. Keine Probleme, keine Unsicherheit - auch robuster Code.

    Denn Stell dir vor du hast Unit Tests mit eingeschaltenen Exceptions und dann im Produktionscode werden sie irgendwo ausgeschalten. uU nur bei einem Objekt. Das führt zu unfindbaren Fehlern.

    Deshalb ist es ganz wichtig: ein Weg! Eine Art Fehler zu reporten. Nicht 2. Nicht 3. Nur eine Art. Weil sonst werden die anderen irgendwann/irgendwo einmal übersehen.



  • volkard schrieb:

    Kann die Testumgebung denn nicht erwarten und testen, daß bei einem falschen Index ein assert zündet?
    Das wäre doch ok, dann könntest Du, wie es sich gehört, Programmierfehler mit assert begrenzen.

    Hast Du völlig Recht, aber soweit ich weiß, bietet zumindest Boost.Test das nicht an (es gibt einen Weg über einen Hack, aber das ist immer so eine Sache..). Vielleicht schaue ich mich mal bei anderen Frameworks um, wie die das unterstützen.



  • cooky451 schrieb:

    bool set( std::size_t index, const Foo& value, bool throw_exception = false );
    const Foo* get( std::size_t index, bool throw_exception = false ) const;
    bool create( std::size_t index, bool throw_exception = false );
    

    Wenn die Klasse eh nur in einem größeren Kontext Sinn macht, in dem Exceptions auf höherer Ebene gefangen werden (z.B. in einem Spiel), dann kannst du auch gleich Variante 1 nehmen. Ansonsten weißt du nicht wie der "Nutzer" der Klasse diese Funktionen nutzen möchte, warum also nicht ihm die Wahl überlassen?

    Also abgesehen davon dass ich da beim hinschauen Kopfweh bekomm, widerspricht das doch rein prinzipiell komplett dem Sinn von Exceptions!?
    Der war doch eben genau dass man die Exception auf irgendeiner Ebene fangen kann und nicht immer direkt eine Ebene drüber behandeln muss. Mit deinem Parameter zwingst du aber erst wieder sämtliche Ebenen dazu, sich mit Exceptions zu befassen, nur halt von der anderen Richtung aus...



  • Du testest falsch. Wenn du z.B. den Zugriff aus Performancegründen so implementieren willst, dass die Gültigkeit von Parametern nicht überprüft wird, dann schreibts du in die Doku, dass falscher Zugriff zu undefiniertem Verhalten führt. Undefiniertes Verhalten brauchst du auch nicht testen. Wenn falsche Parameter zu einer Exception führen, dann schreibst du das in die Doku und testest es auch. Aber mach nicht überall Exceptions rein, nur damit du was zum Testen hast, sondern nur dann Exceptions verwenden, wenn es sinnvoll ist.



  • PS: Ging an sboc, niccht dot.


Anmelden zum Antworten