¿Cuál es el problema exacto con la herencia múltiple?

121

Puedo ver personas preguntando todo el tiempo si se debe incluir la herencia múltiple en la próxima versión de C # o Java. Las personas de C ++, que tienen la suerte de tener esta habilidad, dicen que esto es como darle una cuerda a alguien para que se ahorque.

¿Qué pasa con la herencia múltiple? ¿Hay alguna muestra de concreto?

Vlad Gudim
fuente
54
Solo mencionaría que C ++ es excelente para darle suficiente cuerda para ahorcarse.
tloach
1
Para una alternativa a la herencia múltiple que aborda (y, en mi humilde opinión, resuelve) muchos de los mismos problemas, consulte Rasgos ( iam.unibe.ch/~scg/Research/Traits )
Bevan
52
Pensé que C ++ te da suficiente cuerda para dispararte en el pie.
KeithB
66
Esta pregunta parece suponer que hay un problema con MI en general, mientras que he encontrado muchos idiomas en los que MI es de uso casual. Ciertamente, hay problemas con el manejo de MI en ciertos idiomas, pero no estoy al tanto de que el MI en general tiene problemas importantes.
David Thornley

Respuestas:

86

El problema más obvio es con la anulación de funciones.

Digamos que tiene dos clases Ay B, las cuales definen un método doSomething. Ahora define una tercera clase C, que hereda de ambos Ay B, pero no anula el doSomethingmétodo.

Cuando el compilador inicia este código ...

C c = new C();
c.doSomething();

... ¿Qué implementación del método debería usar? Sin ninguna otra aclaración, es imposible que el compilador resuelva la ambigüedad.

Además de anular, el otro gran problema con la herencia múltiple es el diseño de los objetos físicos en la memoria.

Lenguajes como C ++ y Java y C # crean un diseño fijo basado en direcciones para cada tipo de objeto. Algo como esto:

class A:
    at offset 0 ... "abc" ... 4 byte int field
    at offset 4 ... "xyz" ... 8 byte double field
    at offset 12 ... "speak" ... 4 byte function pointer

class B:
    at offset 0 ... "foo" ... 2 byte short field
    at offset 2 ... 2 bytes of alignment padding
    at offset 4 ... "bar" ... 4 byte array pointer
    at offset 8 ... "baz" ... 4 byte function pointer

Cuando el compilador genera código de máquina (o código de bytes), utiliza esos desplazamientos numéricos para acceder a cada método o campo.

La herencia múltiple lo hace muy complicado.

Si la clase Chereda de ambos Ay B, el compilador tiene que decidir si desea diseñar los datos en ABorden o en BAorden.

Pero ahora imagina que estás llamando métodos a un Bobjeto. ¿Es realmente solo un B? ¿O es realmente un Cobjeto que se llama polimórficamente, a través de su Binterfaz? Dependiendo de la identidad real del objeto, el diseño físico será diferente, y es imposible saber el desplazamiento de la función para invocar en el sitio de la llamada.

La forma de manejar este tipo de sistema es deshacerse del enfoque de diseño fijo, permitiendo que cada objeto sea consultado por su diseño antes de intentar invocar las funciones o acceder a sus campos.

Entonces ... larga historia corta ... es un dolor en el cuello para los autores del compilador admitir la herencia múltiple. Entonces, cuando alguien como Guido van Rossum diseña python, o cuando Anders Hejlsberg diseña c #, saben que admitir la herencia múltiple hará que las implementaciones del compilador sean significativamente más complejas, y presumiblemente no creen que el beneficio valga la pena.

