Wie derived class, base class member zuweisen?



  • Hallo,

    dies ist eine Anfänger Frage. Bitte daher die Lösung nicht zu kompliziert denken. Mir geht es um die zu bevorzugende Standard Lösung für die Aufgabe:

    Einer derived class sollen in einem Schritt die base class Member (es sind viele) zugewiesen werden.

    Bislang habe ich das mit Methode 1 + 2 (siehe code) getan, was auch funktioniert. Mangels Erfahrung ist mir aber nicht klar, welcher dieser beiden Methoden (in einem umfangreichen Projekt) generell der Vorzug zu geben ist und ob dabei Fallstricke drohen.

    Bei Methode 1 stört mich ein wenig, dass auf den ersten Blick nicht gleich of­fen­sicht­lich ist was gemacht wird. Bei Methode 2b weiss ich nicht, ob "using operator =" der Weisheit letzter Schluss ist.

    Unklar ist mir auch, ob es nicht noch eine ganz andere bessere Lösung für die Aufgabe gibt?

    Minimal Beispiel:

    #include <iostream>
    
    class Base
    {
    public:
        int A;
        //.... many more members
    
        Base(int a)
        {
            A = a;
        }
    };
    
    class Derived : public Base
    {
    public:
        int X;
    
        Derived(int a, int x) : Base(a)
        {
            X = x;
        }
    
        Derived& Derived::operator = (const Derived& d)
        {
            A = d.A;
            X = d.X;
            return *this;
        };
    
        Derived& Derived::operator = (const Base& b)
        {
            A = b.A;
            return *this;
        };
    };
    
    class Derived2 : public Derived
    {
    public:
        Derived2(int a, int b) : Derived(a, b)
        {
        }
        using Derived::operator =;
    };
    
    int main()
    {
        Base base(9);
        Derived  derived1a( 1, 1);
        Derived2 derived1b( 2, 1);
        Derived  derived2a( 3, 1);
        Derived2 derived2b( 4, 1);
    
        // Methode 1a:
        Base *basep = &derived1a;
        *basep = base;
        std::cout << derived1a.A << "," << derived1a.X << std::endl; // Ok, Output 9,1
    
        // Methode 1b:
        Base *basep2 = &derived1b;
        *basep2 = base;
        std::cout << derived1b.A << "," << derived1b.X << std::endl; // Ok, Output 9,1
    
        // Methode 2a:
        derived2a = base;
        std::cout << derived2a.A << "," << derived2a.X << std::endl; // Ok, Output 9,1
    
        // Methode 2b:
        derived2b = base;
        std::cout << derived2b.A << "," << derived2b.X << std::endl; // Ok, Output 9,1
    
        return 0;
    }
    

  • Mod

    Du kannst geerbte Methoden explizit aufrufen, auch wenn sie in der erbenden Klasse überschreiben wurden. Hier zum Beispiel auf diese Art und Weise:

    Derived& Derived::operator = (const Derived& d)
    {
      Base::operator=(d);
      X = d.X;
      return *this;
    }
    


  • Das copy-and-swap Idiom erschlägt das komfortabel. Bei einer Selbstzuweisung wird zwar eine unnötige Kopie erzeugt, aber ansonsten ist das good practice.

    Derived& operator=( const Derived& other )
    {
       Derived tmp( other );
    
       // wenn Derived swap implementiert
       tmp.swap( *this );
    
       // ansonsten std::swap
       std::swap( *this, tmp );
    
       return *this;
    }
    

  • Mod

    Copy&Swap ist zwar sehr empfehlenswert, aber die Benutzung von std::swap solltest du noch einmal überdenken. Was macht std::swap hier wohl? Welche Methoden der Klasse wird es aufrufen?



  • Touche 😃



  • @mireiner

    In Zeilen 25 und 32 muss es operator = (const Derived& d) statt Derived::operator = (const Derived& d) heißen.

    Unschön: Alle Attribute sind public.

    Du lässt Basisklassenzeiger auf Objekte vom Typ Derived zeigen. Kann es sein (du sprichst von einem "umfangreichen Projekt"), dass später nicht nur Base-Objekte, sondern auch Derived-Objekte zugewiesen werden sollen? Kann es dann sein, dass es dann virtual Funktionen gibt?



  • Hallo SeppJ,

    danke für diesen Lösungsweg. Nutze ihn nun auch für folgenden Zuweisungsoperator, um "derived = base" schreiben zu können:

    Derived& operator = (const Base& b)
    {
       Base::operator=(b);
       return *this;
    };
    

    Aufgrund Deiner Antwort habe ich herausgefunden, dass sogar die Kurzversion des von Dir genannten Codes funktioniert:

    Base base(..);
    Derived derived(..);
    derived.Base::operator=(base);
    

    Wieder etwas dazu gelernt.



  • Hallo mmuellersbester,

    mein Code ist nur ein minimal Beispiel um das Problem zu zeigen. Die Zeilen 25 und 32 sind Tippfehler. Die gesuchte Lösung sollte auch für Klassen mit virtuellen Funktionen funktionieren. Gibt es da etwas besonderes zu beachten?



  • Hallo DocShoe,

    da "copy-and-swap" völliges Neuland für mich ist, werde ich mir das einmal in Ruhe anschauen - das braucht Zeit. Danke für den Hinweis.


  • Mod

    Ich habe meine Antwort eher als Tipp gemeint, wie man allgemein (scheinbar) verdeckte Methoden der Basisklasse aufrufen kann. Einen Zuweisungsoperator solltest du schon stets als Copy&Swap implementieren, wie DocShoe es vorgeschlagen hat. Bloß eben richtig. Das heißt, nicht die ganze Klasse auf einmal mit std::swap tauschen, sondern die einzelnen Member und einzelnen Basen. Hier also:

    Derived& operator=(Derived d)
    {
      std::swap(static_cast<Base&>(d), static_cast<Base&>(*this));
      std::swap(d.X, this->X);
      return *this;
    }
    


  • @SeppJ

    Bin begeistert. Habe um "Derived=Base" zuweisen zu können noch den entsprechen operator hinzugefügt:

    Derived& operator = (Base& b)
    {
       std::swap(b, static_cast<Base&>(*this));
       return *this;
    }
    

    Beide Zuweisungsoperatoren "Derived = Derived" und "Derived=Base" mit std::swap funktionieren einwandfrei für "Derived".

    Für "Derived2" funktioniert es aber leider nicht. Was mir schwer fällt zu verstehen, weil die Zuweisung mit den "normalen" Zuweisungsoperatoren in "Derived2" funktionierte (siehe einleitendes Beispiel oben). Mir ist die Zuweisung mittels std:swap nicht einmal gelungen, wenn die Zuweisungsoperatoren nochmals in "Derived2" implementiert werden.

    #include <iostream>
    
    class Base
    {
    public:
        int A;
        int B;
        //.... many more members
    
        Base(int a, int b)
        {
            A = a;
            B = b;
            //....
        }
    };
    
    class Derived : public Base
    {
    public:
        int X;
    
        Derived(int a, int b, int x) : Base(a, b)
        {
            X = x;
        }
    
        Derived& operator=(Derived d)
        {
            std::swap(static_cast<Base&>(d), static_cast<Base&>(*this));
            std::swap(d.X, this->X);
            return *this;
        }
    
        Derived& operator = (Base& b)
        {
            std::swap(b, static_cast<Base&>(*this));
            return *this;
        }
    };
    
    class Derived2 : public Derived
    {
    public:
        Derived2(int a, int b, int x) : Derived(a, b, x)
        {
        }
        using Derived::operator=;  // Wird benötigt, sonst Compiler Fehler in Zeile 63
    
        // Auch die erneute Implementierung funktioniert nicht
        // dann natürlich ohne "using Derived::operator="
        /*
        Derived2& operator=(Derived2 d)
        {
            std::swap(static_cast<Base&>(d), static_cast<Base&>(*this));
            std::swap(d.X, this->X);
            return *this;
        }
    
        Derived2& operator = (Base& b)
        {
            std::swap(b, static_cast<Base&>(*this));
            return *this;
        }
        */
    };
    
    int main()
    {
        Base base(7, 7);
        Derived  derived1( 1, 1, 9);
        Derived2 derived2( 2, 2, 9);
    
        derived1 = base;
        std::cout << derived1.A << "," 
                  << derived1.B << "," 
                  << derived1.X << std::endl; // Ok => Output 7,7,9
    
        derived2 = base;
        std::cout << derived2.A << "," 
                  << derived2.B << "," 
                  << derived2.X << std::endl; // ?? => Output 1,1,9
    
        Derived  derived3( 1, 1, 9);
        Derived  derived4( 7, 7, 9);
    
        derived3 = derived4;
        std::cout << derived3.A << "," 
                  << derived3.B << "," 
                  << derived3.X << std::endl; // Ok => Output 7,7,9
    
        Derived2  derived5( 1, 1, 9);
        Derived2  derived6( 7, 7, 9);
        std::cout << derived5.A << "," 
                  << derived5.B << "," 
                  << derived5.X << std::endl; // ?? => Output 1,1,9
    
        return 0;
    }
    

    Was muss geändert werden, damit der std::swap Zuweisungsoperator auch für "Derived2" funktioniert?


  • Mod

    Derived = Base ist eine komische Beziehung. Das deutet darauf hin, dass Vererbung nicht richtig benutzt wurde, denn damit kann man aus einem Dackel einen Schäferhund machen.

    Der von dir geschriebene Operator hat zudem ein ganz großes Problem: Er macht nur swap, kein copy! Es war durchaus beabsichtigt, dass bei dem von mir implementierten Operator das Argument per Kopie anstatt als Referenz übergeben wird. Wenn du das korrigierst, kommt bei deinem zweiten Beispiel auch das heraus, was du erwartest.

    Bei deinem vierten Beispiel machst du keine Zuweisung. Bei derived5 = derived6 käme das heraus, was du erwartest.

    Aber überdenk dringend noch einmal, ob eine Zuweisung von Base an ein Derived sinnvoll ist!


  • Mod

    SeppJ schrieb:

    Derived = Base ist eine komische Beziehung. Das deutet darauf hin, dass Vererbung nicht richtig benutzt wurde, denn damit kann man aus einem Dackel einen Schäferhund machen.

    Immer wert zu beachten, wenn man noch keine richtige Vorstellung hat, wann und warum (oder warum nicht) und wie man Vererbung einsetzen sollte: Liskovsches Substitutionsprinzip
    Unter diesem Prinzip sind Basen immer abstrakt, und kommen praktisch nie als lvalue (und somit als potentielles Argument eines Zuweisungsoperators) vor.



  • SeppJ schrieb:

    Der von dir geschriebene Operator hat zudem ein ganz großes Problem: Er macht nur swap, kein copy! Es war durchaus beabsichtigt, dass bei dem von mir implementierten Operator das Argument per Kopie anstatt als Referenz übergeben wird. Wenn du das korrigierst, kommt bei deinem zweiten Beispiel auch das heraus, was du erwartest.

    Danke das war das Problem, jetzt funktioniert es. Mit "copy und swap" muss ich mich mal in Ruhe eingehender beschäftigen. Bin da immer etwas langsam im Begreifen.

    SeppJ schrieb:

    Bei deinem vierten Beispiel machst du keine Zuweisung. Bei derived5 = derived6 käme das heraus, was du erwartest.

    Oh, das hatte ich übersehen.

    SeppJ schrieb:

    Derived = Base ist eine komische Beziehung. Das deutet darauf hin, dass Vererbung nicht richtig benutzt wurde, denn damit kann man aus einem Dackel einen Schäferhund machen.

    Ja volle Zustimmung. Vor dem Hintergrund mich mit Vererbung eingehender zu befassen, entstand auch meine Frage.

    Weil ich begann mit Pascal und C zu programmieren, habe ich aufgrund mangelnder Erfahrung Vererbung bislang relativ selten verwendet. Die Handhabung eingebundener Objekte (hat ein Beziehung) war und ist mir einfach vertrauter.

    Ich wünschte es gäbe ein ganzes C++ Praxis Buch über den Umgang mit Vererbung in objektorientierten Programmen für Dummies wie mich. Denn dieser Teil wird für meine Bedürfnisse in den C++ Büchern immer viel zu kurz beschrieben. Falls es so ein Buch schon gibt, wäre ich für einen Hinweis sehr dankbar.



  • mireiner schrieb:

    Hallo mmuellersbester,

    mein Code ist nur ein minimal Beispiel um das Problem zu zeigen. Die Zeilen 25 und 32 sind Tippfehler. Die gesuchte Lösung sollte auch für Klassen mit virtuellen Funktionen funktionieren. Gibt es da etwas besonderes zu beachten?

    Generell meine ich, dass auf der rechten Seite einer Zuweisung normalerweise(!) ein Objekt desselben Typs wie auf der linken Seite stehen sollte. Dabei geht es um den Typ zur Laufzeit(!). Bei Vererbung kann es sein, dass eine Oberklassenreferenz auf ein abgeleitetes Objekt zeigt, z.B.

    Derived derived3(5, 7);
    Base& b1 = derived3;
    

    Der Typ links ist zur Kompilierzeit Basis , zur Laufzeit aber Derived , weil b1 auf ein Derived -Objekt verweist. Dann müsste die Zuweisung

    Derived derived4(8, 9);
    b1 = derived4;
    

    möglich sein mit der Wirkung, dass danach (derived3 == derived4) gilt - ist aber nicht so. Kein Problem gibt es, wenn links und rechts
    der Typ schon zur Kompilierzeit übereinstimmt.



  • @camper
    Danke für diesen Hinweis, so explizit und leichtverständlich beschrieben kannte ich das Liskovsche Ersetzbarkeitsprinzip noch nicht.



  • @mmuellersbester
    Vielleicht habe ich mich da nicht deutlich genugt ausgedrückt.
    Bei der Zuweisung der gerbten Klasse "Base" war mein Wunsch, dass nur "Base" neu zugewiesen werden soll. Die Member von "Derived" sollten unverändert bleiben.
    Genau das tut Dein Beispielcode auch. In derived3 und derived4 ist der gerbte Member "Base" (Wert 😎 identisch, der Member von "Derived" aber bleibt unangetastet (derived3 = 7 und derived4 = 9).
    Das in Deinem Beispiel derived3 != derived4 ist, war mir klar.



  • Wie zuvor gesagt, war der eigentliche Hintergrund meiner Frage, wie in der Regel in C++ auf vererbte Member (als eine Einheit) zugegriffen wird, bzw. wie vererbte Member (wieder als eine Einheit) gesetzt / neu zugewiesen werden können.
    Nun ist mir eingefallen, dass ich die einfachste Möglichkeit übersehen habe, das über Getter und Setter zu erledigen:

    #include <iostream>
    
    class Base
    {
    public:
        int A;
        int B;
        //.... many more members
    
        Base(int a, int b)
        {
            A = a;
            B = b;
            //....
        }
    
        void setBase(Base base)
        {
            *this = base;
        }
    
        Base getBase() const
        {
            return *this;
        }
    };
    
    class Derived : public Base
    {
    public:
        int X;
    
        Derived(int a, int b, int x) : Base(a, b) 
        {
            X = x;
        }
    
        Derived& operator = (const Base& b)
        {
            Base::operator=(b);
            return *this;
        }
    };
    
    class Derived2 : public Derived
    {
    public:
        Derived2(int a, int b, int x) : Derived(a, b, x) {}
    
        using Derived::operator=;  // Wird benötigt, sonst Compiler Fehler
    };
    
    int main()
    {
        Base base(7, 7);
        Derived derived1( 1, 1, 9);
        Derived derived2( 2, 2, 9);
        Derived derived3( 3, 3, 9);
    
        derived1.setBase(base);
        std::cout << derived1.A << "," 
                  << derived1.B << "," 
                  << derived1.X << std::endl; // Ok => Output 7,7,9
    
        derived2 = base;
        std::cout << derived2.A << "," 
                  << derived2.B << "," 
                  << derived2.X << std::endl; // Ok => Output 7,7,9
    
        derived3.Base::operator=(base);
        std::cout << derived3.A << "," 
                  << derived3.B << "," 
                  << derived3.X << std::endl; // Ok => Output 7,7,9
    
        return 0;
    }
    

    Nach der Implementation von Gettern und Settern in der Klasse "Base" gibt es jetzt im obigen Beispiel also drei Möglichkeiten die geerbte Klasse "Base" in "Derived" zuzuweisen:

    1. derived.setBase(base);
    2. derived = base;
    3. derived.Base::operator=(base);

    Nun haben mich hier alle darauf hingewiesen, das Methode 2 und damit vermutlich auch Methode 3 nicht zu empfehlen sind, weil sie den Anschein erwecken können, dass Äpfel mit Birnen zugewiesen werden. Diesen Einwand verstehe ich.

    Gibt es zu Methode 1 auch etwas einzuwenden? Oder ist das der empfehlenswerte C++ Standardweg für diese Aufgabe?


  • Mod

    Und wozu überhaupt die Vererbungsbeziehung? Sieht mir eher nach einem Fall für Komposition aus.



  • camper schrieb:

    Und wozu überhaupt die Vererbungsbeziehung? Sieht mir eher nach einem Fall für Komposition aus.

    Es ist so, dass ich bisher hauptsächlich mit Komposition gearbeitet habe und nun versuche, wo an welchen Stellen ich vermehrt Vererbung einsetzen könnte. Vor diesem Hintergrund ist die Frage entstanden, die ich hier stelle. Gut möglich dass mein Problem ist, dass ich immer noch in den Konzepten Komposition denke.

    Bezieht sich Deine Frage darauf, dass eine Neuzuweisung von Base innerhalb von Derived wenig Sinn macht? Was ist die Alternative um Base neue Werte zuzuweisen? Die Instanz von Derived ganz verwerfen / löschen und eine neue Instanz von Derived aufsetzen, mit neuen Werten von Base?

    Ich hatte nun gerade vermutet den richtigen Weg gefunden zu haben mit den Settern und Gettern auf den this Zeiger von Base (siehe oben). Ist der Weg falsch?


Log in to reply