rumigaculum.com

  • Aumentar tamaño del tipo
  • Tamaño del tipo predeterminado
  • Disminuir tamaño del tipo
Inicio Computación y cálculo numérico Clases contenedoras y seguridad ante excepciones en C++

Clases contenedoras y seguridad ante excepciones en C++

E-mail Imprimir PDF

Introducción

Al desarrollar una biblioteca de clases o funciones que sabemos van a utilizar otros somos conscientes de la importancia de documentar su uso hasta donde mejor sepamos. Debemos ser especialmente cuidadosos en el diseño y la codificación, anticiparnos a posibles errores con que pueda encontrarse el usuario y construir una interfaz con el cliente lo más sencilla e intuitiva posible. Si lo que codificamos es una clase contenedora el ciudado debe extremarse aún más. Ahora no se trata sólo de una clase que pueden usar otros, sino que dicha clase a su vez usa las clases de terceras partes (objetos contenidos), cuya arquitectura puede ser variadísima y para nosotros siempre desconocida.

Es normal que en todos los libros de C++ o de cualquier otro lenguaje de programación se nos introduzca en los vericuetos del lenguaje mediante el diseño de una sencilla clase contenedora (una pila o stack suele ser el ejemplo más socorrido). Los buenos autores nos presentan los miembros y métodos característicos de dichas clases, y éstos se van desarrollando y cobrando sentido ante nuestros ojos. Finalmente, y cuando tras una larga lectura parece que todo está perfectamente acabado, se nos invita amablemente a tirar el trabajo a la papelera y a usar un contenedor estándar (de la Standard Template Library) para cualquier codificación de carácter profesional. El neófito entonces, muchas veces disgustado, suele atribuir dicha actitud a cierto celo profesional de los expertos del sector, o tal vez a la excesiva importancia que se da al mantra de no reinventar la rueda. Pero esto no es así. Conforme uno va adquiriendo más conocimientos y habilidades dentro de la programación en C++ se va dando cuenta de lo verdaderamente intrincado que es este lenguaje de programación y de los innumerables escollos que debemos salvar para construir un código como mínimo aceptable. No se trata de evitar la reinvención de la rueda, sino de hacernos ver que nuestras primeras ruedas de seguro serán cuadradas.

Seguridad ante excepciones

Son varios los aspectos que debemos analizar cuando construimos una clase contenedora. Uno, bastante obvio, es el rendimiento. Otro es el manejo de los recursos, que muchas veces está relacionado con el primero. Así, buscamos operaciones que, en la medida de lo posible, no supongan la creación o copia de objetos, la asignación de nueva memoria, etc. Buscamos pasar los objetos por referencia entre funciones, siempre que sea posible, devolver igualmente referencias, etc. Pero quizá el aspecto más sutil y desconocido sea responder a la pregunta de cómo debe comportarse una clase contenedora cuando un objeto contenido lanza una excepción.

Podríamos contruir por contrato un contenedor que sólo fuese apto para clases contenidas que nunca lanzan excepciones (nothrow). En este caso estamos mermando el alcance de nuestra clase contenedora, pues es muy difícil, impráctico y en muchas ocasiones imposible construir una clase T que jamás lance excepciones. Tendríamos no obstante el problema de que si bien T no lanza excepciones, el propio contenedor sí pudiera lanzarlas al asignar la memoria necesaria para albergar los objetos de tipo T. La política más razonable es entonces construir un contenedor que sea capaz de albergar cualquier tipo T, lance éste excepciones o no, y lidiar con las mismas de la mejor manera posible.

Una manera de tratar con dichas excepciones es exigir que la clase contenedora cumpla la garantía débil ante excepciones. Esto significa que, si durante cualquier operación del contenedor (al llamar a uno de sus métodos) la clase contenida T lanza una excepción, queda garantizado que:

  1. No existirán pérdidas o fugas de memoria.
  2. El contenedor podrá haberse visto modificado, pero éste debe quedar en un estado estable. Lo anterior significa que no quedarán internamente punteros apuntando a regiones de memoria desasignadas, que el número de elementos será coherente con el tamaño y los posibles índices del contenedor, etc. Dicho de otro modo: el contenedor debe ser capaz de recorrerse completamente y de destruirse sin dejar rastro.

Obsérvese que, tras el tratamiento de la excepción, el contenedor se halla en un estado estable, pero, sin recorrerlo, es posible que no sepamos qué contiene exactamente. Por otra parte, dicho contenido a posteriori puede sernos útil o no.

