2009-11-06 10 views
27

Supponiamo che io ho la seguente gerarchia di classe:Qual è il modo giusto per sovraccaricare l'operatore == per una gerarchia di classi?

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

A::~A() {} 

class B : public A 
{ 
    int bar; 
}; 

class C : public A 
{ 
    int baz; 
}; 

Qual è il modo giusto per sovraccaricare operator== per queste classi? Se faccio loro tutte le funzioni libere, allora B e C non possono sfruttare la versione di A senza casting. Sarebbe anche impedire a qualcuno di fare un confronto profondo avendo solo riferimenti a R. Se li faccio funzioni membro virtuali, poi una versione derivata potrebbe assomigliare a questo:

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; 
    } 
} 

Ancora una volta, devo ancora lanciare (e si sente sbagliato). C'è un modo preferito per farlo?

Aggiornamento:

Ci sono solo due risposte finora, ma sembra che la strada giusta è analogo a l'operatore di assegnazione:

  • Fai classi non foglia astratti
  • protette non virtuale nelle classi non foglia
  • Pubblico non virtuale nelle classi foglia

Qualsiasi tentativo dell'utente di confrontare due oggetti di tipi diversi non verrà compilato perché la funzione di base è protetta e le classi foglia possono sfruttare la versione del genitore per confrontare quella parte di dati.

+0

Questo è un doppio problema classico spedizione. O la tua gerarchia è conosciuta in anticipo, nel qual caso devi scrivere n * (n - 1)/2 funzioni, oppure no e devi trovare un altro modo (es. Restituire un hash dell'oggetto e confrontare gli hash). –

risposta

9

Per questo tipo di gerarchia seguirò sicuramente i consigli di Effective C++ di Scott Meyer ed eviterò di avere classi base concrete. Sembra che tu stia facendo questo in ogni caso.

Implementerei operator== come funzioni gratuite, probabilmente amici, solo per i tipi di classe concreti foglia-nodo.

Se la classe base deve disporre di membri dati, fornire una funzione di supporto non virtuale (probabilmente protetta) nella classe base (isEqual, ad esempio) che potrebbe essere utilizzata dalle classi derivate 'operator==.

E.g.

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

Evitando avere un operator== che funziona su classi base astratte e tenuta confrontare funzioni protette, non si ottiene mai accidentalmente fallback nel codice client in cui solo la parte di base di due in modo diverso digitato oggetti vengono confrontati.

Non sono sicuro se implementerei una funzione di confronto virtuale con un dynamic_cast, sarei riluttante a farlo ma se ci fosse una comprovata necessità probabilmente andrei con una pura funzione virtuale nella base classe (nonoperator==) che è stata quindi sostituita nelle classi derivate dal calcestruzzo come qualcosa di simile, utilizzando lo operator== per la classe derivata.

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

Hai definitivamente bisogno dell'operatore == nella classe astratta per garantire il polimorfismo. Non penso che questa risposta sia buona perché non risolve il problema. – fachexot

+0

In generale, penso che la classe base debba definire un operatore == sovraccarico (internamente o tramite la classe friend non importa) che controlla l'uguaglianza di tipo e chiama una funzione "equals" virtuale astratta che la classe derivata definirà. In quella funzione la classe derivata potrebbe anche usare static_cast perché il tipoid è già stato controllato per essere lo stesso.Il vantaggio è che l'utente, che dovrebbe in genere utilizzare solo l'interfaccia, è possibile utilizzare il == più semplice per confrontare due oggetti piuttosto che dover chiamare una funzione personalizzata – Triskeldeian

11

ho avuto lo stesso problema l'altro giorno e mi si avvicinò con la seguente soluzione:

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; 
    } 
}; 

La cosa che non mi piace di questo è l'assegno typeid. Cosa ne pensi?

+0

Penso che si otterrà più aiuto postando questo come una questione separata. Inoltre, dovresti considerare la risposta di Konrad Rudolph e pensare se hai davvero bisogno di usare 'operator ==' in questo modo. –

+1