benjismith
fuente
62
Ehm, Python admite MI
Nemanja Trifunovic
26
Estos no son argumentos muy convincentes: el diseño fijo no es complicado en la mayoría de los idiomas; en C ++ es complicado porque la memoria no es opaca y, por lo tanto, puede tener dificultades con los supuestos aritméticos del puntero. En los lenguajes donde las definiciones de clase son estáticas (como en java, C # y C ++), se pueden prohibir los conflictos de nombres de herencia múltiples (¡y C # hace esto de todos modos con las interfaces!).
Eamon Nerbonne
10
El OP solo quería entender los problemas, y los expliqué sin editorializar personalmente sobre el tema. Acabo de decir que los diseñadores de lenguaje y los implementadores del compilador "presumiblemente no creen que el beneficio valga la pena".
benjismith
12
" El problema más obvio es con la anulación de funciones " . Esto no tiene nada que ver con la anulación de funciones. Es un simple problema de ambigüedad.
curioso el
10
Esta respuesta tiene información incorrecta sobre Guido y Python, ya que Python admite MI. "Decidí que mientras apoyara la herencia, también podría soportar una versión simple de herencia múltiple". - Guido van Rossum python-history.blogspot.com/2009/02/… - Además, la resolución de ambigüedad es bastante común en los compiladores (las variables pueden ser locales para bloquear, locales para funcionar, locales para encerrar funciones, miembros de objetos, miembros de clase, globales, etc.), no veo cómo un alcance adicional marcaría la diferencia.
marcus
46

Los problemas que ustedes mencionan no son realmente tan difíciles de resolver. De hecho, por ejemplo, Eiffel lo hace perfectamente bien. (y sin introducir opciones arbitrarias o lo que sea)

Por ejemplo, si hereda de A y B, ambos tienen el método foo (), entonces, por supuesto, no desea una elección arbitraria en su clase C heredando de A y B. Debe redefinir foo para que quede claro qué será se usa si se llama a c.foo () o de lo contrario debe cambiar el nombre de uno de los métodos en C. (podría convertirse en bar ())

También creo que la herencia múltiple a menudo es bastante útil. Si miras las bibliotecas de Eiffel, verás que se usa por todas partes y, personalmente, me perdí la función cuando tuve que volver a programar en Java.


fuente
26
Estoy de acuerdo. La razón principal por la que las personas odian el MI es la misma que con JavaScript o con la escritura estática: la mayoría de las personas solo han usado implementaciones muy malas, o lo han usado muy mal. Juzgar MI por C ++ es como juzgar OOP por PHP o juzgar automóviles por Pintos.
Jörg W Mittag
2
@curiousguy: MI introduce otro conjunto de complicaciones de las que preocuparse, al igual que muchas de las "características" de C ++. El hecho de que sea inequívoco no facilita el trabajo o la depuración. Eliminando esta cadena ya que salió del tema y la sopló de todos modos.
Guvante
44
@Guvante, el único problema con MI en cualquier idioma son los programadores de mierda que piensan que pueden leer un tutorial y de repente conocer un idioma.
Miles Rout
2
Yo diría que las características del lenguaje no son solo para reducir el tiempo de codificación. También se trata de aumentar la expresividad de un idioma y aumentar el rendimiento.
Miles Rout
44
Además, los errores solo ocurren en MI cuando los idiotas lo usan incorrectamente.
Miles Rout
27

El problema del diamante :

surge una ambigüedad cuando dos clases B y C heredan de A, y la clase D hereda de B y C. Si hay un método en A que B y C han anulado , y D no lo anula, entonces qué versión del ¿Qué método hereda D: el de B o el de C?

... Se llama el "problema del diamante" debido a la forma del diagrama de herencia de clase en esta situación. En este caso, la clase A está en la parte superior, tanto B como C por separado debajo de ella, y D une las dos en la parte inferior para formar una forma de diamante ...

J Francis
fuente
44
que tiene una solución conocida como herencia virtual. Solo es un problema si lo haces mal.
Ian Goldby
1
@IanGoldby: la herencia virtual es un mecanismo para resolver parte del problema, si no es necesario permitir las subidas y bajadas que preservan la identidad entre todos los tipos de los que se deriva una instancia o para la que es sustituible . Dado X: B; Y: B; y Z: X, Y; suponga que someZ es una instancia de Z. Con herencia virtual, (B) (X) someZ y (B) (Y) someZ son objetos distintos; dado cualquiera, uno podría obtener el otro a través de un downcast y upcast, pero ¿qué pasa si uno tiene un someZy quiere lanzarlo Objecty luego B? ¿Cuál Bconseguirá?
supercat
2
@supercat Quizás, pero problemas como ese son en gran medida teóricos, y en cualquier caso pueden ser señalados por el compilador. Lo importante es ser consciente de qué problema está tratando de resolver, y luego usar la mejor herramienta, ignorando el dogma de las personas que prefieren no preocuparse por comprender "¿por qué?"
Ian Goldby
@IanGoldby: Problemas como ese solo pueden ser señalados por el compilador si tiene acceso simultáneo a todas las clases en cuestión. En algunos marcos, cualquier cambio en una clase base siempre requerirá una recompilación de todas las clases derivadas, pero la capacidad de usar versiones más nuevas de las clases base sin tener que recompilar las clases derivadas (para las cuales uno podría no tener código fuente) es una característica útil. para marcos que pueden proporcionarlo. Además, los problemas no son solo teóricos. Muchas clases de .NET se basan en el hecho de que un elenco de cualquier tipo de referencia para Objecty de nuevo a ese tipo ...
supercat
3
@IanGoldby: Bastante justo. Mi punto fue que los implementadores de Java y .NET no solo fueron "flojos" al decidir no admitir MI generalizado; apoyar el MI generalizado habría evitado que su marco mantuviera varios axiomas cuya validez es más útil para muchos usuarios que el MI.
supercat
21

La herencia múltiple es una de esas cosas que no se usa con frecuencia, y puede ser mal utilizada, pero a veces es necesaria.

Nunca entendí no agregar una función, solo porque podría ser mal utilizada, cuando no hay buenas alternativas. Las interfaces no son una alternativa a la herencia múltiple. Por un lado, no le permiten hacer cumplir precondiciones o postcondiciones. Al igual que cualquier otra herramienta, debe saber cuándo es apropiado usarlo y cómo usarlo.

KeithB
fuente
¿Puede explicar por qué no le permiten hacer cumplir las condiciones previas y posteriores?
Hasta el
2
@Yttrill porque las interfaces no pueden tener implementaciones de métodos. ¿Dónde pones el assert?
curioso
1
@curiousguy: utiliza un lenguaje con una sintaxis adecuada que le permite colocar las condiciones previas y posteriores directamente en la interfaz: no es necesario "afirmar". Ejemplo de Felix: fun div (num: int, den: int when den! = 0): int esperar resultado == 0 implica num == 0;
Yttrill
@Ytrill OK, pero algunos lenguajes, como Java, no admiten MI ni "condiciones previas y posteriores directamente en la interfaz".
curioso
No se usa a menudo porque no está disponible, y no sabemos cómo usarlo bien. Si echas un vistazo a algún código Scala, verás cómo las cosas comienzan a ser comunes y se pueden refactorizar a rasgos (Ok, no es MI, pero prueba mi punto).
santiagobasulto
16

Supongamos que tiene objetos A y B, ambos heredados por C. A y B implementan foo () y C no. Llamo a C.foo (). ¿Qué implementación se elige? Hay otros problemas, pero este tipo de cosas es grande.

tloach
fuente
1
Pero ese no es realmente un ejemplo concreto. Si tanto A como B tienen una función, es muy probable que C también necesite su propia implementación. De lo contrario, todavía puede llamar a A :: foo () en su propia función foo ().
Peter Kühne
@Quantum: ¿Qué pasa si no es así? Es fácil ver el problema con un nivel de herencia, pero si tiene muchos niveles y tiene alguna función aleatoria que está en algún lugar dos veces, esto se convierte en un problema muy difícil.
tloach
Además, el punto no es que no pueda llamar al método A o B al especificar cuál desea, el punto es que si no especifica, no hay una buena manera de elegir uno. No estoy seguro de cómo C ++ maneja esto, pero si alguien sabe, ¿podría mencionarlo?
tloach
2
@tloach: si C no resuelve la ambigüedad, el compilador puede detectar este error y devolver un error en tiempo de compilación.
Eamon Nerbonne
@Earmon: debido al polimorfismo, si foo () es virtual, es posible que el compilador ni siquiera sepa en tiempo de compilación que esto será un problema.
tloach
5

El principal problema con la herencia múltiple se resume muy bien con el ejemplo de tloach. Al heredar de múltiples clases base que implementan la misma función o campo, es el compilador el que debe tomar una decisión sobre qué implementación heredar.

Esto empeora cuando hereda de varias clases que heredan de la misma clase base. (herencia de diamantes, si dibujas el árbol de herencia obtienes una forma de diamante)

Estos problemas no son realmente problemáticos para que un compilador los supere. Pero la elección que tiene que hacer el compilador aquí es bastante arbitraria, esto hace que el código sea mucho menos intuitivo.

Encuentro que cuando hago un buen diseño OO nunca necesito herencia múltiple. En los casos en que lo necesito, generalmente encuentro que he estado usando la herencia para reutilizar la funcionalidad, mientras que la herencia solo es apropiada para las relaciones "es-a".

Existen otras técnicas como los mixins que resuelven los mismos problemas y no tienen los problemas que tiene la herencia múltiple.

Mendelt
fuente
44
El compilado no necesita hacer una elección arbitraria, simplemente puede generar un error. En C #, ¿cuál es el tipo de ([..bool..]? "test": 1)?
Eamon Nerbonne
44
En C ++, el compilador nunca toma decisiones tan arbitrarias: es un error definir una clase donde el compilador necesitaría hacer una elección arbitraria.
curioso
5

No creo que el problema del diamante sea un problema, consideraría ese sofisma, nada más.

El peor problema, desde mi punto de vista, con herencia múltiple es RAD - víctimas y personas que dicen ser desarrolladores pero en realidad están atrapados con medio conocimiento (en el mejor de los casos).

Personalmente, estaría muy feliz si finalmente pudiera hacer algo en Windows Forms como este (no es el código correcto, pero debería darle la idea):

public sealed class CustomerEditView : Form, MVCView<Customer>

Este es el principal problema que tengo al no tener herencia múltiple. PUEDES hacer algo similar con las interfaces, pero hay lo que yo llamo "código ***", es este c *** repetitivo doloroso que tienes que escribir en cada una de tus clases para obtener un contexto de datos, por ejemplo.

En mi opinión, no debería haber absolutamente ninguna necesidad, ni la más mínima, de CUALQUIER repetición de código en un lenguaje moderno.

Turing completo
fuente
Tiendo a estar de acuerdo, pero solo tiendo: existe la necesidad de cierta redundancia en cualquier idioma para detectar errores. De todos modos, debes unirte al equipo de desarrolladores de Felix porque ese es un objetivo central. Por ejemplo, todas las declaraciones son recursivas mutuamente, y puede ver hacia adelante y hacia atrás, por lo que no necesita declaraciones hacia adelante (el alcance se establece en forma, como las etiquetas C goto).
Yttrill
Estoy completamente de acuerdo con esto: acabo de encontrarme con un problema similar aquí . La gente habla sobre el problema del diamante, lo cita religiosamente, pero en mi opinión es muy fácil evitarlo. (No todos necesitamos escribir nuestros programas como ellos escribieron la biblioteca iostream). La herencia múltiple debe usarse lógicamente cuando tiene un objeto que necesita la funcionalidad de dos clases base diferentes que no tienen funciones superpuestas o nombres de funciones. En las manos correctas, es una herramienta.
jedd.ahyoung
3
@Turing Complete: wrt no tiene ninguna repetición de código: esta es una buena idea pero es incorrecta e imposible. Hay una gran cantidad de patrones de uso y deseamos abstraer los comunes en la biblioteca, pero es una locura abstraerlos a todos porque, incluso si pudiéramos, la carga semántica al recordar todos los nombres es demasiado alta. Lo que quieres es un buen equilibrio. No olvides que la repetición es lo que da estructura a las cosas (el patrón implica redundancia).
2011
@ lunchmeat317: El hecho de que el código generalmente no deba escribirse de tal manera que el 'diamante' plantee un problema, no significa que un diseñador de lenguaje / marco pueda simplemente ignorar el problema. Si un marco de trabajo proporciona que la transmisión ascendente y descendente preservan la identidad del objeto, desea permitir que las versiones posteriores de una clase aumenten el número de tipos para los que puede ser sustituido sin que eso sea un cambio radical, y desea permitir la creación de tipos en tiempo de ejecución, No creo que pueda permitir la herencia de múltiples clases (a diferencia de la herencia de la interfaz) mientras se cumplen los objetivos anteriores.
supercat
3

El Common Lisp Object System (CLOS) es otro ejemplo de algo que soporta MI mientras evita los problemas de estilo C ++: la herencia tiene un valor predeterminado sensato , mientras que aún le da la libertad de decidir explícitamente cómo, exactamente, llamar al comportamiento de un super .

Frank Shearar
fuente
Sí, CLOS es uno de los sistemas de objetos más superiores desde el inicio de la informática moderna, quizás incluso hace mucho tiempo :)
rostamn739
2

