2009-11-06 20 views
27

Supongamos que tengo la siguiente jerarquía de clases:¿Cuál es la forma correcta de sobrecargar el operador == para una jerarquía de clases?

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

A::~A() {} 

class B : public A 
{ 
    int bar; 
}; 

class C : public A 
{ 
    int baz; 
}; 

¿Cuál es la forma correcta de sobrecargar operator== para estas clases? Si hago todas las funciones gratuitas, entonces B y C no pueden aprovechar la versión de A sin conversión. También evitaría que alguien haga una comparación profunda que tiene sólo referencias a A. Si las hago funciones miembro virtuales, a continuación, una versión derivada podría tener este aspecto:

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

Una vez más, todavía tengo a emitir (y se siente mal). ¿Hay una manera preferida de hacer esto?

Actualización:

Sólo hay dos respuestas hasta el momento, pero se ve como la manera correcta es análogo al operador de asignación:

  • hacer que las clases que no son hojas abstractas
  • Protegidas no virtual en las clases que no son hojas
  • Público no virtual en las clases de hojas

Cualquier intento de un usuario para comparar dos objetos de diferentes tipos no se compilará porque la función base está protegida, y las clases de hojas pueden aprovechar la versión de los padres para comparar esa parte de los datos.

+0

Este es un problema clásico de despacho doble. Puede ser que su jerarquía se conoce de antemano, en cuyo caso debe escribir n * (n - 1) (. Por ejemplo devolver un hash del objeto y comparar los valores hash)/2 funciones, o no lo es y tiene que encontrar otra manera. –

Respuesta

9

Para este tipo de jerarquía, definitivamente seguiría el consejo de Cype Effective C++ de Scott Meyer y evitaría tener clases base concretas. Usted parece estar haciendo esto en cualquier caso.

Implementaría operator== como funciones gratuitas, probablemente amigos, solo para los tipos de clase hoja-nodo concretos.

Si la clase base tiene que tener miembros de datos, entonces proporcionaría una función de ayuda no virtual (probablemente protegida) en la clase base (isEqual, por ejemplo) que las clases derivadas 'operator== podrían usar.

E.g.

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

Evitando tener un operator== que funciona en las clases base abstractas y mantenimiento de comparar las funciones protegidas, que no siempre obtiene accidentalmente retrocesos en el código de cliente, donde sólo la parte de base de forma diferente dos mecanografiado objetos se comparan.

No estoy seguro de si implementaría una función de comparación virtual con un dynamic_cast, me resistiría a hacer esto, pero si hubiera una necesidad probada probablemente iría con una función virtual pura en la base clase (nooperator==) que luego se anuló en las clases derivadas de hormigón como algo así, usando operator== para la clase derivada.

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

Definitivamente necesita el operador == en la clase abstracta para otorgar polimorfismo. No creo que esta respuesta sea buena porque no resuelve el problema. – fachexot

+0

En general, creo que la clase base debería definir una sobrecarga de operador == (internamente o mediante clase de amigo no importa) que verifica la igualdad de tipo y llama a una función abstracta abstracta "igual" que la clase derivada definirá. En esa función, la clase derivada podría incluso usar static_cast porque ya se ha comprobado que typeid es el mismo.La ventaja es que el usuario, que normalmente solo debe usar la interfaz, puede usar el más sencillo == para comparar dos objetos en lugar de tener que llamar a una función personalizada – Triskeldeian

11

que estaba teniendo el mismo problema que el otro día y me encontré con la siguiente solución:

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

Lo que no me gusta de esto es el cheque typeid. ¿Qué piensa usted al respecto?

+0

. Creo que obtendrá más ayuda publicando esto como una pregunta separada. Además, debería considerar la respuesta de Konrad Rudolph y pensar si realmente necesita usar 'operator ==' de esta manera. –

+1

Una pregunta sobre la publicación de Konrad Rudolph: ¿cuál es la diferencia entre un método de igualdad virtual y un operador virtual ==? AFAIK, los operadores son solo métodos normales con una notación especial. – Job

+1

@Job: lo son. Pero una expectativa implícita es que los operadores no realizan operaciones virtuales, si recuerdo correctamente lo que Scott Meyers tuvo que decir en Effective C++. Para ser justos, ya no estoy seguro y no tengo el libro a mano ahora. –

8

Si usted hace la suposición razonable de que los tipos de los dos objetos deben ser idénticos para que sean iguales, hay una manera para reducir la cantidad de caldera de la placa requerida en cada clase derivada. Esto sigue a Herb Sutter's recommendation para mantener los métodos virtuales protegidos y escondidos detrás de una interfaz pública. El curiously recurring template pattern (CRTP) se utiliza para implementar el código repetitivo en el método equals por lo que las clases derivadas no necesitan.

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

ver una demostración en http://ideone.com/SymduV

+1

Supongo que sería más eficiente y seguro comprobar la igualdad de tipo en el operador de la clase base y usar el molde estático directamente en la función de igual. Usar el dynamic_cast significa que si if tiene otra clase derivada, llámela X uno podría comparar un objeto de tipo T y X a través de la clase base y los encontraría iguales aunque solo la parte T común sea realmente equivalente. Quizás en algunos casos es lo que quieres, pero en la mayoría de los otros sería un error. – Triskeldeian

+0

@Triskeldeian usted hace un buen punto, pero en algún nivel usted espera que las clases derivadas cumplan con su promesa. Veo que la técnica que muestro más arriba es más sobre una implementación a nivel de interfaz. –

+0

Lo que realmente importa, en mi humilde opinión, es que el desarrollador es consciente de los riesgos y las suposiciones en cualquiera de las técnicas. Lo ideal sería estar de acuerdo con usted pero, desde el punto de vista práctico, considerando que trabajo principalmente con programadores relativamente inexpertos, esa elección puede ser más peligrosa ya que puede introducir un error muy sutil, difícil de detectar, que se cuela inesperadamente. – Triskeldeian

0
  1. Creo que esto se ve raro:

    void foo(const MyClass& lhs, const MyClass& rhs) { 
        if (lhs == rhs) { 
        MyClass tmp = rhs; 
        // is tmp == rhs true? 
        } 
    } 
    
  2. Si la implementación de operador == parece una pregunta de fiar, considerar el tipo de borrado (considerar el tipo de borrado de todos modos, es una técnica encantadora). Here is Sean Parent describing it. Entonces todavía tiene que hacer un despacho múltiple. Es un problema desagradable. Here is a talk about it.

  3. considerar el uso de variantes en lugar de jerarquía. Pueden hacer este tipo de cosas fácilmente.

2

Si no desea utilizar la fundición y también asegurarse de que no va por accidente comparar instancia de B con instancia de C, entonces usted necesita para reestructurar la jerarquía de clases de una manera como Scott Meyers sugiere en el punto 33 de Más efectivo C++. En realidad, este elemento trata sobre el operador de asignación, lo que realmente no tiene sentido si se usa para tipos no relacionados. En caso de funcionamiento compararlo tipo de sentido falso de vuelta al comparar instancia de B con C

A continuación se muestra el código de ejemplo que utiliza RTTI, y no divide jerarquía clase en hojas y concreate base abstracta.

Lo bueno de este código de ejemplo es que no se va a std :: bad_cast al comparar los casos no relacionados (como B con C). Aún así, el compilador le permitirá hacerlo, que sería de desear, se podría implementar en el mismo operador de manera < y utilizarlo para clasificar un vector de varios casos A, B y 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

Debe usar static_cast en lugar de dynamic_cast. Como ya ha comprobado el typeid, esto es seguro y más rápido. – galinette

Cuestiones relacionadas