verstehe das mit const reference nicht...


  • Mod

    Don06 schrieb:

    rect(10, 20);
    

    erzeugt ein temporäres Object.

    richtig.

    Don06 schrieb:

    Temporäre Objekte sin in C++ immer konstant, sonst wäre zB sowas möglich:

    i++++++++++;
    

    nein. Weder sind temporäre Objekte per se (und auch nicht in diesem Falle) konstant, noch ist irgendetwas (vom Stil abgesehen) per se falsch mit i++++++++++

    rect(10, 20) ist ein rvalue. Da rect zudem eine Klasse ist, verweist dieses rvalue auf ein temporäres Objet vom Typ rect (es ist nicht const-qualifiziert). Nebenbei: rvalues skalarer Typen sind keine Objekte und daher niemals cv-qualifiziert; veränderbar ist nur Speicher und nur Objekte stellen Speicherbereiche dar - dieses rvalues sind nicht konstant in dem Sinne, dass ihr Typ const-qualifiziert wäre (es gibt selbstverständlich temporäre Objekte skalarer Typen, aber diese erscheinen niemals als rvalues).
    Der Ausdruck i++++++++ ist mit skalarem Operanden nicht möglich, da das Ergebnis des Postinkrements ein rvalue ist, und dieser Operator nur auf lvalues angewandt werden kann (die zudem modifizierbar=nicht-const sein müssen). Im Falle von Klassen müsste dieser Operator überladen werden - ob daher der Ausdurck i++++++++, falls i ein Klassenobjekt ist, möglich ist, hängt davon ab, wie ++ überladen wurde:

    Der Objektparameter nicht-statischer Memberfunktionen kann ein lvalue oder ein rvalue sein. Ein Referenz auf const T kann an ein (möglicherweise cv-qualifiziertes) rvalue vom Typ T gebunden werden - dabei wird ggf. ein temporäres Objekt erzeugt. Eine Referenz, deren Typ nicht als const T dargestellt werden kann (also T&, volatile T&, const volatile T&), kann dagegen nicht an ein rvalue vom Typ (cv) T gebunden werden. Diese Festlegung ist willkürlich und soll verhindern, dass Operationen, die ihr Argument verändern sollen, stillschweigend mit temporären Objekten arbeiten.
    Wenn unser Postinkrementoperator als Memberfunktion überladen wird, können wir nicht verhindern, dass das Argument ggf. ein rvalue (z.B. als Folge eines vorherigen Aufrufes) sein darf, man müsste dann tricksen, indem man etwa als Ergebnis dieser Funktion ein const T zurückgibt (was zufällig geht, weil rvalues eines Klassentyps gerade const-qualifiziert sein können). Der bessere Weg ist hier die Implementierung als freie Funktion mit einen Referenz als Parameter (oder man findet nichts dabei, rvalues inkrementieren zu können, was ich für durchaus haltbar halte).



  • @~john:
    Es ist _ganz sicher nicht_ "common sense" bei verletzten Invarianten einen "runtime_error" (!) zu werfen. Wenn dann gehört da ein "logic_error" geworfen, und auch das wird (gottseidank) in C++ nicht überall so gemacht.
    IMO ist die passende Reaktion auf eine verletzte Invariante immer noch std::terminate().

    BTW: OOP/OOD ist keine Silver-Bullet. Daher kann man auch nicht sagen dass etwas "falsch" oder "schlecht" ist nur weil es sich nicht mit OOP/OOD verträgt.



  • hustbaer schrieb:

    @~john:
    Es ist _ganz sicher nicht_ "common sense" bei verletzten Invarianten einen "runtime_error" (!) zu werfen.

    Das "common sense" bezog ich auf die Tatsache, daß eine Invariante Teil der Klasse zu sein hat.

    Wenn dann gehört da ein "logic_error" geworfen,

    Welche Art der Fehlerbehandlung man anwendet hängt doch sehr stark von der Problemstellung ab. Wenn dies fehlerhafte Nutzerdaten aus Dateien o.ä. sind, die ein Server zu verarbeiten hat, wird man diesen nur wegen kaputter Datensätze nicht terminieren, und einen Logikfehler hat es in so einem Fall ebenfalls nicht gegeben.

    Unter Umständen kann es ich so einem Fall (z.B. UNIX) auch sinnvoll sein, daß Programm mittels exit() und speziellem Fehlercode zu beenden (in dem man einen globalen Exceptionhandler installiert), so daß das aufrufenden Programm signalisiert bekommt, daß die Verarbeitung fehlschlug etc.



  • John, ich stimme auch mehr Konrad Rudolph zu. Bei einer allgemeinen Klasse Rect sollte es keine Invarianten-Abfrage innerhalb der Klasse geben, sonst müßte man ja x verschiedene Rechteck-Klassen erzeugen. Die Abfrage auf 100 bezog sich ja nur auf ein konkretes Objekt der Klasse.

    Und eine Invarianten-Abfrage würde man auch nicht im Konstruktor machen (denn dieser muß ja erst dafür sorgen, daß das Objekt richtig initialisiert wird), sondern erst in den Methoden.

    Du hast mit deiner Abfrage ja nur die Konstruktor-Parameter abgefragt, und dies wird ja durch den verwendeten Datentypen (int) geregelt.

    Außerdem dient eine Invariante nur dazu, daß sich alle Methoden der Klassen daran halten, und nicht der Anwender einer Klasse daran Schuld ist, sondern der Entwickler der Klasse.



  • Th schrieb:

    John, ich stimme auch mehr Konrad Rudolph zu. Bei einer allgemeinen Klasse Rect sollte es keine Invarianten-Abfrage innerhalb der Klasse geben, sonst müßte man ja x verschiedene Rechteck-Klassen erzeugen. Die Abfrage auf 100 bezog sich ja nur auf ein konkretes Objekt der Klasse.

    Es gibt ja die Möglichkeit Policy Classes zu verwenden. Insofern gibt es einen sauberen und trotzdem flexiblen Weg. Geschwindigkeitsgründe könnte gegen eine Prüfung sprechen, aber bei zeitkritischen Sachen muß man sowieso Kompromisse eingehen.

    Und eine Invarianten-Abfrage würde man auch nicht im Konstruktor machen (denn dieser muß ja erst dafür sorgen, daß das Objekt richtig initialisiert wird), sondern erst in den Methoden.

    Der Konstruktor muß ein 100% korrektes Objekt erzeugen. Daher wirst Du nicht umhin kommen, das Objekt schon während des Konstruktoraufrufs zu prüfen. Und was spricht dafür mehrere Methoden zu implementieren?

    Du hast mit deiner Abfrage ja nur die Konstruktor-Parameter abgefragt, und dies wird ja durch den verwendeten Datentypen (int) geregelt.

    Seit wann begrenzt ein int den Wert auf maximal 100? Und nächste Frage, wer stellt sicher, daß bestimmte Linearkombinationen der Parameter nicht ungültig sind? Ein einfaches Beispiel: eine Datumsklasse. Wenn man das richtig macht, muß allein für den 29.2. eines Jahres einige Regeln beachten.

    Außerdem dient eine Invariante nur dazu, daß sich alle Methoden der Klassen daran halten, und nicht der Anwender einer Klasse daran Schuld ist, sondern der Entwickler der Klasse.

    Eine Invariante dient dazu ein Objekt auf Plausibilität zu überprüfen, wenn es verändert wurde. Die Konstruktion eines Objekts ist trivialerweise ebenfalls eine Methode, die das Objekt verändert.



  • @~john:

    Das "common sense" bezog ich auf die Tatsache, daß eine Invariante Teil der Klasse zu sein hat.

    OK. Aber es ist sicher nicht common sense einer Allgemeinen Klasse wie "rect" die Beschränkung aufzuerlegen dass Instanzen maximal/minimal/whatever 100 Einheiten breit/hoch zu sein haben. Macht irgendwie keinen Sinn.

    Wenn an ein paar wenigen Stellen solche Rechtecke gebraucht werden, dann sollte man diese Beschränkung zu Invarianten der Klasse machen die diese Rechtecke als Member enthält, oder aber zu Preconditions der Methoden die diese Rechtecke übergeben bekommt.

    Welche Art der Fehlerbehandlung man anwendet hängt doch sehr stark von der Problemstellung ab.

    Nicht bei Invarianten, nein. Invarianten sind etwas was immer halten muss, und wenn diese verletzt werden liegt irgendwo ein Programmierfehler vor, und das Programm gehört abgebrochen.

    Der von dir skizzierte Fall stellt übrigens keine Prüfung einer Invariante dar, sondern die Prüfung der Preconditions des Constructors. Bzw. richtiger (siehe unten): definiertes Verhalten des Constructors.

    Wenn dies fehlerhafte Nutzerdaten aus Dateien o.ä. sind, die ein Server zu verarbeiten hat, wird man diesen nur wegen kaputter Datensätze nicht terminieren

    Das sind Bedingungen die gecheckt gehören bevor (!) man den State irgendeines Objektes manipuliert, d.h. bevor noch irgendwelche Invarianten verletzt werden könnten bloss weil ein File/Datensatz "kaputt" ist.

    Dinge wie "das Format der Daten in File XYZ stimmt mit Spec ABC überein" oder "die Daten die ein User eingibt machen Sinn" oder "File XYZ existiert" haben mit Invarianten *garnichts* zu tun, da es Dinge sind die ein Programm nicht erzwingen/verhindern/beeinflussen kann. Daher gehören diese Dinge auch an anderer Stelle geprüft und anders behandelt als die Verletzung von Invarianten.

    und einen Logikfehler hat es in so einem Fall ebenfalls nicht gegeben.

    Doch, wenn der entsprechende Test fehlt der den Fehler erkennen hätte müssen bevor Invarianten verletzt werden, dann ist das ein Logikfehler.

    Unter Umständen kann es ich so einem Fall (z.B. UNIX) auch sinnvoll sein, daß Programm mittels exit() und speziellem Fehlercode zu beenden (in dem man einen globalen Exceptionhandler installiert), so daß das aufrufenden Programm signalisiert bekommt, daß die Verarbeitung fehlschlug etc.

    Ob nun exit, abort oder terminate ... auf jeden Fall etwas in der Art.

    ----

    Der Sinn hinter dem ganzen ist einfach: unterschiedliche Fehler gehören unterschiedlich behandelt. Wenn man nun aber eine Funktion die Invarianten checkt misbraucht um Laufzeitfehler zu behandeln nimmt man sich diese Möglichkeit.

    ----

    Noch etwas: auf Verletzung von "Preconditions" mit einer Exception zu reagieren (*) ist etwas was man sich gut überlegen sollte. Das Problem bei der Sache ist nämlich dass man dieses Verhalten damit zum Teil des Interface macht gegen das andere Leute programmieren. Ob das Verhalten dokumentiert ist oder nicht macht dabei kaum einen Unterschied - Leute werden sich sobald sie das Verhalten einmal beobachtet haben darauf verlassen dass es immer so bleibt. In bestimmten Fällen mag das OK sein, in anderen Fällen wird man sich damit aber böse Probleme einhandeln, z.B. wenn man die ganzen Checks aus Performance-Gründen im Release-Build deaktivieren will. Was dann oft nichtmehr möglich ist, weil sich dann u.U. schon hunderte Stellen auf das alte Verhalten verlassen.

    (*) Wenn man es so macht, dann prüft man genaugenommen keine Preconditions mehr (daher die "" im Absatz oben), sondern definiert die Funktion/Methode so dass sie keine Preconditions mehr hat, dafür aber auf bestimmte Fälle mit einer bestimmten Exception reagiert.

    std::vector::at ist so ein Fall - es ist _keine_ Precondition von std::vector::at dass der übergebene Index kleiner size() ist. Wäre es eine Precondition würde es einen Programmierfehler darstellen einen zu grossen Wert zu übergeben. Tut es aber nicht, das Verhalten in so einem Fall ist genau dokumentiert (=es wird eine Exception geworfen), und damit ist es "OK" wenn man einen zu grossen Index übergibt. So eine Funktion darf dann auch gerne verwendet werden um z.B. mit "ungecheckten" Eingabedaten zu arbeiten.

    Die Funktion mit Precondition "index < size()" gibt es auch, das wäre der operator []. Hier stellt es einen Programmierfehler dar, und korrekterweise wird hier auch in den meisten Implementierungen assert() verwendet um den Fehler zu prüfen/behandeln, und keine Exception geworfen.



  • Eine Invariante dient dazu ein Objekt auf Plausibilität zu überprüfen, wenn es verändert wurde. Die Konstruktion eines Objekts ist trivialerweise ebenfalls eine Methode, die das Objekt verändert.

    Genau aus dem Grund darf man auf verletzte Invarianten auch nie mit einer Exception regaieren, da das Objekt an der Stelle schon "kaputt" ist.
    Wirft man eine Exception kann der aufrufende Code munter mit dem bereits kaputten Objekt weiterarbeiten (sofern es sich eben nicht gerade um einen Konstruktur handelt, sondern um einen ganz normalen Mutator).
    Garnicht gut.

    Und wie aus meinem Posting von gerade eben schon ersichtlich sein müsste: IMO darf man es garnicht so weit kommen lassen dass ein Objekt erst kaputt wird.



  • hustbaer, wir verstehn uns 😉



  • hustbaer schrieb:

    @~john:

    ~john schrieb:

    Das "common sense" bezog ich auf die Tatsache, daß eine Invariante Teil der Klasse zu sein hat.

    OK. Aber es ist sicher nicht common sense einer Allgemeinen Klasse wie "rect" die Beschränkung aufzuerlegen dass Instanzen maximal/minimal/whatever 100 Einheiten breit/hoch zu sein haben. Macht irgendwie keinen Sinn.

    Natürlich hat das sehr viel Sinn, denn "rect100" und "rect" sind nicht zuweisungskompatibel, man kann zwar jedes "rect100" in ein "rect" konvertieren, aber umgekehrt geht dies möglicherweise nicht mehr. Wenn Du keine zwei Typen verwendest sondern nur einen Typen, dann kann es passieren, daß Du einem "rect100" irrtümlich ein "rect" zuweist. In so einem Fall hast Du natürlich einen Logikfehler, den man über die starke Typisierung schon während der Übersetzung hätte abfangen können. Resourcenmangel irgend einer Form kann einem natürlich dazu bewegen für die Lösung "ist ein Typ" zu sein, aber das ist ein Kompromiß und kein sauberer Entwurf.

    hustbaer schrieb:

    ~john schrieb:

    Welche Art der Fehlerbehandlung man anwendet hängt doch sehr stark von der Problemstellung ab.

    Nicht bei Invarianten, nein. Invarianten sind etwas was immer halten muss, und wenn diese verletzt werden liegt irgendwo ein Programmierfehler vor, und das Programm gehört abgebrochen.

    Das ist definitiv falsch. Im Embedded Bereich ist so ein Verhalten tödlich und das möglicherweise sogar im Wortsinne. Ada zum Beispiel kennt gar kein Assert, und das ist die Programmiersprache, die für die härtesten Einsatzbedingungen zertifiziert ist. Es ist vollkommen inakzeptabel, wenn insbesondere eine LowLevel Komponente das ganze Programm ins Verderben reißt. Richtiges Fehlerhandling ist leider ein Thema, das gerne ausgespart wird. Exceptions haben das Problem grundsätzlich vereinfacht, nur leider wird gerne so ein Unfug wie "catch (...)" verwendet, weil man Fehler einfach "wegdrücken" will. Im Grunde ist das simpel, Exceptions, die man nicht kennt, die behandelt man gar nicht und propagiert sie einfach weiter.

    hustbaer schrieb:

    Das sind Bedingungen die gecheckt gehören bevor (!) man den State irgendeines Objektes manipuliert,

    Dazu braucht es einen Parser und wenn man diesen in OOP-Stil runterprogrammiert muß man die Stati sehr vieler Objekte verändern. Wenn man einen Bug im Parser hat, und dieser nur in Zusammenhang mit falschen Dokumenten auftritt und so in der Testphase nicht auftrat, dann ist das kein Grund das Programm per abort abzuschießen.

    hustbaer schrieb:

    Ob nun exit, abort oder terminate ... auf jeden Fall etwas in der Art.

    Korrektur return aus der main Funktion mit Fehlercode war gemeint. Zur Not auch die Exception gar nicht abfangen und so das Programm beenden lassen. abort, exit, terminate haben allesamt das gleiche Problem, sie rufen keine Destruktoren für globale statische Objekte auf, womit man sich einen Rattenschwanz von Problemen einhandeln kann.

    hustbaer schrieb:

    Noch etwas: auf Verletzung von "Preconditions" mit einer Exception zu reagieren (*) ist etwas was man sich gut überlegen sollte.

    Da gibt es nichts zu überlegen, der Kontrakt ist verletzt, so daß man mit einer Exception diese Kontraktsverletzung anzeigt. Wie schon mehrfach erwähnt kann es aus Resourcengründen notwendig sein auf das Testen von Prä- und Postkonditionen zu verzichten, in Rahmen von Softwaretests kann es daher angebracht sein, assertions zu verwenden. Aber das ist eine Maßnahme unter besonderen Rahmenbedingungen und kein allgemeiner Designgrundsatz.

    hustbaer schrieb:

    Das Problem bei der Sache ist nämlich dass man dieses Verhalten damit zum Teil des Interface macht gegen das andere Leute programmieren. Ob das Verhalten dokumentiert ist oder nicht macht dabei kaum einen Unterschied - Leute werden sich sobald sie das Verhalten einmal beobachtet haben darauf verlassen dass es immer so bleibt.

    Hier redest Du über ein Problem des Social Engineerings, und nicht über ein Problem des Designs einer Applikation.

    hustbaer schrieb:

    std::vector::at ist so ein Fall - es ist _keine_ Precondition von std::vector::at dass der übergebene Index kleiner size() ist.

    Natürlich ist das eine Precondition. Der Grundgedanke von DBC ist es, solche Bedingungen zu testen, entweder nur während der Entwicklungs- und Testphase oder permanent im Produktionscode. Ein std::out_of_range zeigt einen Bug im Programm an. Nur weil das Programm an dieser Stelle nicht mit abort wegschmiert, heißt das nicht, daß hier eine Precondition nicht verletzt worden wäre. Ohne DBC hat man die Preconditions nur implizit angenommen und sie nicht ausformuliert. Mit den leider bekannten negativen Folgen. Mit DBC werden die Preconditions explizit getestet, und nein Bertrand Meyer (Eiffel Erfinder und Proponent des DBCs) findet das Abschießen eines Programmes gar nicht gut.



  • hustbaer schrieb:

    ~john schrieb:

    Eine Invariante dient dazu ein Objekt auf Plausibilität zu überprüfen, wenn es verändert wurde. Die Konstruktion eines Objekts ist trivialerweise ebenfalls eine Methode, die das Objekt verändert.

    Genau aus dem Grund darf man auf verletzte Invarianten auch nie mit einer Exception regaieren, da das Objekt an der Stelle schon "kaputt" ist.

    Insbesondere im Bereich von Hochverfügbarkeitslösungen darf man gar nicht anders reagieren. Die Entscheidung, ob man das Programm in einen definierten Zustand überführen kann, wird nicht von Komponenten in niederen Hierachieebenen getroffen. Aber genau dies erklärst Du hier zur Maxime.

    hustbaer schrieb:

    Wirft man eine Exception kann der aufrufende Code munter mit dem bereits kaputten Objekt weiterarbeiten (sofern es sich eben nicht gerade um einen Konstruktur handelt, sondern um einen ganz normalen Mutator).
    Garnicht gut.

    Du schreibst hier wieder über Social Engineering, und nicht über Softwaredesign. Du verwendest abort in der Invarianten als Erziehungsmaßnahme. Das Problem ist mir bekannt, trotzdem ist das kein gutes Design.

    hustbaer schrieb:

    Und wie aus meinem Posting von gerade eben schon ersichtlich sein müsste: IMO darf man es garnicht so weit kommen lassen dass ein Objekt erst kaputt wird.

    Der Witz ist gut: Errare humanum est. Alternativ als Lösungsvorschlag: Wie wäre es mit SPARK?


Anmelden zum Antworten