No hay nada malo en la herencia múltiple en sí. El problema es agregar herencia múltiple a un lenguaje que no fue diseñado teniendo en cuenta la herencia múltiple desde el principio.

El lenguaje Eiffel admite herencia múltiple sin restricciones de una manera muy eficiente y productiva, pero el lenguaje fue diseñado desde ese principio para admitirlo.

Esta característica es compleja de implementar para los desarrolladores de compiladores, pero parece que ese inconveniente podría compensarse por el hecho de que un buen soporte de herencia múltiple podría evitar el soporte de otras características (es decir, sin necesidad de Interfaz o Método de Extensión).

Creo que apoyar o no la herencia múltiple es más una cuestión de elección, una cuestión de prioridades. Una característica más compleja requiere más tiempo para implementarse correctamente y ser operativa, y puede ser más controvertida. La implementación de C ++ puede ser la razón por la cual no se implementó la herencia múltiple en C # y Java ...

Christian Lemer
fuente
1
¿El soporte de C ++ para MI no es " muy eficiente y productivo "?
curioso el
1
En realidad, está algo roto en el sentido de que no encaja con otras características de C ++. La asignación no funciona correctamente con la herencia, y mucho menos la herencia múltiple (consulte las reglas realmente malas). Crear diamantes correctamente es tan difícil que el comité de Normas arruinó la jerarquía de excepciones para mantenerlo simple y eficiente, en lugar de hacerlo correctamente. En un compilador más antiguo que estaba usando en el momento en que probé esto y algunas mezclas de MI e implementaciones de excepciones básicas cuestan más de un Megabyte de código y tardó 10 minutos en compilar ... solo las definiciones.
Yttrill
1
Los diamantes son un buen ejemplo. En Eiffel, el diamante se resuelve explícitamente. Por ejemplo, imagine que el alumno y el maestro heredan de la persona. La persona tiene un calendario, por lo que tanto el alumno como el maestro heredarán este calendario. Si construye un diamante creando un TeachingStudent que hereda tanto del Profesor como del Estudiante, puede decidir cambiar el nombre de uno de los calendarios heredados para mantener ambos calendarios disponibles por separado o decidir fusionarlos para que se comporte más como Persona. La herencia múltiple se puede implementar muy bien, pero requiere un diseño cuidadoso y preferiblemente desde el principio ...
Christian Lemer
1
Los compiladores de Eiffel tienen que hacer un análisis de programa global para implementar este modelo de MI de manera eficiente. Para las llamadas a métodos polimórficos, usan thunks de despachador o matrices dispersas como se explica aquí . Esto no combina bien con la compilación separada de C ++ y la función de carga de clases de C # y Java.
cyco130
2

