16.2. Un vistazo a las plantillas

Ahora surge un nuevo problema. Tenemos un IntStack, que maneja enteros. Pero queremos una pila que maneje formas, o flotas de aviones, o plantas o cualquier otra cosa. Reinventar el código fuente cada vez no parece una aproximación muy inteligente con un lenguaje que propugna la reutilización. Debe haber un camino mejor.

Hay tres técnicas para reutilizar código en esta situación: el modo de C, presentado aquí como contraste; la aproximación de Smalltalk, que afectó de forma significativa a C++, y la aproximación de C++: los templates.

La solución de C. Por supuesto hay que escapar de la aproximación de C porque es desordenada y provoca errores, al mismo tiempo que no es nada elegante. En esta aproximación, se copia el código de una Stack y se hacen modificaciones a mano, introduciendo nuevos errores en el proceso. Esta no es una técnica muy productiva.

La solución de Smalltalk. Smalltalk (y Java siguiendo su ejemplo) optó por una solución simple y directa: Se quiere reutilizar código, pues utilicese la herencia. Para implementarlo, cada clase contenedora maneja elementos de una clase base genérica llamada Object (similar al ejemplo del final del capítulo 15). Pero debido a que la librería de Smalltalk es fundamental, no se puede crear una clase desde la nada. En su lugar, siempre hay que heredar de una clase existente. Se encuentra una clase lo más cercana posible a lo que se desea, se hereda de ella, y se hacen un par de cambios. Obviamente, esto es un beneficio porque minimiza el trabajo (y explica porque se pierde un montón de tiempo aprendiendo la librería antes de ser un programador efectivo en Smalltalk).

Pero también significa que todas las clases de Smalltalk acaban siendo parte de un único árbol de herencia. Hay que heredar de una rama de este árbol cuando se está creando una nueva clase. La mayoría del árbol ya esta allí (es la librería de clases de Smalltalk), y la raiz del árbol es una clase llamada Object - la misma clase que los contenedores de Smalltalk manejan.

Es un truco ingenioso porque significa que cada clase en la jerarquía de herencia de Smalltalk (y Java[80]) se deriva de Object, por lo que cualquier clase puede ser almacenada en cualquier contenedor (incluyendo a los propios contenedores). Este tipo de jerarquía de árbol única basada en un tipo genérico fundamental (a menudo llamado Object, como también es el caso en Java) es conocido como "jerarquía basada en objectos". Se puede haber oido este témino y asumido que es un nuevo concepto fundamental de la POO, como el polimorfismo. Sin embargo, simplemente se refiere a la raíz de la jerarquía como Object (o algún témino similar) y a contenedores que almacenan Objects.

Debido a que la librería de clases de Smalltalk tenía mucha más experiencia e historia detrás de la que tenía C++, y porque los compiladores de C++ originales no tenían librerías de clases contenedoras, parecía una buena idea duplicar la librería de Smalltalk en C++. Esto se hizo como experimento con una de las primeras implementaciónes de C++[81], y como representaba un significativo ahorro de código mucha gente empezo a usarlo. En el proceso de intentar usar las clases contenedoras, descubrieron un problema.

El problema es que en Smalltalk (y en la mayoría de los lenguajes de POO que yo conozco), todas las clases derivan automáticamente de la jerarquía única, pero esto no es cierto en C++. Se puede tener una magnifica jerarquía basada en objetos con sus clases contenedoras, pero entonces se compra un conjunto de clases de figuras, o de aviones de otro vendedor que no usa esa jerarquía. (Esto se debe a que usar una jerarquía supone sobrecarga, rechazada por los programadores de C). ¿Cómo se inserta un árbol de clases independientes en nuestra jerarquía? El problema se parece a lo siguiente:

Contenedores

Figura 16.1. Contenedores


Debido a que C++ suporta múltiples jerarquías independientes, la jerarquía basada en objetos de Smalltalk no funciona tan bien.

La solución parace obvia. Si se pueden tener múltiples jerarquías de herencia, entonces hay que ser capaces de heredar de más de una clase: La herencia múltiple resuelve el problema. Por lo que se puede hacer lo siguiente (un ejemplo similar se dió al final del Capítulo 15).

Herencia múltiple

Figura 16.2. Herencia múltiple


Ahora OShape tiene las características y el comportamiento de Shape, pero como también está derivado de Object puede ser insertado en el contenedor. La herencia extra dada a OCircle, OSquare, etc. es necesaria para que esas clases puedan hacer upcast hacia OShape y puedan mantener el comportamiento correcto. Se puede ver como las cosas se están volviendo confusas rápidamente.

Los vendedores de compiladores inventaron e incluyeron sus propias jerarquías y clases contenedoras, muchas de las cuales han sido reemplazadas desde entonces por versiones de templates. Se puede argumentar que la herencia múltiple es necesaria para resolver problemas de programación general, pero como se verá en el Volumen 2 de este libro es mejor evitar esta complejidad excepto en casos especiales.

16.2.1. La solución de la plantilla

Aunque una jerarquía basada en objetos con herencia múltiple es conceptualmente correcta, se vuelve difícil de usar. En su libro[82], Stroustrup demostró lo que el consideraba una alternativa preferible a la jerarquía basada en objetos. Clases contenedoras que fueran creadas como grandes macros del preprocesador con argumentos que pudieran ser sustituidos con el tipo deseado. Cuando se quiera crear un contenedor que maneje un tipo en concreto, se hacen un par de llamadas a macros.

Desafortunadamente, esta aproximación era confusa para toda la literatura existente de Smalltalk y para la experiencia de programación, y era un poco inmanejable. Básicamente, nadie la entendía.

Mientras tanto, Stroustrup y el equipo de C++ de los Laboratorios Bell habían modificado su aproximación de las macros, simplificándola y moviéndola del dominio del preprocesador al compilador. Este nuevo dispositivo de sustitución de código se conoce como template [83] (plantilla), y representa un modo completamente diferente de reutilizar el código. En vez de reutilizar código objeto, como en la herencia y en la composición, un template reutiliza código fuente. El contenedor no maneja una clase base genérica llamada Object, si no que gestiona un parámetro no especificado. Cuando se usa un template, el parámetro es sustituido por el compilador, parecido a la antigua aproximación de las macros, pero más claro y fácil de usar.

Ahora, en vez de preocuparse por la herencia o la composición cuando se quiera usar una clase contenedora, se usa la versión en plantilla del contenedor y se crea una versión específica para el problema, como lo siguiente:

Contenedor de objetos Figura

Figura 16.3. Contenedor de objetos Figura


El compilador hace el trabajo por nosotros, y se obtiene el contenedor necesario para hacer el trabajo, en vez de una jerarquía de herencia inmanejable. En C++, el template implementa el concepto de tipo parametrizado. Otro beneficio de la aproximación de las plantillas es que el programador novato que no tenga familiaridad o esté incómodo con la herencia puede usar las clases contenedoras de manera adecuada (como se ha estado haciendo a lo largo del libro con el vector).



[80] Con la excepción, en Java, de los tipos de datos primitivos, que se hicieron no Objects por eficiencia.

[81] La librería OOPS, por Keith Gorlen, mientras estaba en el NIH.

[82] The C++ Programming Language by Bjarne Stroustrup (1ª edición, Addison-Wesley, 1986)

[83] La inspiración de los templates parece venir de los generics de ADA