2009-11-06 3 views
27

Angenommen, ich habe die folgende Klassenhierarchie:Was ist der richtige Weg, um den Operator == für eine Klassenhierarchie zu überladen?

class A 
{ 
    int foo; 
    virtual ~A() = 0; 
}; 

A::~A() {} 

class B : public A 
{ 
    int bar; 
}; 

class C : public A 
{ 
    int baz; 
}; 

Was ist der richtige Weg operator== für diese Klassen zu überlasten? Wenn ich alle freien Funktionen mache, können B und C die Version von A nicht ohne Casting nutzen. Es wäre auch jemand zu tun, einen tiefen Vergleich nur Verweise auf A. unter verhindern Wenn ich sie virtuelle Member-Funktionen zu machen, dann eine abgeleitete Version könnte wie folgt aussehen:

bool B::operator==(const A& rhs) const 
{ 
    const B* ptr = dynamic_cast<const B*>(&rhs);   
    if (ptr != 0) { 
     return (bar == ptr->bar) && (A::operator==(*this, rhs)); 
    } 
    else { 
     return false; 
    } 
} 

Wieder warf ich habe immer noch (und es fühlt sich falsch an). Gibt es einen bevorzugten Weg, dies zu tun?

Update:

Es gibt nur zwei Antworten so weit, aber es sieht aus wie der richtige Weg, um den Zuweisungsoperator ist analog:

  • Machen Sie nicht-Blatt Klassen abstrakt
  • Protected nicht-virtuellen in den nicht-Blatt-Klassen
  • öffentliche nichtvirtuellen in dem Blatt Klassen

Jeder Benutzer, der versucht, zwei Objekte unterschiedlicher Typen zu vergleichen, wird nicht kompiliert, da die Basisfunktion geschützt ist, und die Blattklassen können die Version des übergeordneten Elements verwenden, um diesen Teil der Daten zu vergleichen.

+0

Dies ist ein klassisches Double-Dispatch Problem. Entweder ist Ihre Hierarchie vorher bekannt, in diesem Fall müssen Sie n * (n - 1)/2 Funktionen schreiben, oder nicht und Sie müssen einen anderen Weg finden (z. B. einen Hash des Objekts zurückgeben und Hashes vergleichen). –

Antwort

9

Für diese Art von Hierarchie würde ich auf jeden Fall den Scott Meyer's Effective C++ - Rat befolgen und vermeiden, irgendwelche konkreten Basisklassen zu haben. Sie scheinen das auf jeden Fall zu tun.

Ich würde operator== als freie Funktionen implementieren, wahrscheinlich Freunde, nur für die konkreten Blatt-Knoten-Klassen-Typen.

Wenn die Basisklasse Datenelemente haben muss, würde ich eine (wahrscheinlich geschützte) nicht virtuelle Hilfsfunktion in der Basisklasse (, sagen wir), die die abgeleiteten Klassen 'operator== könnte verwenden.

z.

bool operator==(const B& lhs, const B& rhs) 
{ 
    lhs.isEqual(rhs) && lhs.bar == rhs.bar; 
} 

Durch die Vermeidung eines operator== aufweist, die auf abstrakte Basisklassen und Haltung funktioniert vergleichen Funktionen geschützt, Sie schon einmal versehentlich Fallbacks in Client-Code nicht erhalten, wo nur das Basisteil von zwei unterschiedlich typisierte Objekte verglichen werden.

Ich bin mir nicht sicher, ob ich eine virtuelle Vergleichsfunktion mit einer dynamic_cast implementieren würde, würde ich ungern tun, aber wenn es eine nachgewiesene Notwendigkeit dafür wäre, würde ich wahrscheinlich mit einer reinen virtuellen Funktion in der Basis gehen Klasse (nichtoperator==), die dann in den konkreten abgeleiteten Klassen als etwas wie diese überschrieben wurde, die operator== für die abgeleitete Klasse verwenden.

bool B::pubIsEqual(const A& rhs) const 
{ 
    const B* b = dynamic_cast< const B* >(&rhs); 
    return b != NULL && *this == *b; 
} 
+2

Sie benötigen definitiv den Operator == in der abstrakten Klasse, um Polymorphie zu erhalten. Ich denke nicht, dass diese Antwort gut ist, weil sie das Problem nicht löst. – fachexot

+0

Im Allgemeinen denke ich, dass die Basisklasse einen Operator == overload (intern oder über die friend-Klasse ist egal) definieren sollte, der typeid-Gleichheit überprüft und eine abstrakte virtuelle "equals" -Funktion aufruft, die die abgeleitete Klasse definieren wird. In dieser Funktion könnte die abgeleitete Klasse sogar static_cast verwenden, da die typeid bereits als gleich geprüft wurde.Der Vorteil ist, dass der Benutzer, der normalerweise nur die Schnittstelle verwenden sollte, den einfacheren == verwenden kann, um zwei Objekte zu vergleichen, anstatt eine benutzerdefinierte Funktion aufzurufen. – Triskeldeian

