new icon example borderfactory java c++ oop override variance

example - my icon java



¿Por qué no hay contra-varianza de parámetros para anular? (6)

C ++ y Java son compatibles con la covarianza de retorno cuando se reemplazan los métodos.

Sin embargo, ninguno de los dos admite la contra-varianza en los tipos de parámetros; en cambio, se traduce en sobrecarga (Java) u ocultación (C ++).

¿Por qué es eso ? Me parece que no hay daño en permitir eso. Puedo encontrar una razón para ello en Java, ya que tiene el mecanismo de "elegir la versión más específica" para la sobrecarga de todos modos, pero no puedo pensar en ninguna razón para C ++.

Ejemplo (Java):

class A { public void f(String s) {...} } class B extends A { public void f(Object o) {...} // Why doesn''t this override A.f? }


Aunque este es un buen lugar para tener en cualquier idioma, todavía necesito encontrar su aplicabilidad en mi trabajo actual.

Tal vez no hay realmente una necesidad para eso.


Gracias a Donroby por su respuesta anterior, me limito a extenderme.

interface Alpha interface Beta interface Gamma extends Alpha, Beta class A { public void f(Alpha a) public void f(Beta b) } class B extends A { public void f(Object o) { super.f(o); // What happens when o implements Gamma? } }

Te estás enfrentando a un problema similar al motivo por el cual se desaconseja la herencia de implementación múltiple. (Si intenta invocar Af (g) directamente, obtendrá un error de compilación).


Gracias a las respuestas de donroby y David, creo entender que el principal problema con la introducción de contra-varianza de parámetros es la integración con el mecanismo de sobrecarga .

Entonces, no solo hay un problema con una sola anulación para múltiples métodos, sino también de la otra manera:

class A { public void f(String s) {...} } class B extends A { public void f(String s) {...} // this can override A.f public void f(Object o) {...} // with contra-variance, so can this! }

Y ahora hay dos reemplazos válidos para el mismo método:

A a = new B(); a.f(); // which f is called?

Aparte de los problemas de sobrecarga, no podía pensar en otra cosa.

Editar: Desde entonces, he encontrado esta entrada C ++ FQA (20.8) que está de acuerdo con lo anterior: la presencia de sobrecarga crea un problema grave para la contra-varianza de los parámetros.


Para C ++, Stroustrup discute las razones para esconderse brevemente en la sección 3.5.3 de El diseño y la evolución de C ++ . Su razonamiento es (parafraseo) que otras soluciones plantean tantos problemas, y así ha sido desde C With Classes days.

Como ejemplo, da dos clases, y una clase derivada B. Ambas tienen una función de copia virtual () que toma un puntero de sus respectivos tipos. Si decimos:

A a; B b; b.copy( & a );

eso es actualmente un error, ya que B''s copy () oculta A''s. Si no fuera un error, solo las partes A de B podrían actualizarse mediante la función copy () de A.

Una vez más, he parafraseado: si está interesado, lea el libro, que es excelente.


En el puro problema de contra-varianza

Agregar contra-varianza a un lenguaje abre una gran cantidad de problemas potenciales o soluciones sucias y ofrece muy poca ventaja, ya que se puede simular fácilmente sin soporte de lenguaje:

struct A {}; struct B : A {}; struct C { virtual void f( B& ); }; struct D : C { virtual void f( A& ); // this would be contravariance, but not supported virtual void f( B& b ) { // [0] manually dispatch and simulate contravariance D::f( static_cast<A&>(b) ); } };

Con un simple salto adicional puede superar manualmente el problema de un lenguaje que no admite contra-varianza. En el ejemplo, f( A& ) no necesita ser virtual, y la llamada está completamente calificada para inhibir el mecanismo de despacho virtual.

Este enfoque muestra uno de los primeros problemas que surgen cuando se agrega contra-varianza a un idioma que no tiene despacho dinámico completo:

// assuming that contravariance was supported: struct P { virtual f( B& ); }; struct Q : P { virtual f( A& ); }; struct R : Q { virtual f( ??? & ); };

Con la contravarianza en efecto, Q::f sería una anulación de P::f , y eso estaría bien ya que para cada objeto o que puede ser un argumento de P::f , ese mismo objeto es un argumento válido para Q::f . Ahora, al agregar un nivel extra a la jerarquía, terminamos con un problema de diseño: ¿es R::f(B&) una invalidación válida de P::f o debería ser R::f(A&) ?

Sin contravarianza, R::f( B& ) es claramente una anulación de P::f , ya que la firma es una combinación perfecta. Una vez que agrega la contravarianza al nivel intermedio, el problema es que hay argumentos que son válidos en el nivel Q pero que no están en los niveles P o R Para que R cumpla los requisitos Q , la única opción es forzar que la firma sea R::f( A& ) , de modo que el siguiente código pueda compilarse:

int main() { A a; R r; Q & q = r; q.f(a); }

Al mismo tiempo, no hay nada en el lenguaje que impida el siguiente código:

struct R : Q { void f( B& ); // override of Q::f, which is an override of P::f virtual f( A& ); // I can add this };

Ahora tenemos un efecto divertido:

int main() { R r; P & p = r; B b; r.f( b ); // [1] calls R::f( B& ) p.f( b ); // [2] calls R::f( A& ) }

En [1], hay una llamada directa a un método miembro de R Como r es un objeto local y no una referencia o puntero, no existe un mecanismo dinámico de envío en su lugar y la mejor coincidencia es R::f( B& ) . Al mismo tiempo, en [2] la llamada se realiza a través de una referencia a la clase base y se activa el mecanismo de despacho virtual.

Como R::f( A& ) es la anulación de Q::f( A& ) que a su vez es la anulación de P::f( B& ) , el compilador debe llamar a R::f( A& ) . Si bien esto puede definirse perfectamente en el lenguaje, puede ser sorprendente descubrir que las dos llamadas casi exactas [1] y [2] en realidad llaman a métodos diferentes, y que en [2] el sistema llamaría una combinación no mejor de los argumentos.

Por supuesto, se puede argumentar de manera diferente: R::f( B& ) debe ser la anulación correcta, y no R::f( A& ) . El problema en este caso es:

int main() { A a; R r; Q & q = r; q.f( a ); // should this compile? what should it do? }

Si marca la clase Q , el código anterior es perfectamente correcto: Q::f toma una A& como argumento. El compilador no tiene motivos para quejarse sobre ese código. ¡Pero el problema es que bajo esta última suposición, R::f toma un B& y no un A& como argumento! La anulación real que estaría en su lugar no podría manejar el argumento a, incluso si la firma del método en el lugar de llamada parece perfectamente correcta. Este camino nos lleva a determinar que el segundo camino es mucho peor que el primero. R::f( B& ) no puede ser una anulación de Q::f( A& ) .

Siguiendo el principio de menor sorpresa, es mucho más simple tanto para el implementador del compilador como para el programador no tener contra varianza en los argumentos de la función. No porque no sea factible, sino porque el código daría caprichos y sorpresas, y considerando que hay soluciones simples si la función no está presente en el idioma.

En Sobrecarga vs Ocultar

Tanto en Java como en C ++, en el primer ejemplo (con A , B , C y D ) se elimina el despacho manual [0], C::f y D::f son firmas diferentes y no anulaciones. En ambos casos, en realidad son sobrecargas del mismo nombre de función con la ligera diferencia de que, debido a las reglas de búsqueda de C ++, la sobrecarga de C::f estará oculta por D::f . Pero eso solo significa que el compilador no encontrará la sobrecarga oculta por defecto, no es que no esté presente:

int main() { D d; B b; d.f( b ); // D::f( A& ) d.C::f( b ); // C::f( B& ) }

Y con un ligero cambio en la definición de clase, se puede hacer que funcione exactamente igual que en Java:

struct D : C { using C::f; // Bring all overloads of `f` in `C` into scope here virtual void f( A& ); }; int main() { D d; B b; d.f( b ); // C::f( B& ) since it is a better match than D::f( A& ) }


class A { public void f(String s) {...} public void f(Integer i) {...} } class B extends A { public void f(Object o) {...} // Which A.f should this override? }