Uno de los objetivos de diseño de frameworks como Java y .NET es hacer posible que el código compilado funcione con una versión de una biblioteca precompilada, para que funcione igual de bien con versiones posteriores de esa biblioteca, incluso si esas versiones posteriores Añadir nuevas funciones. Si bien el paradigma normal en lenguajes como C o C ++ es distribuir ejecutables vinculados estáticamente que contienen todas las bibliotecas que necesitan, el paradigma en .NET y Java es distribuir aplicaciones como colecciones de componentes que están "vinculados" en tiempo de ejecución .

El modelo COM que precedió a .NET intentó usar este enfoque general, pero en realidad no tenía herencia; en cambio, cada definición de clase definía efectivamente una clase y una interfaz del mismo nombre que contenía a todos sus miembros públicos. Las instancias eran del tipo de clase, mientras que las referencias eran del tipo de interfaz. Declarar que una clase deriva de otra era equivalente a declarar que una clase implementaba la interfaz de la otra, y requería que la nueva clase volviera a implementar todos los miembros públicos de las clases de las que se derivaba. Si Y y Z derivan de X, y luego W deriva de Y y Z, no importará si Y y Z implementan los miembros de X de manera diferente, porque Z no podrá usar sus implementaciones, tendrá que definir su propio. W podría encapsular instancias de Y y / o Z,