11

ich neulich das gleiche Problem habe und ich kam mit folgenden Lösung:

struct A 
{ 
    int foo; 
    A(int prop) : foo(prop) {} 
    virtual ~A() {} 
    virtual bool operator==(const A& other) const 
    { 
     if (typeid(*this) != typeid(other)) 
      return false; 

     return foo == other.foo; 
    } 
}; 

struct B : A 
{ 
    int bar; 
    B(int prop) : A(1), bar(prop) {} 
    bool operator==(const A& other) const 
    { 
     if (!A::operator==(other)) 
      return false; 

     return bar == static_cast<const B&>(other).bar; 
    } 
}; 

struct C : A 
{ 
    int baz; 
    C(int prop) : A(1), baz(prop) {} 
    bool operator==(const A& other) const 
    { 
     if (!A::operator==(other)) 
      return false; 

     return baz == static_cast<const C&>(other).baz; 
    } 
}; 

Die Sache Ich mag nicht über dies ist die typeid überprüfen. Was denkst du darüber?

+0

Ich denke, dass Sie mehr Hilfe erhalten, wenn Sie dies als separate Frage veröffentlichen. Außerdem sollten Sie die Antwort von Konrad Rudolph in Betracht ziehen und überlegen, ob Sie auf diese Weise wirklich operator == verwenden müssen. –

+1

Eine Frage zu Konrad Rudolphs Beitrag: Was ist der Unterschied zwischen einer virtuellen Equals-Methode und einem virtuellen Operator ==? AFAIK, Operatoren sind nur normale Methoden mit einer speziellen Notation. – Job

+1

@Job: Sie sind. Aber eine implizite Erwartung ist, dass Operatoren keine virtuellen Operationen ausführen, wenn ich mich richtig erinnere, was Scott Meyers in Effective C++ zu sagen hatte. Um ehrlich zu sein, bin ich mir nicht mehr sicher und ich habe das Buch gerade nicht zur Hand. –

8

Wenn Sie die vernünftige Annahme machen, dass die Typen beider Objekte identisch sein müssen, damit sie gleich sind, gibt es eine Möglichkeit, die erforderliche Menge an Boiler-Plate in jeder abgeleiteten Klasse zu reduzieren. Dies folgt Herb Sutter's recommendation, um virtuelle Methoden geschützt und hinter einer öffentlichen Schnittstelle verborgen zu halten. Die curiously recurring template pattern (CRTP) wird verwendet, um den Boilerplate-Code in der equals-Methode zu implementieren, so dass die abgeleiteten Klassen dies nicht tun müssen.

class A 
{ 
public: 
    bool operator==(const A& a) const 
    { 
     return equals(a); 
    } 
protected: 
    virtual bool equals(const A& a) const = 0; 
}; 

template<class T> 
class A_ : public A 
{ 
protected: 
    virtual bool equals(const A& a) const 
    { 
     const T* other = dynamic_cast<const T*>(&a); 
     return other != nullptr && static_cast<const T&>(*this) == *other; 
    } 
private: 
    bool operator==(const A_& a) const // force derived classes to implement their own operator== 
    { 
     return false; 
    } 
}; 

class B : public A_<B> 
{ 
public: 
    B(int i) : id(i) {} 
    bool operator==(const B& other) const 
    { 
     return id == other.id; 
    } 
private: 
    int id; 
}; 

class C : public A_<C> 
{ 
public: 
    C(int i) : identity(i) {} 
    bool operator==(const C& other) const 
    { 
     return identity == other.identity; 
    } 
private: 
    int identity; 
}; 

Sehen Sie eine Demo an http://ideone.com/SymduV

+1

Mit Ihrer Annahme, ich denke, es wäre effizienter und sicherer, die Typid-Gleichheit im Basisklassenoperator zu überprüfen und statische Besetzung direkt in der Equals-Funktion verwenden. Die Verwendung von dynamic_cast bedeutet, dass das if T eine andere abgeleitete Klasse hat, rufe es X auf, könnte man ein Objekt vom Typ T und X durch die Basisklasse vergleichen und finde sie gleich, auch wenn nur der gemeinsame T-Teil tatsächlich äquivalent ist. Vielleicht ist es in einigen Fällen das, was Sie wollen, aber in den meisten anderen Fällen wäre es ein Fehler. – Triskeldeian

+0

