Bien, bien: parece que la STL es fabulosa, pero, tras esta exposición histórica, ¿para qué demonios sirve? Durante mucho tiempo se ha achacado a C++ la carencia de estructuras de datos con funcionalidad suficiente para procurar las bases de una deseada genericidad en la codificación. Y la STL proporciona justamente eso: un conjunto de estructuras de datos y de algoritmos que operan sobre aquéllas. Pero sobre todo procura, según Koenig, “un entorno conceptual que facilita a los usuarios la adición de sus propios algoritmos y estructuras de datos” de una forma ciertamente genérica: los nuevos algoritmos funcionarán con todas las estructuras de datos (contenedores) existentes, a la vez que sobre las nuevas estructuras de datos se podrán aplicar todos los algoritmos existentes. La STL viene, pues, a cubrir un hueco mal tapado hasta ahora por macros y clases no-estandarizadas, propias o de bibliotecas comerciales, usualmente poco efectivas, con un uso inadecuado de la herencia, con escaso o nulo chequeo de tipos, tan sustancial a C++. En los páragrafos siguientes se expondrá, trufado con varios ejemplos (validados con VisualAge C++ para OS/2 y STL<ToolKit>), un acercamiento a la STL que no se pretende exhaustivo (toda vez que la sola descripción completa de las clases y algoritmos involucrados supera con mucho el ámbito de un artículo, ademas de existir un excelente manual de dominio público en que tales se detallan), sino más bien comprehensivo e iniciático. No se detallará, tampoco, la sintaxis y uso de las plantillas en C++. En definitiva mi intención es transmitir al lector el “espíritu” de la STL y la animosidad suficiente para que pueda usarla con efectividad, esperando, entre otras cosas, no vuelva a usar otra clase “vector” que la provista por esta esta biblioteca [1]. A ello. ESTRUCTURA DE LA BIBLIOTECA La STL está construida sobre cinco componentes:
aunque la relación fundamental entre componentes podría establecerse así:
Esto es, el núcleo de la STL son los algoritmos, que utilizan iteradores para acceder a contenedores. Como bien explican Stepanov y Lee, si quisiéramos codificar ‘c’ contenedores para ‘t’ tipos de datos accesibles mediante ‘a’ algoritmos, deberíamos contemplar ‘c*t*a’ combinaciones. Debido a la parametrización de tipos que procuran las plantillas, la STL sólo necesitaría ‘c*a’ combinaciones, que finalmente se reducen a ‘a+c’ por la reutilización de los algoritmos en diferentes contenedores. Adicionalmente los componentes de la STL son al menos tan eficientes como los codificados a-mano (debido al uso del “inlining” y el prácticamente nulo uso de la derivación); proveen un fuerte chequeo de tipos (mediante el uso de plantillas); sus iteradores son generalizaciones de los punteros (pudiendo usarse donde tales aparecen, singularmente en los arrays de C); y permiten el uso, sobre todo en algoritmos, de funciones encapsuladas en objetos (también como el uso de las típicas funciones C/C++). CONTENEDORES En la STL se dan las siguientes categorías de contenedores, que en definitiva son “objetos que contienen otros objetos”:
que a su vez encierran las siguientes colecciones concretas, todas ellas codificadas en C++ mediante plantillas y con las subdisiviones que se indican:
Todas las colecciones de la STL tienen definido un interfaz mínimo, compuesto por las siguientes funciones públicas (se usará el contenedor “ list ” para los prototipos):
STL provee otras funciones de comparación (C) que se derivan (en sentido figurado) de los operadores == y < : <= , >= , > y != . En realidad STL provee tales operadores adicionales para cualquier objeto, supuesta la existencia de los dos primeros. Pero veamos un ejemplo de uso de los contenedores STL:
Los adaptadores son, en esencia, plantillas que procuran un interfaz especial para los otros tipos de contenedores. Así cualquier contenedor que soporte las operaciones de push_back y pop_back (como vector , list y queue ) puede ser usado para instanciar “ stack ”. Si se añade la operación front , entonces se podrá instanciar una “ priority_queue ”. Por fin “ queue ” podrá instanciarse con contenedores que soporten front , back , push_back y pop_front . Como el lector fácilmente habrá adivinado, los adaptadores “adaptan” o “adecúan” un determinado contenedor para que adopte un nuevo comportamiento, restringiendo ciertas características y cambiando otras. “Bien, bien -exclamará el impaciente lector- Todo esto está muy bien, pero yo ya uso los contenedores de la biblioteca de clases X (o de una propia) que me van estupendamente”. ¡Estupendo! Pero el énfasis de la STL no está en los contenedores, sino en los algoritmos (que veremos poco después). De cualquier manera la genericidad que otorgan las plantillas es difícilmente superable por una biblioteca comercial, y la efectividad de los contenedores de la STL es, por otro lado, virtualmente inmejorable. Pero de poco sirve una mera estructura sin las herramientas que permitan accederla, revisar y modificar tanto su propia configuración como los ítems que contenga. Y aquí aparecen los iteradores. ITERADORES Según Andrew Koenig, “un iterador es algo que permite un conjunto de operaciones en una estructura de datos sin decir nada sobre la naturaleza de tal estructura”. Esta definición resulta especialmente relevante dentro del entorno que proporciona la STL pues ésta, centrada en los algoritmos, se encuentra con la dificultad de aplicar éstos directamente en los contenedores o estructuras genéricas de datos: algunos algoritmos funcionan con ciertas estructuras; otros no. Con la introducción de los iteradores se genera un nuevo nivel de indirección que permite, en su calidad de mediador, el perfecto entendimiento entre contenedores y algoritmos. Los iteradores, así, saben poco de algoritmos o de contenedores, de forma que son grandemente independientes de ambos. De esta manera los algoritmos se expresarán en términos de iteradores, de manera que las estructuras de datos accedidas, en otro nivel, por los iteradores, serán independientes de los primeros. De hecho esto es precisamente lo que diferencia a la STL de otras bibliotecas de clases. Pero, ¿son en realidad los iteradores totalmente independientes de las colecciones que acceden? O sea, ¿cualquier iterador funciona en cualquier contenedor? Humm, parece que no, pues si, en el sentido práctico que nos ocupa, un iterador es un objeto que permite recorrer los elementos de una colección, recorriéndola (o “atravesándola”) con distintas estrategias, para cada estrategia quizás haya que procurar un iterador distinto. Ciertamente los iteradores de la STL funcionan, en general, tanto en los contenedores STL, como en los arrays de C o en los “streams” de C++, pero esto no significa que un iterador (un tipo de iterador) concreto funcione a la vez en todos. Nos hacen falta, pues, varios tipos de iteradores. Veámoslos:
En resumen, si ‘i’ y ‘j’ son iteradores y ‘n’ un entero cualquiera, se cumple:
dándose la siguiente relación entre iteradores:
de forma que donde se espera un cierto iterador, puede aplicarse cualquier iterador a su derecha. Y, claro, aquí el lector podría exclamar: “¡Una jerarquía de herencia de iteradores!”. Pues no, irredento lector: nada de jerarquías: aunque parezca lo contrario, la clase del iterador bidirectional no “deriva” de la clase del iterador forward, como no lo hacen tampoco las demás. Andrew Koenig acertadamente afirma que habría que pensar en las relaciones entre iteradores como “herencia conceptual”, pues un iterador es una familia de tipos relacionados en razón de formas que no son directamente expresables en C++. Además de los iteradores expuestos, la STL provee otros igualmente útiles: istream_iterator y ostream_iterator (que operan como cin y cout respecto de streams C++ de entrada y salida, respectivamente), reverse_bidirectional_iterator y reverse_iterator (que iteran hacia atrás), insert_iterator (inserta en la posición a que apunta), back_insert_iterator y front_insert_iterator (sólo insertan al final y al principio de un contenedor, respectivamente), etc. ¡Demasiados iteradores! -podría aquí perfectamente exclamar el lector: ¿Cómo saber qué iterador aplicar en cada contenedor o algoritmo? Bueno, respecto de los algoritmos sólo hay que mirar su signatura o prototipo; respecto de los contenedores, he aquí la tabla:
donde se explicita algo evidente: únicamente las “Colecciones de Primera Clase” (esto es, contenedores secuenciales y asociativos) pueden ser accedidas por iteradores. La ligazón entre contenedores, iteradores y algoritmos se produce así: la descripción de los contenedores incluye la categoría de los tipos de iteradores que proveen, mientras que los algoritmos genéricos incluyen (en su prototipo) la categoría de iteradores con que trabajan. Así, para evitarle al usuario la consulta o aprendizaje de tablas como la anterior, cada contenedor define una serie adecuada de typedef s, de forma que la notación contenedor<class T>::iterator enmascara los tipos:
Pero además se dan los siguiente útiles typedef s:
para iterar adecuadamente en colecciones “escribibles” o de “sólo-lectura”, hacia adelante o hacia atrás. De esta manera, podríamos trabajar con los iteradores de la siguiente guisa:
dejando que los typedef s hagan el trabajo de elección del iterador correcto por nosotros. ¿No es sencillamente fantástico? ALGORITMOS He aquí el corazón de la STL: algoritmos reutilizables. Y es que a pesar de la importancia de los algoritmos que acompañan a la biblioteca, lo verdaderamente significativo se encuentra en el entorno-marco que procura protocolos exactos para la adición de otros muchos algoritmos, tal y como hemos visto al revisar los iteradores. Los algoritmos en la STL hacen uso de iteradores y objetos-funciones (de entre ellos los “predicados”) para acceder a los elementos de los contenedores. De entre los numerosos algoritmos que vienen con la librería, y que el lector puede escudriñar en el completo manual, podemos mencionar:
Pero veamos un ejemplo de un algoritmo eminentemente práctico: “ for_each ”, que con signatura
tiene como propósito aplicar a cada elemento comprendido en el rango [primero_, ultimo_) la función determinada por funcion_ :
¿Qué es lo que aquí sucede? El algoritmo for_each aplica a cada elemento de la lista (a partir del segundo) la función ‘ detecta ’, que toma como argumento el elemento visitado en cada caso, y esta función discrimina a los políticos con ideas, que son insertados en una lista negra para un rápido direccionamiento al ostracismo. Pero en el ejemplo vemos también que una función se trata como un objeto. Y es exactamente eso. ¿FUNCIONES-OBJETO U OBJETOS-FUNCIONES? Muchos de los algoritmos usan de funciones que son aplicadas a los elementos de los contenedores por medio de iteradores. Tales funciones pueden ser bien punteros a funciones normales de C/C++ o una generalización de éstas: funciones encapsuladas en objetos. Los objetos-funciones son instancias de clases con un comportamiento definido por sus funciones miembros públicas, pero cuya sintaxis simula la de las funciones C mediante la sobrecarga del operador (); pero además los objetos-funciones incorporan datos miembros y operan como el resto de los objetos: se crean, se modifican y se destruyen. En general los objetos-funciones, o simplemente funciones respecto de la STL, pueden dividirse en unarios o binarios, dependiendo si toman uno o dos argumentos, pero también, en función de su propósito, en
Pero veamos un ejemplo de cada uno, empezando por los predicados. Si consideramos el algoritmo “ find_if ” con prototipo:
que devuelve un iterador apuntando al primer elemento del contenedor que cumple el predicado (esto es, la función de tipo Predicate aplicada a ese elemento devuelve “ true ”):
Veamos seguidamente cómo operan los comparadores. Si consideramos el algoritmo “ min ” con prototipo:
cuya funcionalidad es la de comparar los elementos a_ y b_ usando la función determinada por comparador_, devolviendo el menor elemento:
Por último examinemos las funciones generales, para lo que trataremos con el algoritmo “count_if” con prototipo:
y que procura el número de ocurrencias del cumplimiento de un determinado predicado respecto de cada uno de los elementos de un contenedor. Como predicado usaremos el objeto-función logical_not, que devuelve “ true ” si el argumento que se le pasa es cero, definido así en la STL:
y que encaja perfectamente en el ejemplo siguiente:
Como vemos los objetos-funciones necesitan ser parametrizados y esta operación puede ser prolija: para facilitar esta labor aparecen los adaptadores de funciones. Así, por ejemplo, los “negadores” not1 y not2 toman como argumento predicados unarios y binarios y devuelven sus complementos. Pero no abundaremos más en adaptadores, cuyo estudio se deja al ávido lector. DE LOS PELIGROS DE LA EXTENSIBILIDAD Examinemos, por ejemplo, el algoritmo “ count() ”, que cuenta el número de ítems en un contenedor que satisfagan un determinado valor. Su prototipo es:
O sea, este algoritmo recorre una coleccion o contenedor desde el primer elemento hasta pasado el último, y compara cada ítem encontrado con “ value ” aplicando el operador ==. Si la comparación devuelve “ true ” entonces se sumará uno a “ n ”. Pero el lector, siempre atento, notará que el algoritmo no inicializa “ n ” a cero, y ni siquiera devuelve el número de ocurrencias, que parece lo más intuitivo, sino “ void ”. Pero tal es natural, pues la reusabilidad de un algoritmo se sustenta en no fijar presunciones que lesionen su genericidad: de esta manera podemos pasar por referencia a “ count() ” una variable con un valor anterior para que sea modificada con las ocurrencias buscadas (caso que se da, por ejemplo, en la concatenación de dos o más búsquedas sobre la misma o distinta colección). Se deja, así, al usuario de la STL que especifique expresamente su intención:
Bien, bien -reconoce aquí el lector-: de acuerdo con la genericidad pero ¿y si yo quiero algo más sencillo? O sea, ¿No resulta peligrosamente tendente al error tanta genericidad? Por otra parte, si ya conocemos el contenedor sobre el que se aplica el algoritmo, ¿por qué repetirlo en los dos primeros argumentos? ¿Y si se me olvida inicializar el contador? ¿No sería más adecuada una versión “simplificada” de tales algoritmos, quizás por derivación de clases? Bueno, el lector ha puesto el dedo en la llaga, como siempre. Pero ¿es la llaga correcta? Hay que pensar que esto mismo le achacaban los pascalistas al C: “Demasiada libertad es peligrosa” (cualquier remembranza política es deliberada). Veamos los peligros de dejar “demasiada responsabilidad” en manos del usuario:
Visto lo anterior parece que para la mayoría de los casos un algoritmo como el siguiente sería mucho más legible y seguro:
a lo que se podría llegar derivando de la clase “ vector ” en la STL una nueva clase que incorporara como función miembro una función “ count() ” que, a su vez, usara del algoritmo “ count() ”. Algo así como:
De esta manera las listas anteriores deberían ser instanciadas como objetos de nuestro vector personalizado:
Y ahora sí se podría utilizar la función miembro, como antes se ha visto. De hecho este es el enfoque que han adoptado la mayoría de implementaciones comerciales de la STL, con el objetivo de facilitar el uso de la biblioteca a los usuarios. Pero este enfoque tiene muchos puntos negros. Veámoslos:
Un enfoque sustancialmente distinto es el que adopta STL<ToolKit>, de ObjectSpace Inc. Para facilitar el uso de los algoritmos de la STL, éstos han añadido unos cuantos llamados “algoritmos de ayuda”, que anteceden los identificadores estándar de la STL con el prefijo “ os_ ”, de forma que así se podría codificar:
donde os_count sería codificada como:
Personalmente me parece una solución que, además de respetar el espíritu de la STL, resulta tan eficiente como la original (por su implementación “inline”), pero más fácil de usar para la mayoría de situaciones, aparte de no incurrir en ninguna de las anteriores desventajas (el mismo algoritmo de ayuda funciona, por ejemplo, para todos los contenedores de la STL). LEVES CRÍTICAS Si la STL es tan endiabladamente buena, ¿qué impide su adopción generalizada? ¡Nada! Bueno, sí, quizás una cierta candidez cultural, semejante a la de aquellos ciudadanos que no comen marisco en público por temor -mayormente fundado- a no saber manejarse con los cubiertos. En realidad la propia sintaxis del lenguaje es mucho más prolija y compleja que la estructuración en que se basa la STL, de forma que cualquiera que use C++ debiera usar STL o, más aún, la Biblioteca Estándar del lenguaje que incluye, en ella diluida y sin forma explícita, a la STL. ¡Ah, pero biblioteca no es bibliotenaje! Esto es, como titularía Pitigrilli, “El pollo no se come con la mano”: hay que estudiar y entender la STL antes de usarla, así como apreciar los leves inconvenientes derivados de su uso, que en la actualidad se reducen a dos: la dificultad de depuración del código erróneo (debida no tanto a la estructura de la biblioteca como a sus actuales implementaciones) y el aumento del tamaño de los ejecutables (que Cargill cifra en un 25%). Hay que afinar la balanza, señores, pero sinceramente no creo que tales dificultades afecten al grueso de los programadores en C++. APRENDIZAJE Y DOCUMENTACIÓN La implementación de STL de Hewlett Packard, que con una decisión encomiable liberó todas las licencias afectas a la biblioteca convirtiendo la STL y el trabajo de muchos años en “freeware”, fue distribuida por RPP en un disquete, pero también puede ser conseguida, junto con el manual de Stepanov y Lee en postscript, mediante ftp anónimo bien en ftp://butler.hpl.hp.com/stl/ bien en ftp://ftp.cs.rpi.edu/pub/stl/ , pero también en distintos foros electrónicos (como el de Microsoft Spain en CI$). En el World-Wide Web habría que echarle un vistazo a la página (home page) que mantiene David R. Musser, del Instituto Politécnico Rensselaer, en http://www.cs.rpi.edu/~musser/stl , con documentación on-line y varios enlaces. En general los enlaces de OOA/D/P pueden ser accedidos a través de “ La Página Orientada-a-Objetos” (The Object-Oriented Page: The OOPage) que yo mismo edito para Galaxy en el Web[2] ( http://galaxy.einet.net/galaxy/Engineering-and-Technology/Computer-Technology/Object-Oriented-Systems/ricardo-devis/oo.html), o también a través de mis propias páginas personales en The Well (http://www.well.com/user/ritchie). En cuanto al aprendizaje y uso, en repetidas ocasiones he mencionado el manual que acompaña a la STL. Naturalmente este manual es perfecto si la perfección estilística se entiende a la manera de Borges: esto es, eliminando adjetivos y vocablos en un proceso iterativo que genera un documento denso y completo, pero que hay que releer varias veces, y donde lo esencial ni siquiera se separa de lo accesorio, en aras de la comletitud formal, claro. Bien: el manual de Stepanov y Lee es perfecto (perfectamente técnico) y un cierto ejemplo sobre cómo debe componerse una buena documentación. Sin embargo, para iniciarse en la STL (o para el gran público, que dicen los legalistas de C++) quizás sea mejor atender a otros textos (y es que Voltaire acuñó una frase omnipresente en informática). Pero, ¿qué textos? Pues los de las implementaciones comerciales de la STL, que son bastante buenos. ¡Un momento, un momento! ¿Implementaciones comerciales? ¡Pues claro! La STL hace un uso extensivo e intensivo de las plantillas y, por ende, carece de un soporte pre-construido que cubra la mayoría de los compiladores del mercado, características que proporcionan los vendedores de STL, que además incorporan diversas golosinas, ciertamente prácticas, que aumentan la facilidad y funcionalidad de la STL. Las implementaciones comerciales actuales arrostran un precio usualmente no superior a los $200,00 y básicamente (existen algunas más) se deben a Modena Software Inc. (Stl++), con soporte para tablas hash, y ObjectSpace Inc. (Stl<ToolKit>), con soporte para la mayoría de los compiladores C++ actuales, aunque Rogue Wave ha anunciado también la inclusión de la STL en su conocida biblioteca de clases Tools.h++. Restringiéndonos a los dos primeras, ambas con buenos manuales y varios añadidos, yo recomendaría la segunda: STL<ToolKit>, que provee un manual de 421 páginas perfectamente pedagógico, con más de 250 ejemplos de uso y una distribución de la información sencillamente perfecta (tanto es así que parece que en breve se publicará el manual como libro en Prentice Hall). Además ObjectSpace, accesible en el Web por http://www.objectspace.com provee extensiones “multi-thread”, multitud de “algoritmos de ayuda” y soporte para asignadores dinámicos de memoria. REFERENCIAS DIRECTAS
[1] Mucha gente piensa, como Prémontal, que las bibliotecas son como las farmacias: muchos venenos y pocos remedios. Así que prefieren las soluciones caseras a-mano. El mercado mismo evidencia su error. [2] ¡Ah, lo siento! Para mí es “el Web” y no “ la Web”. Y es que pese a la traducción de “Telaraña a lo ancho del mundo” (que a mí me resuena a un cierto Capitán Tan), los barbarismos “WWW”, “World-Wide Web”, “Web” ó “W3” me resultan terminantemente masculinos. Claro que una solución “políticamente correcta” sería la de “el/ la Web”, tan común hoy en día en los textos norteamericanos. Pero, vaya, tras leer la versión “políticamente correcta” de “Los Tres Cerditos” creo que me batiría en duelo por reivindicar el sano derecho a la incorrectitud individual. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Pº. Castellana 188, 14º e · 28046 - Madrid · info@a4devis.com |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||