La dificultad en Java y .NET es que el código puede heredar miembros y tener acceso a ellos se refiere implícitamente a los miembros principales. Supongamos que uno tuviera clases relacionadas con WZ como arriba:

class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z  // Not actually permitted in C#
{
  public static void Test()
  {
    var it = new W();
    it.Foo();
  }
}

Parece W.Test()que crear una instancia de W llama a la implementación del método virtual Foodefinido en X. Supongamos, sin embargo, que Y y Z en realidad estaban en un módulo compilado por separado, y aunque se definieron como anteriormente cuando se compilaron X y W, luego se cambiaron y se volvieron a compilar:

class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }

Ahora, ¿cuál debería ser el efecto de llamar W.Test()? Si el programa tuviera que estar enlazado estáticamente antes de la distribución, la etapa de enlace estático podría ser capaz de discernir que si bien el programa no tenía ambigüedad antes de que se cambiaran Y y Z, los cambios en Y y Z han hecho las cosas ambiguas y el enlazador podría negarse a construir el programa a menos o hasta que se resuelva dicha ambigüedad. Por otro lado, es posible que la persona que tiene W y las nuevas versiones de Y y Z sea alguien que simplemente quiere ejecutar el programa y no tiene código fuente para ninguno de ellos. Cuando se W.Test()ejecuta, ya no estaría claro quéW.Test() debería hacerlo, pero hasta que el usuario intente ejecutar W con la nueva versión de Y y Z, no habría forma de que ninguna parte del sistema pudiera reconocer que había un problema (a menos que W fuera considerado ilegítimo incluso antes de los cambios en Y y Z) .

