Excepciones, destructores y técnica RAII (VII)


Técnica RAII (segunda parte)

La técnica RAII no se circunscribe únicamente a la asignación dinámica de memoria, sino que afecta a cualquier tipo de recurso, sea éste un fichero, un mutex (programación concurrente), un socket (networking), etcétera.

Este tratamiento homogéneo de recursos en C++ contrasta con el de otros lenguajes de programación orientados a objetos, en los que debe distinguirse claramente entre los mecanismos de recolección de basura (ligados a la gestión de la memoria dinámica) y los bloques try-catch-finally (a utilizar con otros tipos de recursos). Cabe decir que, frente al esquema tradicional de adquisición y liberación de recursos en bloques try-catch-finally, Java 7 y superiores ofrecen la construcción alternativa try-with-resources [1]. Análogamente, C# 8.0 y superiores proporcionan los denominados using statements [2]. Por su parte, Python 2.5 y superiores poseen los with statements [3]. En todos los casos, sin embargo, se trata de soluciones parciales que no poseen el alcance y generalidad de la técnica RAII.

Como ejemplo clásico de empleo de la técnica RAII en recursos distintos a la memoria, consideremos el siguiente código inseguro ante excepciones, que no garantiza el cierre del fichero al término de la función:

   void file_test(char const* name, char const* mode)    {       std::FILE* const file = std::fopen(name, mode);       if (!file)          throw std::runtime_error{"Unable to open the file"};       // usamos el fichero (este código pudiese emitir excepciones)...       std::fclose(file); // cerramos el fichero    }

En efecto, de lanzarse una excepción en algún punto de ejecución intermedio entre la apertura del fichero y su cierre al témino de la función, la sentencia std::fclose(file) nunca llegaría a ejecutarse, produciéndose la fuga del recurso. Nuevamente, podemos hacer frente a tal eventualidad mediante la introducción de un bloque de limpieza try-catch de la forma:

   void file_test(char const* name, char const* mode)    {       std::FILE* const file = std::fopen(name,mode);       if (!file)          throw std::runtime_error{"Unable to open the file"};       try {          // usamos el fichero (posible emisión de excepciones)...       }       catch(...) {  // capturamos cualquier excepción,          std::fclose(file); // cerramos el fichero          throw;             // y relanzamos la excepción       }       std::fclose(file); // en caso de que no se hayan emitido excepciones    }

Haciendo uso de la técnica RAII, sin embargo, podemos optar por definir una clase File_handle cuyo constructor abra el archivo y cuyo destructor se encargue de cerrarlo y permitir que la vida del recurso quede ligada a la duración de almacenamiento de un objeto local de esta clase  [4]:

   class File_handle {       std::FILE* file_;    public:       // constructor y destructor:       File_handle(char const* name, char const* mode)          : file_{std::fopen(name,mode)}       {          if (!file_)             throw std::runtime_error{"Unable to open the file"};       }       ~File_handle()       {          if (file_) // protección ante posible fallo de segmentación             std::fclose(file_);       }       // resto de la interfaz pública (operaciones input/output, etc)    };

Pudiendo prescindir del bloque try-catch, la función file_test puede ahora reescribirse de forma más limpia y eficiente como:

   void file_test(char const* name, char const* mode)    {       auto file = File_handle {name, mode};       // usamos el fichero (el código puede emitir excepciones)...    } // File_handle::~File_handle() cierra el fichero automáticamente

Observemos cómo el destructor de la clase File_handle se encarga de cerrar el archivo tanto si se emite una excepción durante la ejecución de la función file_test() como si ésta finaliza normalmente. 

En conclusión, en C++ se emplea la técnica RAII para establecer un orden determinista en la gestión de recursos. En particular, se desaconseja el uso explícito de expresiones new y delete en código, salvo internamente en clases que implementen la técnica RAII. Para ello, la biblioteca estándar del lenguaje C++ incluye, entre otras muchas funcionalidades:
  • Punteros inteligentes (std::unique_ptr<>, std::shared_ptr<> y std::weak_ptr<>).
  • Estructuras dinámicas de datos (std::vector<>, std::queue<>, std::stack<>, std::list<>, std::map<>, std::unordered_map<>, std::set<>, std::unordered_set<>, etc.).
  • std::fstream y similares para la gestión de ficheros.
  • std::string y similares para la representación de cadenas de caracteres.
  • Y un largo etcétera.


Referencias bibliográficas:
  1. Java try-with-resources – https://en.wikipedia.org/wiki/Java_syntax#try-with-resources_statements
  2. C# using statements – https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement
  3. Python - The 'with' statement - https://www.python.org/dev/peps/pep-0343/
  4. http://www.stroustrup.com/bs_faq2.html#exceptions

No hay comentarios:

Publicar un comentario