=-Operator bei Vererbung



  • Nachtrag:
    Was mach ich eigentlich, wenn ich sowas hier habe (siehe Kommentare im Code):

    class Base
    {
    public:
    	virtual int operator =(int value)
    	{
    		return value;
    	}
    
    private:
    	Base &operator =(const Base &value);
    	// Keine Implementierung, weil so eine
    	// Zuweisung nicht möglich sein soll.
    };
    
    class Derived : public Base
    {
    public:
    	using Base::operator =;
    	// Das geht jetzt nicht mehr.
    };
    
    int main()
    {
    	Derived d;
    
    	d = 5;
    }
    

  • Administrator

    Jimmbob schrieb:

    Aber Derived &operator =(Derived const &)[/cpp] hat ja eine andere Signatur als [c]int operator =(int) . Wieso verdeckt der eine dann den anderen? Selbst wenn es diesen implizit vorhandenen Operator in Derived schon gibt, dürfte sich der ja mit dem aus Base kommenden Operator gar nicht in die Quere kommen.

    Die Signatur spielt absolut keine Rolle. Es geht hier um den Scope, in welchem die Funktion definiert wurde. Es werden zuerst alle operator= in Derived gesucht. Wenn welche gefunden wurden, dann wird nicht mehr weitergesucht, sondern nur noch der beste ausgewählt. Leider gibt es aber keinen, daher gibt es einen Kompilerfehler. Wenn theoretisch kein operator= gefunden worden wäre, erst dann würde in Base nach operator= Methoden gesucht werden. So ist es halt im Standard definiert. Das gilt übrigens ganz allgemein für alle möglichen Funktionen.

    Zu deinem zweiten Problem:
    Die Zuweisung ist an sich schon etwas fraglich. Normalerweise weist man einer Variable nur Werte des gleichen Typs zu. Da kommt doch etwas die Frage auf, ob eine Funktion nicht besser wäre?

    Grüssli



  • Nebenbei: Ein virtueller Zuweisungsoperator ist ein wenig fragwürdig. Und zwar einfach, weil C++ kein Double-Dispatching kann und der rechte Operand somit als statischer Typ interpretiert wird, was bei polymorphem Code wie

    Base* a = ...;
    Base* b = ...;
    
    *a = *b;
    

    nur die Basisklassenversion Base::operator= in Betracht zieht.

    Alternativen sind oft auch semantisch sinnvoller: Entweder, man verbietet Wertsemantik komplett (durch privaten Kopierkonstruktor und Zuweisungsoperator), oder man lässt sie auf Objekten gleichen Typs (in der gleichen Hierarchiestufe) zu, wodurch man wiederum kein virtual benötigt.



  • Die einfachste Möglichkeit einer auch polymorph korrekt funktionierenden Zuweisung ist, in jeder Klasse einer Vererbungshierarchie den operator=() zu implementieren und in ihm eine virtuelle Funktion assign() oder zuweisung() aufzurufen, die die eigentliche Arbeit macht. Beispiel:

    Derived& operator=(const Derived& rhs) { 
          return assign(rhs); 
       }
    
       virtual Derived& assign(const Base& rhs) {
          Derived temp(dynamic_cast<const Derived&>(rhs));
          swap(temp);
          return *this;
       }
    

    Der dynamic_cast sorgt für den richtigen Typ. Bei falscher Verwendung (= nicht passende Typen) gibt es eine Exception.



  • Ergänzung: Jede Klasse sollte auch eine Methode swap() haben, die in assign() aufgerufen wird. swap(arg) vertauscht einfach das Argument mit *this.

    void swap(Derived& rhs) {
          X::swap(rhs);           // Oberklassendaten
          std::swap(lokalesDatum1, rhs.lokalesDatum1);    // lokale Daten
          std::swap(lokalesDatum2, rhs.lokalesDatum2);    // lokale Daten
       }
    

    X muss dabei die direkte Oberklasse von Derived sein. Falls Derived direkt von Base erbt, gilt also X=Base. Die Methode swap() ruft die entsprechende Funktion der Standardbibliothek auf.



  • UBr schrieb:

    Ergänzung: Jede Klasse sollte auch eine Methode swap() haben, die in assign() aufgerufen wird. swap(arg) vertauscht einfach das Argument mit *this.

    Warum soll man swap bei einer Zuweisung verwenden?
    Das macht ja dein zugewiesenes Objekt 'kaputt'

    UBr schrieb:

    virtual Derived& assign(const Base& rhs) {
          Derived temp(dynamic_cast<const Derived&>(rhs));
          swap(temp);
          return *this;
       }
    

    Der dynamic_cast sorgt für den richtigen Typ. Bei falscher Verwendung (= nicht passende Typen) gibt es eine Exception.

    Ich persönlich würde ein static_cast verwendet, der in den meisten Fällen ausreicht.

    EDIT: ups... Falsche Aussage gelöscht



  • CSpille schrieb:

    Warum soll man swap bei einer Zuweisung verwenden?
    Das macht ja dein zugewiesenes Objekt 'kaputt'

    Das ganze nennt sich Copy-and-Swap. Dabei wird zuerst eine Kopie des Operanden angelegt und mit der gespwapt. Vorteil: man kann den Kopierkonstruktor wiederverwenden und das ist exceptionsicher.



  • @CSpille:

    Base b;
    Derived d;
    b = d; // s.u.
    

    So eine Zuweisung ist syntaktisch erlaubt, semantisch i.Allg. aber nicht erwünscht. Mit static_cast würde es keine Exception für diesen Fall geben. Nur wenn b=d tatsächlich erlaubt sein soll, ist static_cast besser. Ich kann mir aber keinen praktisch relevanten Anwendungsfall dafür vorstellen.



  • @make it right:

    Dessen war ich mir bewusst, jedoch würde ich die Performance bevorzugen.
    Im anderen Fall erhältst du ja auch nur einen Runtime-Fehler...

    Naja...
    evlt. dynamic_cast für Debug und static_cast für Release



  • CSpille schrieb:

    @make it right:

    Dessen war ich mir bewusst, jedoch würde ich die Performance bevorzugen.
    Im anderen Fall erhältst du ja auch nur einen Runtime-Fehler...

    Nicht unbedingt - hängt vom nachfolgenden Code ab. Der Performance-Unterschied ist minimal, weil der Laufzeitaufwand gegenüber dem Kopierkonstruktor (erst recht bei dynamischen Daten) praktisch keine Rolle spielt. Die Performance sollte man da optimieren, wo sie wirklich (=messbar!) aufgefressen wird.

    CSpille schrieb:

    Naja...
    evlt. dynamic_cast für Debug und static_cast für Release

    Wie soll man das pragmatisch und wartungsfreundlich realisieren?
    Ist es sinnvoll, dass ein Programm sich möglicherweise bei Debug und Release jeweils verschieden verhält?



  • make it right schrieb:

    @CSpille:

    Base b;
    Derived d;
    b = d; // s.u.
    

    So eine Zuweisung ist syntaktisch erlaubt, semantisch i.Allg. aber nicht erwünscht. Mit static_cast würde es keine Exception für diesen Fall geben. Nur wenn b=d tatsächlich erlaubt sein soll, ist static_cast besser. Ich kann mir aber keinen praktisch relevanten Anwendungsfall dafür vorstellen.

    Welche Exception?



  • make it right schrieb:

    Ist es sinnvoll, dass ein Programm sich möglicherweise bei Debug und Release jeweils verschieden verhält?

    Ja.



  • volkard schrieb:

    Welche Exception?

    std::bad_cast natürlich



  • make it right schrieb:

    CSpille schrieb:

    @make it right:

    Dessen war ich mir bewusst, jedoch würde ich die Performance bevorzugen.
    Im anderen Fall erhältst du ja auch nur einen Runtime-Fehler...

    Nicht unbedingt - hängt vom nachfolgenden Code ab. Der Performance-Unterschied ist minimal, weil der Laufzeitaufwand gegenüber dem Kopierkonstruktor (erst recht bei dynamischen Daten) praktisch keine Rolle spielt. Die Performance sollte man da optimieren, wo sie wirklich (=messbar!) aufgefressen wird.

    Also ich persönlich habe einen dynamic_cast (ohne jemals Messungen angestellt zu
    haben) immer als sehr teuer angesehen. Immerhin werden (so zumindest meine Vorstellung)
    alle möglichen Klassen-IDs verglichen. Somit ist ein dynamic_cast wohl eine
    der aufwändigsten Anweisungen.

    make it right schrieb:

    CSpille schrieb:

    Naja...
    evlt. dynamic_cast für Debug und static_cast für Release

    Wie soll man das pragmatisch und wartungsfreundlich realisieren?
    Ist es sinnvoll, dass ein Programm sich möglicherweise bei Debug und Release jeweils verschieden verhält?

    Evtl. Geschmacksache... Warum schmeißen Collection in Java unter bestimmten
    Umständen eine ConcurrentModificationException (auf die man sich nicht verlassen kann).
    Ich möchte nur Probleme aufdecken. Ich denke nicht, dass man einen solchen
    Mechanismus so einsetzt, dass man diese Exception (oder eben nicht) wirklich möchte

    Optimal wäre natürlich ein Mechanismus, der zur Kompilierzeit solche Fehler aufdeckt.

    Ich persönlich würde wahrscheinlich in meiner Basisklasse den Zuweisungskonstruktor
    verbieten und in der Klasse selbst von einem Template ableiten, das den
    Zuweisungsoperator implementiert. Falls von dieser Klasse ebenfalls abgeleitet
    werden kann, würde ich eine zusätzliche Klasse dazwischen packen, die
    alles implementiert außer den Zuweisungsoperator.

    So hat man dann absolut keine Probleme mehr, weil nur der passende Zuweisungsoperator
    deklariert ist.



  • CSpille schrieb:

    Also ich persönlich habe einen dynamic_cast (ohne jemals Messungen angestellt zu
    haben) immer als sehr teuer angesehen. Immerhin werden (so zumindest meine Vorstellung)
    alle möglichen Klassen-IDs verglichen. Somit ist ein dynamic_cast wohl eine
    der aufwändigsten Anweisungen.

    Nicht alle möglichen Klassen-IDs, sondern nur in der Vererbungshierarchie.
    D.h. derselbe Aufwand wie bei einem virtuellen Funktionsaufruf.

    CSpille schrieb:

    Optimal wäre natürlich ein Mechanismus, der zur Kompilierzeit solche Fehler aufdeckt.

    Stimmt. Natürlich schwierig bei Polymorphismus ...

    CSpille schrieb:

    Ich persönlich würde wahrscheinlich in meiner Basisklasse den Zuweisungskonstruktor
    verbieten und in der Klasse selbst von einem Template ableiten, das den
    Zuweisungsoperator implementiert. Falls von dieser Klasse ebenfalls abgeleitet
    werden kann, würde ich eine zusätzliche Klasse dazwischen packen, die
    alles implementiert außer den Zuweisungsoperator.

    Klingt komplex. Da wäre ein konkretes Beispiel interessant, um zu sehen, ob das wirklich einfacher ist als die obige Struktur mit assign/swap! Halte ich für nicht möglich, lasse mich aber gern überzeugen.



  • make it right schrieb:

    D.h. derselbe Aufwand wie bei einem virtuellen Funktionsaufruf.

    Das glaube ich jetzt mal nicht.



  • de scha wü?



  • make it right schrieb:

    Klingt komplex. Da wäre ein konkretes Beispiel interessant, um zu sehen, ob das wirklich einfacher ist als die obige Struktur mit assign/swap! Halte ich für nicht möglich, lasse mich aber gern überzeugen.

    Ist auch komplex, aber du hast deinen Fehler zur Kompilierzeit

    Hier ein Beispiel:

    #include<iostream>
    
    template<typename T>
    class Assignable{
    public:
            T& assign(const T& t){
                    T temp(t);
                    T& tRef = static_cast<T&>(*this);
                    swap(tRef, temp);
                    return tRef;
            }
    };
    
    class IA {
    public:
            int a;
    private:
            IA& operator=(const IA& a);
    };
    
    class IB : public IA{
    public:
            int b;
    private:
            IB& operator=(const IB& a);
    };
    
    class A :public IA, public Assignable<A>{
    public:
            A& operator=(const A& t){
                    return assign(t);
            }
    };
    
    class B :public IB, public Assignable<B>{
    public:
            B& operator=(const B& t){
                    return assign(t);
            }
    };
    
    void swapInternal(IA& a, IA& b){
            std::swap(a.a, b.a);
    }
    
    void swap(A& a, A& b){
            swapInternal(a,b);
    }
    
    void swap(B& a, B& b){
            swapInternal(a, b);
            std::swap(a.b, b.b);
    }
    
    int main(){
            A a;
            a.a = 1;
            B b;;
            b.a = 1;
            b.b = 1;
            A a2;
            a2 = a;
            B b2;
            b2 = b;
            // b2 = static_cast<IB&>(b); <- Das geht dann nicht!!!
            return 0;
    }
    

    Die Frage, ob man sich den Aufwand macht, ist natürlich Geschmacksache,
    jedoch hat man eine wirklich saubere Schnittstelle...



  • Es ging ja darum, eine polymorphe Zuweisung zu ermöglichen.
    Das geht mit deinem Beispiel nicht. Ergänze dein main-Programm um

    IB& ibref = b2;   // legales C++
            B b3;
            b3.a = 4;
            b3.b = 5;
            ibref = b3;       // nicht möglich mit deinem Modell
            IA& iaref = b2;   // legales C++
            iaref = b3;       // geht auch nicht
    

    Das andere Modell von oben ist nicht nur einfacher, es hat auch kein Problem mit polymorpher Zuweisung, static_cast hin oder dynamic_cast her.



  • Klar kannst du keiner Schnittstellen-Definition eine konkrete Instanz zuweisen
    oder andersrum. Deswegen ist mein Ziel eine solche Zuweisung zu verhindern.

    Wenn du einer abgeleiteten Klasse eine Basisklasse zuweist kannst du natürlich
    deren Felder kopieren, aber die zusätzlichen Felder haben ja einen Einfluß auf
    das Verhalten der Klasse und werden weiterhin in irgendeiner Art und Weise
    berücksichtigt.

    Ich kann einer Banane die Kalorien etc. eines anderen Obst (z.B. Apfel) zuweisen,
    jedoch bleibt sie immer noch eine Banane.
    Andersrum kann ich einem Obst die Kalorien einer Banane zuweisen, jedoch bleibt
    mein Objekt immer noch z.B. ein Apfel.

    Ich kann ja nicht das komplette Objekt 'transformieren'. Wenn man sowas machen
    würde/könnte, hätte man plötzlich in seinen Korb mit Äpfeln eine Banane...


Anmelden zum Antworten