Una domanda sul post di Konrad Rudolph: qual è la differenza tra un metodo equals virtuale e un operatore virtuale ==? AFAIK, gli operatori sono solo normali metodi con una notazione speciale. – Job

+1

@Job: lo sono. Ma un'aspettativa implicita è che gli operatori non eseguono operazioni virtuali, se ricordo correttamente ciò che Scott Meyers ha dovuto dire in Effective C++. Per essere onesti però, non ne sono più sicuro e non ho il libro a portata di mano proprio ora. –

8

Se si presuppone ragionevolmente che i tipi di entrambi gli oggetti devono essere identici affinché siano uguali, c'è un modo per ridurre la quantità di piastra della caldaia richiesta in ogni classe derivata. Questo segue Herb Sutter's recommendation per mantenere i metodi virtuali protetti e nascosti dietro un'interfaccia pubblica. Lo curiously recurring template pattern (CRTP) viene utilizzato per implementare il codice boilerplate nel metodo equals in modo che le classi derivate non siano necessarie.

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; 
}; 

vedere una demo in http://ideone.com/SymduV

+1

Con la tua ipotesi, penso che sarebbe più efficiente e più sicuro controllare l'uguaglianza di tipo nell'operatore della classe base e utilizzare il cast statico direttamente nella funzione equals. Usando il dynamic_cast significa che se T ha un'altra classe derivata, chiamala X si può confrontare un oggetto di tipo T e X attraverso la classe base e trovarli uguali anche se solo la parte T comune è effettivamente equivalente. Forse in alcuni casi è quello che vuoi ma nella maggior parte degli altri sarebbe un errore. – Triskeldeian

+0

@Triskeldeian sei un buon punto, ma a un certo livello ti aspetti che le classi derivate siano utili per il loro è una promessa. Vedo la tecnica che mostro sopra per essere più su un'implementazione a livello di interfaccia. –

+0

Ciò che conta davvero, IMHO, è che lo sviluppatore è consapevole dei rischi e delle ipotesi su entrambe le tecniche. Idealmente sono perfettamente d'accordo con te, ma sul punto pratico di te, considerando che lavoro principalmente con programmatori relativamente inesperti, quella scelta può essere più pericolosa in quanto può introdurre un errore molto sottile, difficile da individuare, che si insinua inaspettatamente. – Triskeldeian

0
  1. Penso che questo sembra strano:

    void foo(const MyClass& lhs, const MyClass& rhs) { 
        if (lhs == rhs) { 
        MyClass tmp = rhs; 
        // is tmp == rhs true? 
        } 
    } 
    
  2. Se attuazione operatore == sembra una domanda legittima, prendere in considerazione la cancellazione di tipo (tipo di considerazione cancellando comunque, è una tecnica adorabile). Here is Sean Parent describing it. Quindi devi ancora eseguire il dispacciamento multiplo. È un problema spiacevole. Here is a talk about it.

  3. Considerare l'utilizzo di varianti anziché gerarchia. Possono fare questo tipo di cose facilmente.

2

Se non volete usare fusione e anche fare in modo da non per caso confrontare istanza di B con istanza di C, allora avete bisogno di ristrutturare la gerarchia di classe in un modo come suggerisce Scott Meyers al punto 33 della Più efficace C++. In realtà questo articolo riguarda l'operatore di assegnazione, che in realtà non ha senso se utilizzato per tipi non correlati. In caso di operazione di confronto ha senso restituire false quando si confronta l'istanza di B con C.

Di seguito è riportato un codice di esempio che utilizza RTTI e non divide la gerarchia di classi in foglie concreate e base astratta.

La cosa buona di questo codice di esempio è che non si otterrà std :: bad_cast quando si confrontano istanze non correlate (come B con C). Tuttavia, il compilatore ti permetterà di fare ciò che potrebbe essere desiderato, potresti implementare nello stesso modo l'operatore < e usarlo per ordinare un vettore di varie istanze A, B e C.

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

Dovresti usare static_cast invece di dynamic_cast. Come hai già controllato il typeid, questo è sicuro e più veloce. – galinette

Problemi correlati