La garantía débil ante excepciones es el listón mínimo exigido a todas las operaciones de la Standard Template Library (STL) de C++. No obstante, a la mayoría de las operaciones se les exige aún más, salvo que dicha exigencia extra comprometa seriamente el rendimiento de tal operación. Este nivel mayor de exigencia se denomina garantía fuerte ante excepciones. Un método que cumpla con esta garantía fuerte promete que, de ocurrir una excepción durante la operación, el contenedor regresa al mismo estado que tenía antes de invocarse dicho método. Se exige naturalmente que no acontezcan fugas de memoria. Este es el requisito al que deben aspirar los métodos de un contenedor. El hecho de que el contenedor no varíe hace que sea perfectamente usable después del tratamiento de la excepción, y con total conocimiento de su contenido por parte del usuario. Obsérvese entonces que un método que cumpla la garantía fuerte ante excepciones hace al contenedor transparente ante la excepción: ésta simplemente es relanzada fuera del contenedor y no afecta al estado del mismo.

Para diseñar un método que cumpla la garantía fuerte ante excepciones lo que se hace es ejecutar todas aquellas operaciones que sean susceptibles de excepción (creación, copia y asignación de objetos) sobre objetos ajenos a nuestro contenedor y luego incorporar dichos objetos a través de operaciones que nunca lanzan excepciones, como pueden ser: intercambio (swap), escisión (splice o split) y concatenación (link o concatenate). No es objeto de este artículo describir concienzudamente este proceso y tampoco somos expertos en la materia. Recomendamos al lector el siguiente libro, redactado por uno de los grandes gurús del lenguaje de programación C++:

Exceptional C++. 47 Engineering Puzzles, Programming Problems, and Solutions. Herb Sutter.

Clases dsm::Array y dsm::Matrix

No existe en la STL ningún contenedor basado en una lista doblemente enlazada que pueda manejarse como un array, esto es, con índices. Tampoco existe ninguna implementación de una clase para la representación de matrices convencionales. Cierto es que existen otras librerías que contienen clases análogas a las aquí presentadas (boost), y seguramente mejores, pero de haberlas escogido para mis proyectos no hubiera aumentado mis conocimientos de C++, que es a la postre lo que realmente me interesa. Con estos contenedores no sé si he conseguido una rueda circular, pero al menos gira con cierta soltura.

Pero para introducir estos contenedores en los desarrollos presentados en este sitio web (y que podríamos calificar de semiprofesionales), ha sido necesario un arduo trabajo de codificación y sobre todo de prueba, con el objeto de garantizar una calidad mínima. Listamos a continuación las características principales de cada contenedor:

dsm::Array<T>

  • Es un array construido sobre una lista doblemente enlazada, con la ventaja de poder ser manejado (además de por iteradores) por índices, al modo de la clase ArrayList de Java.
  • El origen de índices es seleccionable y modificable por el usuario en cualquier momento. Por defecto es cero.
  • La gran mayoría de sus métodos cumple con la garantía fuerte ante excepciones. Los que no, cumplen con la garantía débil.
  • A diferencia de los contenedores de la STL, no permite el uso de asignadores de memoria alternativos. Creemos que la posibilidad de usar asignadores distintos a std::allocator<T> es acertada, pero esta opción se usa con una frecuencia despreciable por la comunidad de programadores de C++. De hecho la clase dsm::Array<T> ni siquiera usa std::allocator<T>; la asignación de memoria y la creación de objetos siempre acontece de forma paralela.
  • La búsqueda de un elemento parte (generalmente) del elemento anteriormente inspeccionado (mediante la inclusión de un puntero al nodo que denominamos foco). Así pues, el recorrido de la lista adelante y atrás es bastante ágil.
  • Es compatible con los algoritmos de la STL.

dsm::Matrix<T>

  • El corazón de esta clase lo constituye el objeto dsm::Array< dsm::Array<T> > que contiene.
  • Los origenes de índices, tanto para filas como para columnas, son seleccionables y modificables por el usuario en cualquier momento. Por defecto valen cero.
  • La gran mayoría de sus métodos cumple con la garantía fuerte ante excepciones. Los que no, cumplen con la garantía débil.
  • A semejanza de la STL, los algoritmos aplicables a matrices (álgebra lineal básica) están disgregados en un fichero distinto (MatrixAlgo.h) y podrían ser explotados por otros contenedores matriciales.

Cuando afirmamos que la gran mayoría de los métodos correspondientes a las clases anteriores cumplen con la garantía fuerte ante excepciones, no es una afirmación gratuita: nace de la profunda inspección de cada uno de los métodos y sobre todo de haberlos sometido a la generación automática y controlada de excepciones. El procedimiento para ello no es original, y está documentado en internet.

Descarga de las clases contenedoras

Los ficheros: Array.h, Matrix.h y MatrixAlgo.h están incluidos en muchos de los proyectos que se ofrecen en rumigaculum.com. No obstante en la página de descargas existe una descarga especifica donde el lector no sólo hallará estas clases contenedoras, sino lo que es igual de importante: los tests de prueba, tanto funcionales como los de determinación del comportamiento de los distintos métodos ante excepciones.

Share