He implementado dos clases (operador primario y derivado) de comparación, pero al probarlo he notado un comportamiento confuso en caso de usar punteros. Además, tengo algunas otras preguntas sobre buenas prácticas.

Aquí está el código:

struct A
{
    int getN() { return _n; }  

    virtual bool equals(A &other) {
        return getN() == other.getN();
    }

    friend bool operator==(const A &one, const A &other);

    A(int n) : _n(n) { }

private:
    int _n;
};

bool operator==(const A &one, const A &other) {
    return one._n == other._n;
}

struct B : public A
{
    friend bool operator==(const B &one, const B &other);

    B(int n, int m = 0) : A(n), _m(m) { }

private:
    int _m;
};

bool operator==(const B &one, const B &other) {
    if( operator==(static_cast<const A&>(one), static_cast<const A&>(other)) ){
        return one._m == other._m;
    } else {
        return false;
    }
}

int main()
{
    A a(10), a2(10);
    B b(10, 20), b2(10, 20), b3(10, 30);

    A *a3 = new B(10, 20);

    bool x1 = a == a2;  // calls A operator (1)
    bool x2 = b == b2;  // calls B operator (2)
    bool x3 = a == b;   // calls A operator (3)
    bool x4 = b == a;   // calls A operator (4)

    bool x5 = b == *a3; // calls A operator (5)

    bool x6 = a == b3;  // calls A operator (6)
    bool x7 = b3 == a;  // calls A operator (7)

    return 0;
}

Preguntas

Comparando instancias A con B, se llama al operador de clase A, ¿es este el comportamiento correcto?

El punto 5 es el que me parece confuso. a3 se declara como A pero se instancia como B, pero se llama al operador de clase A. ¿Hay alguna forma de resolver esto?

Si el operador se implementó como un método de instancia , dependiendo de que se llame con un objeto A o uno B, el método ejecutado es diferente. Por ejemplo:

a == b // executes A::operator==
b == a // executes B::operator==

Supongo que esto es confuso y propenso a errores, y debe evitarse. Estoy en lo cierto?

2
Dan 6 mar. 2018 a las 20:41

3 respuestas

La mejor respuesta

Comparando A instancias con B, se llama al operador de clase A, ¿es este el comportamiento correcto?

Sí, porque esta es la única sobrecarga aplicable.

Si el operador se implementó como un método de instancia, dependiendo de que se llame con un objeto A o uno B, el método ejecutado es diferente. [...] Supongo que esto es confuso y propenso a errores, y debe evitarse.

Correcto, esta no es una buena idea, porque el operador de igualdad debe ser simétrico. Aunque es posible implementarlo simétricamente con dos operadores separados, introduce una responsabilidad de mantenimiento en el código.

Un enfoque para resolver esto es expandir su función miembro equals y hacer que cada subclase implemente la igualdad para su propio tipo:

struct A {
    int getN() const { return _n; }  

    virtual bool equals(A &other) const {
        return getN() == other.getN();
    }

    friend bool operator==(const A &one, const A &other);

    A(int n) : _n(n) { }

private:
    int _n;
};

struct B : public A
{
    B(int n, int m = 0) : A(n), _m(m) { }
    virtual bool equals(B &other) const {
        return A::equals(*this, other) && _m == other._m;
    }
private:
    int _m;
};

bool operator==(const A &one, const A &other) {
    return typeid(one)==typeid(two) && one.equals(two);
}

"En el corazón" de esta implementación se encuentra typeid operador, que le permite verificar la igualdad de tipos en tiempo de ejecución. La llamada virtual a one.equals(two) solo ocurre si los tipos de one y two son exactamente iguales.

Este enfoque pone la responsabilidad de la comparación de igualdad con la clase misma. En otras palabras, cada clase necesita saber cómo comparar sus propias instancias y, opcionalmente, confiar en su base para comparar su estado.

2
dasblinkenlight 6 mar. 2018 a las 18:06

Comparando instancias A con B, se llama al operador de clase A, ¿es este el comportamiento correcto?

Como no tiene ninguna función virtual en A, solo se ocupa de function overloading, lo que significa que el compilador decide qué función llamar en el momento de la compilación. Como B se hereda públicamente de A, el puntero o la referencia a B se pueden convertir implícitamente en A pero no viceversa. En este caso significa "a menos que ambos argumentos sean B, solo se llamará a la primera resolución". Tenga en cuenta que cuando desreferencia un puntero, solo el tipo de puntero es importante para determinar el tipo en el momento de la compilación, por lo que si A *pointer apunta a la instancia de A o B no importa en este caso, { {X10}} siempre tiene el tipo A.

Si desea que se invoquen funciones en función del tipo real, debe usar funciones virtuales, puede encontrar detalles sobre cómo hacerlo para operator== aquí implementando operador == cuando usa herencia

0
Slava 6 mar. 2018 a las 18:25

Al comparar A instancias con B, se llama al operador de clase A, es este es el comportamiento correcto?

B& se puede convertir a const A& pero A& no se puede convertir a const B&. Entonces, comparando a == b en realidad solo hay una opción.

Los "operadores de clase" no son una cosa. En su código, implementó operator== como funciones que no son miembros, y también se hizo amigo de A y B.

El punto 5 es el que me parece confuso. a3 se declara como A pero se instancia como B, pero se llama al operador de clase A. Hay alguna manera de resolver esto?

Anular la referencia a3 devuelve A&, no B&.

Si el operador se implementó como un método de instancia, dependiendo de ello se llama con un objeto A o uno B, el método ejecutado es diferente. Por ejemplo:

a == b // executes A::operator== 
b == a // executes B::operator== 

Supongo que esto es confuso y propenso a errores, y debe evitarse. Estoy en lo cierto?

Si operator== se implementa como un método de instancia, se llama en el operando de la izquierda. En el primer caso es a, en el segundo es b. Entonces b == a solo es válido si ha implementado bool B::operator==(const A&).

0
ryhp 6 mar. 2018 a las 18:22