¿Cuál es la forma correcta de sobrecargar al operador == para una jerarquía de clases?

Supongamos que tengo la siguiente 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 al 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 con solo referencias a A. Si las hago funciones miembro virtuales, entonces una versión derivada podría verse así:

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

Nuevamente, todavía tengo que lanzar (y se siente mal). ¿Hay una manera preferida de hacer esto?

Actualizar:

Hasta el momento, solo hay dos respuestas, pero parece que la forma correcta es análoga al operador de asignación:

  • Hacer clases no hojas abstractas
  • Protegido no virtual en las clases no hoja
  • Público no virtual en las clases de hoja

Cualquier bash del usuario de comparar dos objetos de diferentes tipos no se comstackrá 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.

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 concretos de clase hoja-nodo.

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 decir, isEqual ) que el operator== las clases derivadas podría usar.

P.ej

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

Al evitar tener un operator== que funciona en clases base abstractas y mantener las funciones de comparación protegidas, nunca se producen retrocesos accidentales en el código del cliente, donde solo se compara la parte base de dos objetos de tipo diferente.

No estoy seguro de si implementaría una función de comparación virtual con un dynamic_cast , sería reacio a hacer esto, pero si hubiera una necesidad comprobada, probablemente iría con una función virtual pura en la clase base ( no operator== ) que luego se anuló en las clases derivadas concretas como algo así, usando el 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; } 

El otro día estaba teniendo el mismo problema y se me ocurrió 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(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(other).baz; } }; 

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

Si hace la suposición razonable de que los tipos de ambos objetos deben ser idénticos para que sean iguales, hay una manera de reducir la cantidad de placa de caldera requerida en cada clase derivada. Esto sigue la recomendación de Herb Sutter de mantener los métodos virtuales protegidos y ocultos detrás de una interfaz pública. El patrón de plantilla curiosamente recurrente (CRTP) se utiliza para implementar el código repetitivo en el método equals , por lo que las clases derivadas no necesitan hacerlo.

 class A { public: bool operator==(const A& a) const { return equals(a); } protected: virtual bool equals(const A& a) const = 0; }; template class A_ : public A { protected: virtual bool equals(const A& a) const { const T* other = dynamic_cast(&a); return other != nullptr && static_cast(*this) == *other; } private: bool operator==(const A_& a) const // force derived classes to implement their own operator== { return false; } }; class B : public A_ { public: B(int i) : id(i) {} bool operator==(const B& other) const { return id == other.id; } private: int id; }; class C : public A_ { public: C(int i) : identity(i) {} bool operator==(const C& other) const { return identity == other.identity; } private: int identity; }; 

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

Si no quieres usar el casting y también asegúrate de que no compararás accidentalmente la instancia de B con la instancia de C, entonces necesitas reestructurar tu jerarquía de clases de la manera que sugiere Scott Meyers en el ítem 33 de C ++ más efectivo. 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 el caso de la operación de comparación, tiene sentido devolver el resultado falso al comparar la instancia de B con C.

A continuación se muestra un código de ejemplo que usa RTTI, y no divide la jerarquía de clases en hojas de concrea y base abstracta.

Lo bueno de este código de muestra es que no obtendrá std :: bad_cast al comparar instancias no relacionadas (como B con C). Aún así, el comstackdor le permitirá hacer lo que pueda desearse, puede implementar de la misma manera el operador

vivir

 #include  #include  #include  #include  #include  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(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(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)); } 

  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 el operador de implementación == parece una pregunta legítima, considere la posibilidad de borrado de tipo (considere la eliminación de tipo de todos modos, es una técnica encantadora). Aquí está Sean Parent describiéndolo. Entonces todavía tienes que hacer un despacho múltiple. Es un problema desagradable. Aquí hay una charla sobre eso.

  3. Considere usar variantes en lugar de jerarquía. Pueden hacer este tipo de cosas fácilmente.