@Triskeldeian Sie machen einen guten Punkt, aber auf einer bestimmten Ebene erwarten Sie abgeleitete Klassen, um ihr Versprechen zu erfüllen. Ich sehe die Technik, die ich oben gezeigt habe, mehr über eine Implementierung auf Interface-Ebene. –

+0

Was wirklich wichtig ist, IMHO, ist, dass der Entwickler sich der Risiken und Annahmen der beiden Techniken bewusst ist. Im Idealfall stimme ich perfekt mit Ihnen überein, aber wenn Sie bedenken, dass ich hauptsächlich mit relativ unerfahrenen Programmierern arbeite, kann diese Wahl gefährlicher sein, da sie einen sehr subtilen Fehler verursachen kann, der schwer zu erkennen ist und unerwartet eindringt. – Triskeldeian

0
  1. Ich denke, das seltsam aussieht:

    void foo(const MyClass& lhs, const MyClass& rhs) { 
        if (lhs == rhs) { 
        MyClass tmp = rhs; 
        // is tmp == rhs true? 
        } 
    } 
    
  2. Wenn Betreiber Umsetzung == wie eine legitime Frage scheint, Typ Löschung betrachten (man denke Typ löschen sowieso, es ist eine schöne Technik). Here is Sean Parent describing it. Dann müssen Sie noch einige Multi-Dispatching tun. Es ist ein unangenehmes Problem. Here is a talk about it.

  3. Verwenden Sie Varianten anstelle von Hierarchien. Sie können diese Art von Dingen leicht tun.

2

Wenn Sie nicht Gießen verwenden möchten, und auch sicherstellen, dass Sie nicht versehentlich Instanz von B mit Instanz von C vergleichen, dann müssen Sie Ihre Klassenhierarchie in einer Weise neu strukturieren wie Scott Meyers in Artikel schlägt vor, 33 Effektiveres C++. Eigentlich handelt es sich um einen Zuweisungsoperator, der bei nicht verwandten Typen keinen Sinn ergibt. Im Fall der Vergleichsoperation ist es sinnvoll, beim Vergleich der Instanz von B mit C false zurückzugeben.

Unten ist ein Beispielcode, der RTTI verwendet und die Klassenhierarchie nicht in Concreate-Blätter und abstrakte Basis unterteilt.

Die gute Sache über diesen Beispielcode ist, dass Sie std :: bad_cast nicht erhalten, wenn Sie nicht verwandte Instanzen (wie B mit C) vergleichen. Dennoch, der Compiler wird Ihnen erlauben, es zu tun, was vielleicht gewünscht ist, Sie könnten auf die gleiche Weise den Operator < implementieren und ihn zum Sortieren eines Vektors verschiedener A-, B- und C-Instanzen verwenden.

live

#include <iostream> 
#include <string> 
#include <typeinfo> 
#include <vector> 
#include <cassert> 

class A { 
    int val1; 
public: 
    A(int v) : val1(v) {} 
protected: 
    friend bool operator==(const A&, const A&); 
    virtual bool isEqual(const A& obj) const { return obj.val1 == val1; } 
}; 

bool operator==(const A& lhs, const A& rhs) { 
    return typeid(lhs) == typeid(rhs) // Allow compare only instances of the same dynamic type 
      && lhs.isEqual(rhs);  // If types are the same then do the comparision. 
} 

class B : public A { 
    int val2; 
public: 
    B(int v) : A(v), val2(v) {} 
    B(int v, int v2) : A(v2), val2(v) {} 
protected: 
    virtual bool isEqual(const A& obj) const override { 
     auto v = dynamic_cast<const B&>(obj); // will never throw as isEqual is called only when 
               // (typeid(lhs) == typeid(rhs)) is true. 
     return A::isEqual(v) && v.val2 == val2; 
    } 
}; 

class C : public A { 
    int val3; 
public: 
    C(int v) : A(v), val3(v) {} 
protected: 
    virtual bool isEqual(const A& obj) const override { 
     auto v = dynamic_cast<const C&>(obj); 
     return A::isEqual(v) && v.val3 == val3; 
    } 
}; 

int main() 
{ 
    // Some examples for equality testing 
    A* p1 = new B(10); 
    A* p2 = new B(10); 
    assert(*p1 == *p2); 

    A* p3 = new B(10, 11); 
    assert(!(*p1 == *p3)); 

    A* p4 = new B(11); 
    assert(!(*p1 == *p4)); 

    A* p5 = new C(11); 
    assert(!(*p4 == *p5)); 
} 
+0

Sie sollten static_cast anstelle von dynamic_cast verwenden. Wenn Sie den Typid bereits überprüft haben, ist dies sicher und schneller. – galinette

Verwandte Themen