Kopien und Zuweisungen von Klassenobjekten



  • Huhu 🙂

    ich beschäftige mich gerade mit Zuweisungen und Kopien von Klassenobjekten und frage mich, wie das in Subroutinen gehen soll.
    Als Testbeispiel habe ich folgendes Konstrukt:

    #include <iostream>
    #include <vector>
    
    class A {
    public:
        A(const int dx, const int dy) : x(dx), y(dy) {}
        A(const A& einA) :x(einA.x), y(einA.y) {}
        void swapi(A& einA);
        A &operator=(A& einA)  {swapi(einA); return *this;}
        int getX() const {return x;}
        int getY() const {return y;}
    private:
        int x;
        int y;
    };
    
    void A::swapi(A& einA) {
        std::swap(x,einA.x);
        std::swap(y,einA.y);
    }
    
    void wechseln(std::vector<A> foo) {
        A foo2(3,4);
        foo[0]=foo2;
    }
    
    int main () {
        std::vector<A> foo;
        foo.push_back(A(1,2));
        std::cout << foo[0].getX() << " " << foo[0].getY() << "\n";
        A foo3(4,5);
        foo[0]=foo3;
        std::cout << foo[0].getX() << " " << foo[0].getY() << " <- geht! \n";
        wechseln(foo);
        std::cout << foo[0].getX() << " " << foo[0].getY() << " <- geht nicht! \n";
    
        return 0;
    }
    

    Mit dem Kopierkonstruktur und Zuweisungskontruktur bin ich noch am üben. Ich bin mir nicht sicher, ob die so richtig sind. 🙂
    Außerdem: Die Regel der Drei ist immer zu befolgen. Aber wenn ich da einen Destruktur hinsetzen würde, wäre der nicht genauso wie der Std-Destruktor? Oder wie genau muss er in dem Fall aussehen?
    Mit der Dreierregel bin ich auch noch verwirrt. Manchmal gilt sie, dann heißt es "blos nicht den Destruktor aufrufen" ... öhm ... ^^''

    Das eigentliche Problem ist jetzt die Zuweisung: foo[0]=foo2.
    Wenn ich das in main mache, klappt das wunderbar. In einer Subroutine, wo ein temporäres Objekt noch liegt aber nicht. Dabei wird doch nur der Inhalt des temporeren Objektes kopiert?! Dass es temporär ist, sollte es eigentlich nicht jucken .....

    Verwirrend ... 🙂

    Vielen Dank schonmal für die Hilfe.

    PS: ich muss heute Abend noch aus dem Haus. Wenn ich kurzzeitig nicht antworte, dann liegt es daran, dass ich aufm Handy zwar mitlese, aber nicht testen kann.

    MERCI! 🙂



  • Dein Zuweisungsoperator ist falsch. Für Copy n Swap musste erst einA kopieren oder direkt byValue übergeben.
    Ansonsten gilt die Dreierregel. Wenn bei einem der drei der Standardgenerierte nicht ausreicht,brauchste auch die anderen, in deinem Fall also keinen von denen, da der Compiler ähnlichen Code schreibt.
    Haste eine. benutzerdefinierten Destruktor, rufste ihn natürlich nicht auf, das geschieht automatisch.



  • die dreierregel ist meinem wissen nach keine echte regel von der sprache c++ aus sondern nur eine faustregel, da *oft* wenn man einen der drei funktionen manuell schreibt, die automatisch generierten versionen der beiden anderen funktionen nicht das tun, was man gerne hätte.

    zu deinem code:
    linie 9 müsste so aussehen:

    A &operator=(A einA)  {swapi(einA); return *this;}
    //           ^ keine referenz, sondern call-by-value
    // damit wird automatisch der kopierkonstruktor aufgerufen
    

    compiler können teils bei kopien im funktionskopf besser optimieren als wenn man eine kopie von einer (const&) im rumpf machen würde.

    linie 22 müsste *vielleicht" so aussehen:

    void wechseln(std::vector<A>& foo) {
    

    ein konzept mit iteratoren wäre 10x besser.
    edit: habe nicht richtig gelesen, hier wäre es einfach am besten ein einzelnes objekt mit call-by-reference zu übergeben (da du ja eh nur eins an einer definierten stelle modifizierst)...

    mfg



  • igno schrieb:

    die dreierregel ist meinem wissen nach keine echte regel von der sprache c++ aus sondern nur eine faustregel, da *oft* wenn man einen der drei funktionen manuell schreibt, die automatisch generierten versionen der beiden anderen funktionen nicht das tun, was man gerne hätte.

    Die Ausnahme ist zu vernachlässigen.



  • instancecounting schrieb:

    igno schrieb:

    die dreierregel ist meinem wissen nach keine echte regel von der sprache c++ aus sondern nur eine faustregel, da *oft* wenn man einen der drei funktionen manuell schreibt, die automatisch generierten versionen der beiden anderen funktionen nicht das tun, was man gerne hätte.

    Die Ausnahme ist zu vernachlässigen.

    struct base
    {
        virtual ~base()
        {
        }
        // ...
    };
    

    ?



  • Seit C++11 schreibt man:

    virtual ~foo() = default;
    

    Das macht deutlich, dass die vom Compiler generierte Version ausreicht, was sie ja auch tut. Nur die Signatur soll anders sein.



  • Vielen Dank euch allen!
    So funktionierts! 🙂

    Ich muss das morgen aber noch an einem anderen Beispiel nochmal ausprobieren, da kommt bestimmt noch eine Frage 😉

    Bisher schonmal allen vielen dank!! 🙂



  • instancecounting schrieb:

    Seit C++11 schreibt man:

    virtual ~foo() = default;
    

    Das macht deutlich, dass die vom Compiler generierte Version ausreicht, was sie ja auch tut. Nur die Signatur soll anders sein.

    Ich bin aber faul.



  • Hä? ^^
    Ich kann euch grad nicht so ganz folgen...: Wann muss ich den Destruktor angeben, wann reicht der default-Destruktor? ^^

    Eine Sache ist mir klar:
    Wenn von der Basisklasse abgeleitet wird, bekommt die Basisklasse einen virtuellen Destruktor.
    Das heißt dann aber noch nicht, dass es die Dreierregel braucht.
    Anm.: Jede abgeleitete Klasse hat ihre eigenen Zuweisungs und Kopierkonstruktoren, oder? Keine gemeinsame virtuelle?

    Andersrum:
    Wenn ich wie hier sogar nur zwei von den dreien benutze, braucht es noch keinen Destruktor, weil der den ich angeben würde genauso wie der default-Destruktor aussehen wurde?

    Von welchen Ausnahmen redet ihr? Ausnahmen zur Regel "Dreierregel immer anwenden" oder ist die Dreierregel jetzt wieder eine Ausnahme ....?



  • Lymogry schrieb:

    Wann muss ich den Destruktor angeben, wann reicht der default-Destruktor?

    Der Defaultkonstruktor tut genau gar nichts. Wenn das ok ist ist es ok, ansonsten halt nicht.

    Lymogry schrieb:

    Anm.: Jede abgeleitete Klasse hat ihre eigenen Zuweisungs und Kopierkonstruktoren, oder? Keine gemeinsame virtuelle?

    Richtig. Das ist sogar ein ziemlich großes Problem. Ich erwarte dass das geändert wird.

    Die Dreierregel ist übrigens inzwischen eine 5er-Regel. Davon gibt es halt Ausnahmen, häufig beim Konstruktor, der nur ein paar Werte initialisiert, und virtuellem Destruktor. Das Prinzip bleibt aber, immer wenn du von der Regel abweichst solltest du es dir genau überlegen.


  • Mod

    Ich kann euch grad nicht so ganz folgen...: Wann muss ich den Destruktor angeben, wann reicht der default-Destruktor? ^^

    Wenn irgendetwas bei der Zerstörung des Objekts passieren soll, definierst du einen Destruktor. Einfach, oder?

    Wenn von der Basisklasse abgeleitet wird, bekommt die Basisklasse einen virtuellen Destruktor.

    Nicht immer. Nur wenn das ganze polymorph sein soll.

    Jede abgeleitete Klasse hat ihre eigenen Zuweisungs und Kopierkonstruktoren, oder? Keine gemeinsame virtuelle?

    Interessanter Nebenfakt: Du kannst einen virtuellen Zuweisungsoperator deklarieren. Der Parameter muss immer gleichbleiben (daher musst du ggf. immer einen zweiten mit dem konkreten Typen definieren), die zurückgegebene Referenz darf aber immer den konkreten Typen haben (covariant return types).

    Von welchen Ausnahmen redet ihr? Ausnahmen zur Regel "Dreierregel immer anwenden" oder ist die Dreierregel jetzt wieder eine Ausnahme ....?

    Die Dreierregel ist keine Ausnahme, sondern eine immer zu berücksichtigende Faustregl. Wenn du ein wenig erfahrener wirst, passiert alles von selbst.



  • nwp3 schrieb:

    Lymogry schrieb:

    Wann muss ich den Destruktor angeben, wann reicht der default-Destruktor?

    Der Defaultkonstruktor tut genau gar nichts. Wenn das ok ist ist es ok, ansonsten halt nicht.

    falsch und nicht auf die quote zutreffend. der defaultkonstruktor ruft die defaultkonstruktoren von allen basisklassen und von allen membern auf (in dieser reihenfolge). der defaultdestruktor ruft die destruktoren von allen membern und von allen basisklassen auf (auch in dieser reihenfolge).



  • nwp3 schrieb:

    Lymogry schrieb:

    Anm.: Jede abgeleitete Klasse hat ihre eigenen Zuweisungs und Kopierkonstruktoren, oder? Keine gemeinsame virtuelle?

    Richtig. Das ist sogar ein ziemlich großes Problem. Ich erwarte dass das geändert wird.

    ein virtueller Copy-Konstruktor kann doch ganr nicht funktionieren oder? Die haben doch alle unterschiedliche Signaturen, was ja auch nötig ist, da man den exakten Typ der Kopie zur Compile-Zeit benötigt.


  • Mod

    Das ist sogar ein ziemlich großes Problem.

    Wo ist da ein großes Problem? Das lässt sich alles durch kleine Workarounds per virtuellem clone() o.ä. lösen.



  • ähm jungs..... 😃 ich hab mal den Test gemacht und den Zuweisungs- und Kopierkonstruktor auskommentiert: es funktioniert immernoch 😮

    Was sagt mir das?
    Dass meine Klasse gerade so einfach ist, dass die defaults das auch können? Oder ist mein Compiler grad so schlau? Ist es sicherer, wenn man die mitangibt, wirds dann Compilerunabhängiger?

    Hmm.. irgendwie hab ich im Gedächtnis, dass das gestern ohne die Dinger nicht funktioniert hat. Was hat sich verändert, ich weiß es nicht ..... lol ...

    #include <iostream>
    #include <vector>
    
    class A {
    public:
        A(const int dx, const int dy) : x(dx), y(dy) {}
     //   A(const A& einA) :x(einA.x), y(einA.y) {}
     //   A &operator=(A einA)  {swapi(einA); return *this;}
        void swapi(A& einA);
        int getX() const {return x;}
        int getY() const {return y;}
    
    private:
        int x;
        int y;
    };
    
    void A::swapi(A& einA) {
        std::swap(x,einA.x);
        std::swap(y,einA.y);
    }
    
    void wechseln(std::vector<A>& foo) {
        A foo2(3,4);
        foo[0]=foo2;
    }
    
    void wechseln2(std::vector<A*> boo) {
        A foo2(7,8);
        *(boo[0])=foo2;
    }
    
    int main () {
        std::vector<A> foo;
        foo.push_back(A(1,2));
        std::cout << foo[0].getX() << " " << foo[0].getY() << "\n";
        A foo3(4,5);
        foo[0]=foo3;
        std::cout << foo[0].getX() << " " << foo[0].getY() << " <- geht! \n";
        wechseln(foo);
        std::cout << foo[0].getX() << " " << foo[0].getY() << " <- geht! \n";
    
        std::vector<A*> boo;
        boo.push_back(&foo[0]);
        std::cout << boo[0]->getX() << " " << boo[0]->getY() << "  \n";
        wechseln2(boo);
        std::cout << boo[0]->getX() << " " << boo[0]->getY() << " <- geht! voll cool!  \n";
    
        return 0;
    }
    

    Ich hatte hier mal eine Frage zu einer Klasse gestellt, wo die Klasse ihre eigenen Elemente mitgezählt hat. Da war dann in der {} Klammer noch eine Anweisung. Hängt das mit den {} Klammern zusammen? Dass die Dreier/Fünferregel gerade in den Fällen notwendig ist, wo zusätzlich zur Initialisierung noch was getan werden muss?


  • Mod

    Ist es sicherer, wenn man die mitangibt, wirds dann Compilerunabhängiger?

    Das sicher nicht.

    Dass meine Klasse gerade so einfach ist, dass die defaults das auch können?

    Richtig - die implizit definierten Versionen tun das gleiche.



  • Lymogry schrieb:

    ähm jungs..... 😃 ich hab mal den Test gemacht und den Zuweisungs- und Kopierkonstruktor auskommentiert: es funktioniert immernoch 😮

    Was sagt mir das?

    Dass meine Klasse gerade so einfach ist, dass die defaults das auch können? Oder ist mein Compiler grad so schlau? Ist es sicherer, wenn man die mitangibt, wirds dann Compilerunabhängiger?

    Jeder Compiler generiert Code, der genau das macht, was du geschrieben hast.

    Ich hatte hier mal eine Fragei zu einer Klasse gestellt, wo die Klasse ihre eigenen Elemente mitgezählt hat. Da war dann in der {} Klammer noch eine Anweisung. Hängt das mit den {} Klammern zusammen? Dass die Dreier/Fünferregel gerade in den Fällein notwendig ist, wo zusätzlich zur Initialisierung noch was getan werden muss?

    Wenn nachdem Kopieren etwas lgetan werden muss, muss das idR im Destruktor
    rückgängig gemacht werden.



  • Supi!

    Dank eurer Hilfe wird mir immer einiges klarer.
    DANKE EUCH ALLEN! 🙂



  • FRAAGEE! 🙂

    Wie ihr im aktuellen Code seht, gibt es einen Vektor

    std::vector<A*> boo
    

    wo die Adressen von irgendwelchen A Klassenelementen liegen.
    Diese Inhalte kann man jetzt einfach überschreiben: wechseln2

    Aber was passiert, wenn die Größe von diesem Vektor in einer Subroutine vergrößert werden muss?
    Sowas geht natürlich nicht:

    void tuwas(std::vector<A*> boo) {
        boo.push_back(A*(1,2));  // das geht so nicht! 
        A foo3(9,10);
        boo.push_back(&foo3);   // so geht das natürlich auch nicht! 
    }
    

    Das Problem ist, dass dort alles erstmal temporär ist. Kommt man an einen Platzhalter oder so dran? 🙂


  • Mod

    Nein. Um die Lebenszeit des Objektes dynamisch zu machen, nutzt man new .

    boo.push_back( new A(1,2) );
    

    Allerdings gibt es hier ein Problem: Wann wirst du den Speicher wieder freigeben?


Log in to reply