operator== in Vererbungshierarchie...



  • Hi,

    Basisfrage...

    struct X
    {
        // ...
    };
    
    struct Y : public X
    {
        // ...
    };
    

    Es kann auch ein X mit dynamischen Typ Y in der Anwendung geben und sowohl X als auch Y besitzen Attribute. Ich möchte jetzt operator== implementieren.

    Wie löse ich das denn am schönsten?

    Das hier klappt, nutzt jedoch typeid, wobei mir dazu auch kein Nachteil einfällt:

    class X
    {
    public:
       X(int x) : x(x) {}
    
       bool operator==(const X& rhs) const
       {
           return typeid(*this) == typeid(rhs) && compare(rhs);
       }
    
    protected:
       virtual bool compare(const X& rhs) const
       {
            return x == rhs.x;
       }
    
       int x;
    };
    
    class Y : public X
    {
    public:
    	Y(int x, int y) : X(x), y(y) {}
    
    private:
        virtual bool compare(const X& rhs) const
        {
        	return X::compare(rhs) && y == static_cast<const Y&>(rhs).y;
        }
    
        int y;
    };
    

    Wie löst ihr das?



  • Ich glaube um typeid oder umgekehrt um dynamic_cast wirst du nicht drum herum kommen.
    Lasse mich aber gerne vom Gegenteil überzeugen.



  • Eisflamme schrieb:

    Wie löst ihr das?

    Es kommt drauf an, was man braucht, und wie man das benutzt. Ich glaub, hier gibts keine wirklich schöne Lösung.
    Du hast z.B. die Prüfung ob der Typ gleich ist. Das ist ja schon mal eine ziemlich starke Einschränkung. Es kann ja durchaus reichen zu sagen, dass ein paar Basiseigenschaften für die Übereinstimmung ausreichen. Mir fällt bei uns mindestens ein Fall ein, wo eine abgeleitete Klasse zuerst schaut, ob das andere Objekt eine Instanz derselben Klasse ist und dann mehr vergleicht, und ansonsten einfach weniger vergleicht. Kommt halt auf den Kontext drauf an, was "gleich" bedeutet.



  • ein Int mit dem Wert 1 sollte doch gleich einem float mit dem Wert 1.0 sein.
    das ein float genau 1.0 ist lass ich mal beiseite.

    warum sollte man da die typeid beachten?

    struct Foo{ int i; };
    struct Bar{ int i; };
    
    Foo f = {1};
    Bar b =  {1};
    

    ist Foo f nun gleich Bar b?



  • Inwiefern soll das jetzt helfen?
    Das Gleichheit kontext-abhängig ist, ist wohl jedem klar und offenbar heisst Gleichheit hier "ganz genau gleich".
    Die Frage ist ja nur, wie man das 'schön' umsetzt.



  • was ist schon schön ...?
    vielleicht

    hash(a) == hash(b)
    

    oder

    a.hash() == b.hash()
    

    ersteres hat weniger Buchstaben, und sollte eigentlich zu zweiterem weiterleiten wenn ersteres nicht definiert ist, ich hoffe das kommt irgendwann.

    ansonst, in einer Objekt Hierarchie wird man nicht virtuelle Funktionen herum kommen, wenn nicht so eine ID gibt.

    Vielleicht die compare Funktion für den eigenen Typen überladen damit man sich im optimal Fall ein cast spart.

    Es stellt sich jedoch schon die Frage, warum sollte ich einen Fisch auf Gleichheit mit einer Katz vergleichen nur weil beides Lebewesen sind.



  • kurze_frage schrieb:

    Es stellt sich jedoch schon die Frage, warum sollte ich einen Fisch auf Gleichheit mit einer Katz vergleichen nur weil beides Lebewesen sind.

    Weil du vielleicht einen Fisch hast, von dem du wissen willst, ob er in einem Lebewesen-Container enthalten ist.



  • Ich persönlich überlade keine operatoren virtual, aber die richtige vorgehensweise hier wäre:

    virtual bool Derived::compareTo(Base const& other) const {
      Derived const* o = dynamic_cast<Derived const*>(&other);
      if(!o) return false; //this != other guard
    
      return this->value == o->value; //der echte vergleich
    }
    

    Man kann auch Visitor verwenden, das wird aber oft etwas umständlich. Kann aber den dynamic_cast sparen auf Kosten einer komplexeren Hierachie.



  • Und wie würde dein ==-Operator aussehen?
    So wie bei Eisflamme? Dann hättest du ja dynamic_cast und typeid.
    Warum ist das 'die richtige vorgehensweise'?



  • Jockelx schrieb:

    Und wie würde dein ==-Operator aussehen?

    Den gäbe es gar nicht.

    So wie bei Eisflamme? Dann hättest du ja dynamic_cast und typeid.
    Warum ist das 'die richtige vorgehensweise'?

    Kein typeid, nur der dynamic cast.



  • Kein operator== aber dafür compareTo, wieso? Und ist dynamic_cast nicht teurer als typeid, das ergab meine bisherige Recherche jedenfalls.



  • Weils bisher noch nicht genannt wurde werfe ich nochmal Double Dispatch in den Raum. Das wäre zumindest eine Lösung ohne dynamic_cast und typeid . Ob sie besser/schneller ist weiß ich nicht.



  • Ah, mein compareTo ist auch nicht ganz richtig, hier kommt die richtige Variante.

    Das Problem mit dem typeid ist ja, dass man damit nur exakt gleiche Klassen testen kann. Das kann gewollt sein aber oft verhindert das sinnvolles Subclassing.

    Und da die compare Funktion ja öffentlich (protected) ist, kann man da Fehler machen wenn ich die aufrufe obwohl ich den op== verwenden sollte.

    Operatoren mit virtuellen verhalten mage ich persönlich nicht, deshalb habe ich eine compareTo funktion für die polymorphen Vergleiche. Aber prinzipiell kann man hier auch den op== verwenden.

    class Base {
    public:
            Base(int a) : value(a) {}
    
            virtual bool compareTo(Base const& other) const {
                    return this->value == other.value;
            }
    
    private:
            int value;
    };
    
    class Derived : public Base {
    public:
            Derived(int a, int b) : Base(a), value(b) {}
    
            virtual bool compareTo(Base const& other) const {
                    Derived const* p = dynamic_cast<Derived const*>(&other);
                    if(!p) return Base::compareTo(other);
                    return value==p->value && Base::compareTo(other);
            }
    private:
            int value;
    };
    
    #include <iostream>
    
    using namespace std;
    
    int main() {
            Base b(1);
            Derived x(1,1);
            Derived y(2,1);
    
            cout<< "b==x: " << b.compareTo(x) << endl;
            cout<< "x==b: " << x.compareTo(b) << endl;
            cout<< "b==y: " << b.compareTo(y) << endl;
            cout<< "y==b: " << y.compareTo(b) << endl;
            cout<< "x==y: " << x.compareTo(y) << endl;
            cout<< "y==x: " << y.compareTo(x) << endl;
            cout<< "b==b: " << b.compareTo(b) << endl;
            cout<< "x==x: " << x.compareTo(x) << endl;
            cout<< "y==y: " << y.compareTo(y) << endl;
    }
    


  • Jockelx schrieb:

    kurze_frage schrieb:

    Es stellt sich jedoch schon die Frage, warum sollte ich einen Fisch auf Gleichheit mit einer Katz vergleichen nur weil beides Lebewesen sind.

    Weil du vielleicht einen Fisch hast, von dem du wissen willst, ob er in einem Lebewesen-Container enthalten ist.

    Hä? Was hat der Lebewesen-Container mit einer Katze zu tun??
    fisch == katze muss wohl false sein.

    lebewesen == fisch und
    fisch == lebewesen müssen beide false ergeben. Objekt slicing beim Vergleich? Wo soll das sinnvoll sein?



  • Verstehe deine Frage nicht. Mit slicing hat das 0,0 zu tun.
    fisch == katze = false wird ja gerade durch das typeid erreicht.

    Pseudocode:

    class Tier
    {
      string farbe;
    };
    class Fisch : Tier;
    class Katze : Tier;
    
    vector<Tier*> tiere = {new Tier("gelb"), new Fisch("pink"), new Katze("lila")};
    Tier* fischpink= new Fisch("pink");
    Tier* fischlila= new Fisch("lila");
    for(Tier* t : tiere)
    {
      if(*t == *fischpink) cout << "Jau, pinken Fisch gibts schon";
      if(*t == *fischlila) cout << "Jau, lila Fisch gibts schon";
    }
    //Out: Jau, fischpink gibts
    


  • Aufpassen: nicht jede Vererbung erzeugt etwas komplett anderes. Klar koennen Fisch und Katze unterschiedliche Tiere sein, wenn ich aber nach dem Haustier mit dem Namen "Bello" suche, ist mir das aber egal ob es ein Fisch oder eine Katze ist.

    Auch kommt es ja oft vor dass man Vererbt um Funktionalitaet hinzuzufuegen. Rein auf die typeid zu testen limitiert solche Situationen. Aber Klassen Design ist immer auf einen speziellen Fall anzuwenden und es gibt kaum Allgemein gueltige Aussagen.



  • Mmh!? Schön, aber behauptet nur niemand!
    Eisflamme hat ein bestimmtes Scenario beschrieben und gefragt und wie man das 'schön' umsetzt.



  • Jockelx schrieb:

    Verstehe deine Frage nicht. Mit slicing hat das 0,0 zu tun.
    fisch == katze = false wird ja gerade durch das typeid erreicht.

    Pseudocode:

    class Tier
    {
      string farbe;
    };
    class Fisch : Tier;
    class Katze : Tier;
    
    vector<Tier*> tiere = {new Tier("gelb"), new Fisch("pink"), new Katze("lila")};
    Tier* fischpink= new Fisch("pink");
    Tier* fischlila= new Fisch("lila");
    for(Tier* t : tiere)
    {
      if(*t == *fischpink) cout << "Jau, pinken Fisch gibts schon";
      if(*t == *fischlila) cout << "Jau, lila Fisch gibts schon";
    }
    //Out: Jau, fischpink gibts
    

    ja so ungefähr, wenn du dich jetzt selber darum kümmerst das der Fisch ein Fisch ist, was so sein sollte denn warum sollte ein Fisch für dich testen das die Katze kein Fisch ist, dann gibts ganz normale == operatoren die Eigenes vergleichen und Reste an die Basis zum vergleich weiterleiten.

    for(Tier* t : tiere)
    {
      if (artgleich(t, fischpink) {
        if(*t == *fischpink) cout << "Jau, pinken Fisch gibts schon";
        if(*t == *fischlila) cout << "Jau, lila Fisch gibts schon";
      }
    }
    


  • Ich verstehe den Vorteil des externen Vergleichs auf Typgleichheit nicht. Könntest du den nochmal erläutern?



  • Eisflamme schrieb:

    Ich verstehe den Vorteil des externen Vergleichs auf Typgleichheit nicht. Könntest du den nochmal erläutern?

    bezahle nur für das was du brauchst dort wo du es brauchst.

    Ich sehe grade das das cast im Beispiel oben fehlt, so ist es komplett

    for(Tier* t : tiere)
    {
      Fish* f = dynamic_cast<Fish>(t);
      if (f) {
        if(*f == *fischpink) cout << "Jau, pinken Fisch gibts schon";
        if(*f == *fischlila) cout << "Jau, lila Fisch gibts schon";
      }
    

Log in to reply