Super gato
fuente
2

El diamante no es un problema, siempre y cuando no use nada como la herencia virtual de C ++: en la herencia normal, cada clase base se asemeja a un campo miembro (en realidad, se presentan en la RAM de esta manera), lo que le da un poco de azúcar sintáctica y un capacidad adicional para anular más métodos virtuales. Eso puede imponer cierta ambigüedad en tiempo de compilación, pero eso suele ser fácil de resolver.

Por otro lado, con la herencia virtual, se sale de control con demasiada facilidad (y luego se convierte en un desastre). Considere como ejemplo un diagrama de "corazón":

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

En C ++ es totalmente imposible: tan pronto como sea Fy Gse fusionan en una sola clase, sus As se fusionan demasiado, y punto. Eso significa que nunca puede considerar que las clases base sean opacas en C ++ (en este ejemplo, debe construir, Apor Hlo que debe saber que está presente en algún lugar de la jerarquía). Sin embargo, en otros idiomas puede funcionar; por ejemplo, Fy Gpodría declarar explícitamente A como "interno", lo que prohíbe la fusión consiguiente y se hace efectivamente sólido.

Otro ejemplo interesante ( no específico de C ++):

  A
 / \
B   B
|   |
C   D
 \ /
  E

Aquí, solo Busa herencia virtual. Entonces Econtiene dos Bs que comparten lo mismo A. De esta manera, puede obtener un A*puntero que apunta E, pero no puede lanzarlo a un B*puntero, aunque el objeto es realmente B como tal el lanzamiento es ambiguo, y esta ambigüedad no se puede detectar en el momento de la compilación (a menos que el compilador vea el programa completo). Aquí está el código de prueba:

struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};

int main() {
        E data;
        E *e = &data;
        A *a = dynamic_cast<A *>(e); // works, A is unambiguous
//      B *b = dynamic_cast<B *>(e); // doesn't compile
        B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
        std::cout << "E: " << e << std::endl;
        std::cout << "A: " << a << std::endl;
        std::cout << "B: " << b << std::endl;
// the next casts work
        std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
        std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
        std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
        std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
        return 0;
}

Además, la implementación puede ser muy compleja (depende del lenguaje; ver la respuesta de benjismith).

Número cero
fuente
Ese es el verdadero problema con MI. Los programadores pueden necesitar diferentes resoluciones dentro de una clase. Una solución en todo el lenguaje limitaría lo que es posible y obligaría a los programadores a crear kludges para que el programa funcione correctamente.
shawnhcorey