=-Operator bei Vererbung



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



  • Alles Banane. ibref hat den statischen Typ IB (Sicht des Compilers), aber den dynamischen Typ B (Sicht zur Laufzeit). Unterschiedliche statische und dynamische Typen sind gerade ein Wesenszug des Polymorphismus!! (nachlesen!!!). Damit repräsentiert die Referenz ibref ein vollwertiges B-Objekt, dem man selbstverständlich ein anderes B-Objekt zuweisen kann. Also Apfel = Apfel.
    Und für eine nicht-virtuelle Zuweisung braucht man keine Extra-Templates.



  • Man könnte denken, dass ein virtueller Zuweisungsoperator genügen würde. Das ist nicht der Fall, weil der Zuweisungsoperator anders als "normale" virtuelle Funktionen aufgerufen wird. Wenn nur ein virtueller Zuweisungsoperator geschrieben wurde, wird bei der Compilation der nicht-virtuelle automatisch auch noch erzeugt - und ggf. statt des virtuellen aufgerufen (siehe Abschnitt 13.5.3 im Standardentwurf). Das ist kein Problem, wenn die abgeleitete Klasse keinerlei dynamische Daten hat. Wenn es jedoch Zeigerattribute gibt, denen mit new etwas zugewiesen wird, ist ein selbstgeschriebener nicht-virtueller Zuweisungsoperator unumgänglich, weil der compilergenerierte nur den Zeiger kopieren würde, ohne Speicherplatz zu beschaffen.

    Damit scheint es am einfachsten, den Zuweisungsoperator bei dynamischen Attributen in jeder Klasse der Vererbungshierarchie zu definieren und in ihm eine virtuelle Methode assign() aufzurufen. Weil der polymorphe Aufruf dafür sorgt, dass sowieso die richtige der überschriebenen assign()-Methoden aufgerufen wird, reicht ein static_cast! In diesem Punkt hat CSpille recht. Der dynamic_cast wirft im Zuweisungsoperator keine Exception bei der Zuweisung

    base = derived;
    

    weil der Typ ja stimmt. Das Problem liegt woanders: In C++ ist diese Zuweisung erlaubt, kopiert wird aber nur der Basisklassenanteil (object slicing). Die Methode assign() der obersten Basisklasse sei

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

    Der Kopierkonstruktor kopiert hier nur den Basisklassenanteil.
    Wenn sichergestellt werden soll (und nur dann!), dass die Zuweisung base = derived; als Fehler betrachtet werden soll, weil es keine sinnvolle praktische Anwendung dafür gibt (Gegenbeispiel gesucht...), müssen die Objekttypen links und rechts verglichen werden, also

    virtual Base& assign(const Base& rhs) {
          if(typeid(*this) != typeid(rhs)) { 
             throw std::bad_typeid();
          }
          Base temp(rhs);
          swap(temp);
          return *this;
    }
    

    Wenn diese Typprüfung gewünscht ist, muss sie in den assign()-Methoden der abgeleiteten Klassen wiederholt werden.

    typeid ist von der Performance her vermutlich etwa wie dynamic-cast anzusehen. Ob der Performance-Aspekt von typeid überhaupt bei der Zuweisung eine wichtige Rolle spielt, kann nur im Vergleich mit der Performance des Kopierkonstruktors beurteilt werden.

    Ein vergleichbares Problem hat man beim Vergleichsoperator und dem Vergleich (base == derived). Ohne Typprüfung wäre der Vergleichsoperator nicht symmetrisch, d.h. (base == derived) kann true sein und das umgekehrte false (wobei dann ohne Typprüfung auf undefinierte Bereiche zugegriffen wird). Weil der Vergleich in abgeleiteten Klassen die Basisklasse einschließt, muss die Prüfung mit typeid in abgeleiteten Klassen nicht wiederholt werden; im Gegensatz zur Zuweisung, wo die Prüfung, sofern gewünscht, in jeder Klasse vorgenommen werden muss, weil assign() nicht die entsprechende Oberklassenmethode aufruft. Also etwa

    virtual bool Base::operator==(const Base& arg) const {
       return 
          typeid(*this) == typeid(arg) &&    // Zeile löschen bei nicht erwünschter Prüfung
          vergleichKlassenlokalerAttribute(arg);  // gibt bool zurück
    }
    

    bzw. in einer abgeleiteten Klasse

    bool Derived::operator==(const Base& arg) const {
       const Derived& rarg = static_cast<const Derived&>(arg);
       return X::operator==(arg) &&          // X = *direkte* Oberklasse (ggf. Base)
          vergleichKlassenlokalerAttribute(rarg);  // gibt bool zurück
    }
    


  • @UBr: Durch die Zusätzliche Vererbungsschicht ohne operator= möchte ich genau
    diese Zuweisungsproblematik beseitigen. So eine Zuweisung ist in meinen Augen
    nämlich kein Runtime-Fehler. Sonst kann man auch vorher casten.

    EDIT: In den meisten Fällen würde ich jedoch wegen des enormen Aufwandes auf ein
    korrekte Anwendung des Zuweisungsoperstors im Programm achten. Ich wollte nur eine
    theoretisch saubere Lösung vorschlagen.
    Einen dynamic_cast im Debug und static_cast im Release bei Verwendung
    eines Templates finde ich in der Regel aber ausreichend. So sieht man, wenn auch
    erst zur Laufzeit, dass etwas schief geht.

    Oder willst du bei beiden nicht genau wissen von welchem Typ sie sind ^^

    UBr schrieb:

    virtual Base& assign(const Base& rhs) {
          if(typeid(*this) != typeid(rhs)) { 
             throw std::bad_typeid();
          }
          Base temp(rhs);
          swap(temp);
          return *this;
    }
    

    Wenn beide von der gleichen abgeleiteten Klasse sind, dann ist die Zuweisung
    erlaubt und kopiert nur das 'halbe' Objekt?
    Oder wie soll ich das verstehen?

    Gibt es im Standard eigentlich typeid?

    zamborambo schrieb:

    Und für eine nicht-virtuelle Zuweisung braucht man keine Extra-Templates.

    Ich denke das Template ist hier extrem sinnvoll, ansonsten bräuchte ich den
    Code in jeder abgeleiteten Klasse.
    Genau wegen des oben bestehenden Problems verwende ich Templates...



  • Ich verstehe den Bedarf gar nicht. Typverändernde Zuweisungen mache ich nicht auf Objekten, zum Beispiel weil das eh zu selten klappt, sondern auf Basisklassenzeigern, und auf einmal ist es logisch, einfach, schnell und einfach.



  • volkard schrieb:

    Ich verstehe den Bedarf gar nicht. Typverändernde Zuweisungen mache ich nicht auf Objekten, zum Beispiel weil das eh zu selten klappt, sondern auf Basisklassenzeigern, und auf einmal ist es logisch, einfach, schnell und einfach.

    👍
    Genau aus diesem Grund benutze ich sehr selten Collections (Ausnahme: einfache Datentypen),
    die keine Zeiger besitzen.

    In den Anfängen meiner C++-Programmierung habe ich gerne von so einem
    Template geerbt, damit ich gesehen habe, was schief läuft.

    Inzwischen verlasse ich mich aber komplett darauf, dass ich solche
    'Zuweisungsfehler' nicht in meinem Code habe.


Anmelden zum Antworten