8: Herencia múltiple

Tabla de contenidos

8.1. Perspectiva
8.2. Herencia de interfaces
8.3. Herencia de implementación
8.4. Subobjetos duplicados
8.5. Clases base virtuales
8.6. Cuestión sobre búsqueda de nombres
8.7. Evitar la MI
8.8. Extender una interface
8.9. Resumen
8.10. Ejercicios

El concepto básico de la herencia múltiple (HM) suena bastante simple: puede crear un nuevo tipo heredando de más una una clase base. La sintaxis es exactamente la que espera, y en la medida en que los diagramas de herencia sean simples, la HM puede ser simple también.

Sin embargo, la HM puede presentar un buen número de situaciones ambiguas y extrañas, que se cubren en este capítulo. Pero primero, es útil tener algo de perspectiva sobre el asunto.

8.1. Perspectiva

Antes de C++, el lenguaje orientado a objetos más popular era Smaltalk. Smaltalk fue creado desde cero como un lenguaje orientado a objetos. A menudo se dice que es puro, mientras que a C++ se le llama lenguaje híbrido porque soporta múltiples paradigmas de programación, no sólo el paradigma orientado a objeto. Uno de las decisiones de diseño de Smalltalk fue que todas las clases tendrían solo herencia simple, empezando en una clase base (llamada Object - ese es el modelo para la jerarquía basada en objetos) [18] En Smalltalk no puede crear una nueva clase sin derivar de un clase existente, que es la razón por la que lleva cierto tiempo ser productivo con Smalltalk: debe aprender la librería de clases antes de empezar a hacer clases nuevas. La jerarquía de clases de Smalltalk es por tanto un único árbol monolítico.

Las clases de Smalltalk normalmente tienen ciertas cosas en común, y siempre tienen algunas cosas en común (las características y el comportamiento de Object), de modo que no suelen aparecer situaciones en las que se necesite heredad de más de una clase base. Sin embargo, con C++ puede crear tantos árboles de herencia distintos como quiera. Por completitud lógica el lenguaje debe ser capaz de combinar más de una clase a la vez - por eso la necesidad de herencia múltiple.

No fue obvio, sin embargo, que los programadores requiriesen herencia múltiple, y había (y sigue habiendo) mucha discrepancia sobre si es algo esencial en C++. La HM fue añadida en cfront release 2.0 de AT&T en 1989 y fue el primer cambio significativo en el lenguaje desde la versión 1.0. [19] Desde entonces, se han añadido muchas características al Estándar C++ (las plantillas son dignas de mención) que cambian la manera de pensar al programar y le dan a la HM un papel mucho menos importante. Puede pensar en la HM como una prestación menor del lenguaje que raramente está involucrada en las decisiones de diseño diarias.

Uno de los argumentos más convincentes para la HM involucra a los contenedores. Suponga que quiere crear un contenedor que todo el mundo pueda usar fácilmente. Una propuesta es usar void* como tipo para el contenido. La propuesta de Smalltalk, sin embargo, es hacer un contenedor que aloja Object, dado que Object es el tipo base de la jerarquía de Smalltalk. Como todo en Smalltalk está derivado de Object, un contenedor que aloja Objects puede contener cualquier cosa.

Ahora considere la situación en C++. Suponga que el fabricante A crea una jerarquía basada-en-objetos que incluye un conjunto de contenedores incluye uno que desea usar llamado Holder. Después, se da cuenta de que la jerarquía de clases del fabricante B contiene alguna clase que también es importante para usted, una clase BitImage, por ejemplo, que contiene imágenes. La única forma de hacer que un Holder de BitImage es derivar de una nueva clase que derive también de Object, y así poder almacenarlos en el Holder, y BitImage:

Figura 8.1.


Éste fue un motivo importante para la HM, y muchas librerías de clases están hechas con este model. Sin embargo, tal como se vio en el Capítulo 5, la aportación de las plantillas ha cambiado la forma de crear contenedores, y por eso esta situación ya no es un asunto crucial en favor de la HM.

El otro motivo por el que se necesita la HM está relacionado con el diseño. Puede usar la HM intencionadamente para hacer un diseño más flexible y útil (o al menos aparentarlo). Un ejemplo de esto es el diseño de la librería original iostream (que persiste hoy día, como vio en el Capítulo 4.

Figura 8.2.


Tanto iostream como ostream son clases útiles por si mismas, pero se pueden derivar simultáneamente por una clase que combina sus características y comportamientos. La clase ios proporciona una combinación de las dos clases, y por eso en este caso la HM es un mecanismo de FIXME:code-factoring.

Sin importar lo que le motive a usar HM, debe saber que es más difícil de usar de lo que podría parecer.



[18] Esto también ocurre en Java, y en otros lenguajes orientados a objetos.

[19] Son números de versión internos de AT&T.