Excepciones, destructores y técnica RAII (VI)


Técnica RAII (primera parte)


Consideremos la función siguiente:

   void array_test(std::size_t n)    {       auto const p = new double[n]{};                   // (A)       // usamos el array, lo que puede dar lugar al lanzamiento de excepciones...              delete[] p;                                       // (B)    }

En ella, se asigna dinámicamente memoria para una matriz unidimensional de escalares tipo double mediante una expresión de tipo new. Una vez completado un conjunto de operaciones sobre la matriz, el bloque de memoria es liberado mediante una expresión delete. Este código no es seguro ante excepciones. En efecto, observemos que de lanzarse una excepción entre la asignación de memoria en (A) y su posterior liberación en (B), el proceso de desenredo de la pila no liberaría la memoria apuntada por el puntero, pues la operación delete[] p no llegaría a ejecutarse. Con el fin de evitar esta posible fuga de memoria, analizaremos dos opciones, siendo la segunda la solución idiomática en C++:

[1] Introducir un bloque de limpieza try-catch: El código intermedio que puede lanzar la excepción se encierra en un bloque try y la excepción se atrapa y se maneja convenientemente en un bloque catch. Aunque válida, esta solución complica innecesariamente el código (incluso en una situación tan sencilla como la considerada) y resulta poco eficiente:

   void array_test(std::size_t n)    {       auto const p = new double[n]{}; // (A)       try {          // usamos el array, lo que puede dar lugar al lanzamiento de excepciones...       }       catch (...) {  // capturamos cualquier posible excepción,          delete[] p; // liberarmos la memoria          throw;      // y relanzamos la excepción       }       delete[] p; // en caso de que no se hayan emitido excepciones    }

Observemos que si new[] no encontrara memoria suficiente para asignar en (A), se emitiría una excepción de tipo std::bad_alloc. Ante tal eventualidad, permitimos sencillamente que la excepción se propague fuera de la función sin necesidad de realizar operaciones adicionales. En efecto, no existiría fuga de recurso, pues el bloque de memoria no habría llegado de adquirirse.

[2] Utilizar la denominada técnica RAII (Resource Acquisition Is Initialization), diseñada por Bjarne Stroustrup, según la cual todo recurso debe quedar representado por un objeto cuyo destructor se encargue de su liberación. En nuestro ejemplo, definiremos una plantilla de clase Dinarray<>, capaz de encapsular matrices unidimensionales de tamaño conocido en tiempo de ejecución. El constructor de la clase adquirirá la memoria dinámica para la matriz mediante una expresión new y su destructor la liberará mediante una expresión delete:

   template<typename T>    class Dynarray {       T* p_;             // inicio del alojamiento       std::size_t size_; // número de elementos a almacenar    public:       // construcción y destrucción:       explicit Dynarray(std::size_t n) : p_{new T[n]}, size_{n} { }       ~Dynarray() { delete[] p_; }              // acceso a elementos (sin bound checking):       auto operator[](std::size_t i) const -> T const& { return p_[i]; }       auto operator[](std::size_t i)       -> T&       { return p_[i]; }       // resto de la interfaz pública (operaciones de copia/movimiento, iteradores, etc)    };

Haciendo uso de esta plantilla, la función array_test() se reduce ahora simplemente a:

   void array_test(std::size_t n)    {       auto d = Dynarray<double>{n}; // construimos un array con n elementos              // usamos el array, lo que puede dar lugar al lanzamiento de excepciones...           } // el array es destruido y su memoria liberada automáticamente en este punto

Comparemos la sencillez y eficacia de este código con el caso anterior. De emitirse una excepción una vez creada la matriz, el destructor de la clase Dynarray<double> será invocado durante el desenredo de la pila, liberándose la memoria dinámica asignada por el constructor. Asimismo, de no producirse emisión de excepciones, la función array_test() terminará de ejecutarse normalmente y el destructor de la clase será llamado de forma automática al salir el objeto d fuera de ámbito, destruyendo la matriz y liberando la memoria como en el caso anterior. En cualquiera de los casos, pues, es el destructor el encargado de invocar, de manera automática y determinista, la operación delete[]. Se trata de una solución natural y eficiente que caracteriza, más que ninguna otra, al lenguaje C++.

Finalmente, aunque a primera vista pudiera parecer que esta solución requiere un mayor esfuerzo de codificación, debemos tener en cuenta que, una vez implementada en un fichero de cabecera, la plantilla Dynarray<> puede ser reutilizada en múltiples proyectos. Por supuesto, se recomienda utilizar las estructuras de datos proporcionadas por el propio estándar del lenguaje. La plantilla std::vector<> disponible en el el fichero de cabecera <vector> sería una elección natural en este contexto.


Puedes acceder al siguiente artículo de la serie a través de este enlace.

No hay comentarios:

Publicar un comentario