verstehe das mit const reference nicht...



  • Temporäre Objekte sin in C++ immer konstant
    

    ah langsam verstehe ich wohl...

    int a = 1;
    //1 ist logisch eine numerische Konstante - es gibt überhaupt keine Möglichkeit oder Sinn 1 direkt zu ändern.

    rect r = rect(10,20);
    //rect(10,20) ist ein konstantes objekt das nach r kopiert wird... rect(10,20) selbst kann man nicht ändern...



  • Bei einer Wertübergabe dürfte der Compiler das temporäre Objekt einspaaren, versuchs doch mal so 😉

    EDIT://
    btw. wenn du auf die Idee kommst das ganze zu const_casten bekommst du AFAIR undefiniertes Verhalten 😉



  • Wenn du einen Parameter "korrigieren" willst musst du schon selbst eine Kopie anlegen:

    void foo(const rect& r)
    {
        rect myRect = r;
        myRect.width = 100;
        //...
    }
    


  • Bei einer Wertübergabe dürfte der Compiler das temporäre Objekt einspaaren, versuchs doch mal so

    du meinst so:

    void foo(rect r) {}

    foo(rect(10,20));

    ich denke da wird kopiert - es müsste 1 Objekte im Speicher entstehen... rect(10,20) wäre eine konstante das sowieso im programm ist.

    Bei einer Referenz würde kein Objekt entstehen, sondern man hätte die Konstante rect(10,20) gehabt.



  • TheShadow2000 schrieb:

    rect r = rect(10,20);
    //rect(10,20) ist ein konstantes objekt das nach r kopiert wird... rect(10,20) selbst kann man nicht ändern...

    Genau, und wenn Du das Teil als Wert übergibst, statt als (konstante) Referenz, passiert dank der Intelligenz moderner Compiler (hoffentlich) das gleiche - das Objekt, auf das Du in der Funktion zugreifst, wird direkt aus den Koordinaten initialisiert. Dann kannst Du es in der Funktion nach belieben korrigieren, benutzen und dann wegwerfen (lassen). Voraussetzung ist natürlich, dass die Klasse die üblichen Kriterien für die Kopierbarkeit erfüllt.

    Und wieder einmal gilt: Bitte keine Kopierphobie entwickeln. :xmas1:



  • TheShadow2000 schrieb:

    class rect
    {
        public:
        int width;
        int height;
    
        rect(int width, int height)
        {
            this->width=width;
            this->height=height;
        }
    };
    
    void foo(rect& r)
    {
      if (r.width>100) r.width=100;
      if (r.height>100) r.height=100;
      //..
    }
    
    int main()
    {
        foo(rect(10,20));  //<<<FEHLER
        return 0;
    }
    

    Das ist doch schon sehr nah dran, das einzige Prolem ist wie schon erwähnt das temporäre Objekt. Wenn die Funktion was überprüfen soll, braucht du das Objekt doch sowieso außerhalb wieder, also schreibt man doch einfach:

    int main()
    {
        rect myrect;
        foo(myrect); 
        return 0;
    }
    

    Oder worum geht es hier?



  • class rect
    {
        typedef unsigned int pos_t;
        typedef std::pair <pos_t, pos_t> point_t;    
    
    protected:
        point_t m_topleft;
        point_t m_bottomright;
    
    public:
        rect(pos_t x, pos_t y, std::size_t cx, std::size_t cy)
            : m_topleft(std::make_pair(x, y)), m_bottomright(std::make_pair(x + cx, y + cy))
        {}
        rect(point_t const& topleft, point_t const& bottomright)
            : m_topleft(topleft), m_bottomright(bottomright)
        {}
    
    public:
        std::size_t get_width() const { return m_bottomright.first - m_topleft.first; }
        std::size_t get_height() const { return m_bottomright.second - m_topleft.second; }
        point_t get_x const { return m_topleft.first; }
        point_t get_y const { return m_topleft.second; }
    };
    

    😃 so sieht die Klasse doch net aus 😉

    void foo(rect const& rc)
    {
        const std::size_t rc_width(std::min<std::size_t>(rc.get_width(), 100));
        const std::size_t rc_height(std::min<std::size_t>(rc.get_height(), 100));
        // ...
    }
    
    int main()
    {
        foo(rect(0, 0, 10, 20));
    }
    

    fertig! 😃



  • was ist point_t und std::pair? welche includes braucht man da?



  • xBlackKnightx schrieb:

    was ist point_t

    (D)Evil schrieb:

    typedef unsigned int pos_t;
        typedef std::pair <pos_t, pos_t> point_t;
    

    und std::pair? welche includes braucht man da?

    #include <utility>
    


  • TheShadow2000 schrieb:

    ja ich verstehe dass es ein temporäres objekt ist... das ist ok... warum aber sollen die funktionen so ein objekt nicht ändern dürfen?

    Das wiederspricht dem Gedanken der OOP. Stichwort Kapselung von Daten

    was ist, wenn foo feststellt, dass z.B. rect über der Grenze liegt und dann einen parameter korrigiert, etwa:

    Dann ist das schlechter Programmierstil. So wäre es deutlich besser:

    #include <stdexcept>
    
    class rect {
        int width_;
        int height_;
    public:
        void invariant () const {
            if ((100 < width_) || (100 < height_ ))
                 throw std::runtime_error ("rect::invariant violated");
        }
        rect(int const width, int const height) : width_(width), height_(height) {
            this->invariant();
        }
    };
    


  • ~john schrieb:

    TheShadow2000 schrieb:

    ja ich verstehe dass es ein temporäres objekt ist... das ist ok... warum aber sollen die funktionen so ein objekt nicht ändern dürfen?

    Das wiederspricht dem Gedanken der OOP. Stichwort Kapselung von Daten

    Hä? Was hat Kapselung bitte mit Veränderbarkeit von Daten zu tun? Das sind zwei unterschiedliche Paar Schuhe.

    was ist, wenn foo feststellt, dass z.B. rect über der Grenze liegt und dann einen parameter korrigiert, etwa:

    Dann ist das schlechter Programmierstil. So wäre es deutlich besser:

    Nein, so wäre es nicht besser, denn so erfüllt es seinen Zweck nicht. Du hast jetzt die Invariante fest in der rect-Klasse verdrahtet. Wahrscheinlich sollte das aber gar nicht passieren, sondern die Invariante sollte nur für 'foo' gelten. Abgesehen davon benutzt Du sowieso eine ganz andere Semantik (Fehler werfen). Wer sagt Dir, dass Deine Semantik pauschal besser geeignet ist? Es gibt Situationen für beide Szenarien.



  • Konrad Rudolph schrieb:

    Hä? Was hat Kapselung bitte mit Veränderbarkeit von Daten zu tun? Das sind zwei unterschiedliche Paar Schuhe.

    Nein, das hängt direkt zusammen. Die Prüfung, ob ein Exemplar einer Klasse ein gültiges Exemplar einer Klasse ist gehört definitiv nicht aus der Klasse ausgelagert! Nach jeder Methode, die ein Objekt verändert gehört automatisch die Invariante aufgerufen, so daß die Korrektheit der Objekte gewährleistet ist. Wenn man die Invariante aus der Klasse auslagert, kann man diese Korrektheit nicht mehr garantieren und die Fehlerquellen nehmen stark zu. -> schlechtes Design
    Die Sache ist eindeutig zu beanworten, weil hier eine Verständnisfrage gestellt wurde und keine Anleitung für die Wartung von Altcode gesucht wurde.

    Nein, so wäre es nicht besser, denn so erfüllt es seinen Zweck nicht. Du hast jetzt die Invariante fest in der rect-Klasse verdrahtet.

    Nur so ist es korrektes OOD. "foo" erfüllt in ursprünglichen Variante ausschließlich die Aufgabe einer Invariante. Ergo, gibt es auch kein Begründung für die Auslagerung der Invarianten.

    Wer sagt Dir, dass Deine Semantik pauschal besser geeignet ist?

    Das ist mittlerweile "common sense" in der C++ Programmierung. Nachzulesen in den diversen C++ Lehrbüchern z.B. aus der "C++ in Depth Series", den diversen Lehrbüchern über OOA, OOD und OOP.



  • (D)Evil, du solltest deinen Codestil mal überdenken


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


Anmelden zum Antworten