Excepciones, destructores y técnica RAII (V)


Garantías ante excepciones


Al implementar nuestros propios algoritmos y estructuras de datos en C++, debemos analizar cuidadosamente el tipo de garantía que ofrecemos a los usuarios ante una eventual emisión de excepciones por parte de una operación dada. De acuerdo con la clasificación [1] realizada por David Abrahams, uno de los desarrolladores principales de la familia de bibliotecas Boost [2], pueden distinguirse tres tipos fundamentales de garantías, a modo de acuerdo contractual entre las componentes y el usuario:
  1. Garantía básica: se respetan los invariantes básicos de todos los objetos y se evita la fuga de recursos (memoria, ficheros, etcétera). Ello permite el uso de los objetos (particularmente su destrucción) con posterioridad a la emisión de la excepción.
  2. Garantía fuerte: la operación concluye satisfactoriamente o emite una excepción, en cuyo caso el estado del programa retorna al inmediatamente anterior a la ejecución de la operación.
  3. Garantía nothrow: la operación no emite excepciones bajo ninguna circunstancia.
Los algoritmos de la biblioteca estándar del lenguaje proporcionan, al menos, la garantía básica ante excepciones. Ciertos métodos como std::vector<>::push_back(), std::uninitialized_copy(), std::uninitialized_fill() o std::uninitialized_fill_n() proporcionan la garantía fuerte, mientras que operaciones como std::swap() no emiten excepciones bajo ninguna circunstancia.

Con el fin de ilustrar la clasificación anterior, centremos nuestra atención en el algoritmo std::uninitialized_copy(), disponible en el fichero de cabecera estándar <memory>:

   namespace std {       template<typename In, typename Forw>       auto uninitialized_copy(In first, In last, Forw res) -> Forw;    }

Aquí, In y Forw denotan iteradores de tipo "input" y "forward", respectivamente. Este algoritmo construye copias de los elementos almacenados en el rango [first,last) (incluyendo el elemento apuntado por first, pero no el apuntado por last) en un área de memoria preasignada (pero aún sin inicializar) referenciada por el iterador res. El algoritmo retorna un iterador "forward" al elemento inmediatamente posterior al último elemento copiado. Una primera e incompleta implementación del algoritmo tomaría la forma:

   template<typename In, typename Forw>    auto uninitialized_copy(In first, In last, Forw res) -> Forw    {       using Value_type = typename std::iterator_traits<Forw>::value_type;  // (A)       for (; first != last; ++first, ++res)          ::new(static_cast<void*>(&*res)) Value_type(*first);              // (B)       return res;    }

En (A) definimos un alias (Value_type) para el tipo de elemento referenciado por los iteradores de tipo In. Se construye entonces una copia de cada elemento contenido en el rango de origen [first,last) en el área de memoria referenciada por res (B).

La sintaxis empleada ::new(static_cast<void*>(&*res))Value_type(*first) (denominada placement new [3]) implica la construcción de una copia del elemento *first de tipo Value_type en el espacio de memoria preasignado referenciado por el iterador res.

Observemos que los iteradores Forw no son implementados necesariamente como punteros --pueden tratarse de tipos abstractos de datos que imiten el comportamiento de los punteros ordinarios mediante la sobrecarga de operadores--, por lo que, con carácter general, deberemos desreferenciar el iterador res (operación *res) y aplicar después el operador & (operación &*res) con el fin de obtener la dirección en memoria donde construir la copia. Asimismo, el uso del operador global ::new y la conversión explícita a un puntero void* evitan la llamada a cualquier redefinición del operador new que haya podido introducirse en la clase Value_type o en el espacio de nombres global y que acepte punteros Value_type* como segundo argumento. Para más detalles, consúltese [4].

Deseamos que el algoritmo proporcione una garantía fuerte ante excepciones, lo que significa que, de emitirse una excepción durante la construcción de un elemento copia, deberemos capturarla, destruir todos las copias construidas satisfactoriamente antes del lanzamiento de la excepción y, finalizado dicho proceso, relanzarla. Para ello, basta introducir un sencillo bloque try-catch como en el código expuesto a continuación:

   template<typename In, typename Forw>    auto uninitialized_copy(In first, In last, Forw res) -> Forw // garantía fuerte    {       using Value_type = typename std::iterator_traits<Forw>::value_type;       auto p = res;       try {          for (; first != last; ++first, ++p)             ::new(static_cast<void*>(&*p)) Value_type(*first);          return p;       }       catch (...) {          for (; res != p; ++res)             (&*res)->~Value_type();          throw; // relanzamos la excepción actual       }    }


Referencias bibliográficas:
  1. Exception safety - https://www.boost.org/community/exception_safety.html
  2. Boost Libraries - https://www.boost.org/
  3. New expression - https://en.cppreference.com/w/cpp/language/new
  4. Stroustrup B., 'The C++ Programming Language'. Addison-Wesley, 4th Edition (2013). Page 377.

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

No hay comentarios:

Publicar un comentario