Excepciones, destructores y técnica RAII (I)

Última actualización: 12 de septiembre de 2020

Artículos de la serie:

Introducción: Destructores

Cuando se crea un objeto de una clase, el constructor invocado debe establecer sus invariantes --es decir, el conjunto de propiedades que deben respetarse independientemente del estado del objeto--, posiblemente adquiriendo recursos tales como memoria dinámica, archivos, locks (programación concurrente), sockets (networking), etcétera. Una de las características definitorias del lenguaje C++ descansa en el hecho de que, al finalizar el tiempo de vida de dicho objeto, éste llama implícitamente y de forma automática al destructor de su clase [1], una función miembro especial cuyas características describiremos más adelante.

La denominada técnica RAII (Resource Acquisition is Initialization) [2], a la que dedicaremos nuestra atención en esta nueva serie de posts, fue inventada por Bjarne Stroustrup como mecanismo de adquisición y liberación de recursos en C++ de forma segura ante excepciones. Intrínsecamente ligada a la mencionada propiedad de los destructores, esta técnica dicta que sea el destructor de la clase el encargado de liberar los recursos que el objeto haya adquirido durante su tiempo de vida y que aún se encuentren activos.

Como ya hemos mencionado, dicha liberación se lleva a cabo de manera automática y determinista, de forma que los recursos no son utilizados más tiempo del estrictamente necesario, algo clave en el contexto de la programación de sistemas. A modo de ejemplo, el siguiente código muestra la creación de un flujo de salida con un fichero de texto a través de un objeto de la clase estándar std::ofstream. Dicho flujo se cierra de forma automática cuando el objeto sale fuera del ámbito local en el que fue definido:

// abrimos un ámbito local: {       auto ofs = std::ofstream{"datos.txt"}; // creamos un flujo de salida con el fichero       // trabajamos con el fichero...    } // cerramos el ámbito local y   // ofs llama automáticamente al destructor de ofstream, que cierra el flujo

Por lo que respecta a los recursos de memoria, este proceso automático contrasta claramente con los mecanismos de recolección de basura disponibles en otros lenguajes de programación. La recolección de basura (en sus modalidades mark-sweep o mark-compact) se ha demostrado ciertamente innecesaria para la mayor parte de los programas (bien) escritos en C++, si bien se ha argumentado que su uso podría facilitar en cierta medida la implementación de estructuras de datos altamente concurrentes (véase, por ejemplo, la discusión realizada en el artículo [3]).

A diferencia del constructor o constructores de una clase, el destructor es único. Esta función miembro especial se denota con el mismo nombre que la clase a la que pertenece, precedido por una tilde (~), y no puede poseer argumentos ni retornar valores. De no declararse explícitamente, el compilador generará un destructor por defecto (como miembro inline public) para la clase.

Cuando el destructor de una clase es invocado para un objeto, se ejecuta primeramente el cuerpo de dicha función. Cuando el flujo del programa alcanza el final del cuerpo de la función (es decir, su llave de cierre }), todos los subobjetos (si los hubiese) son destruidos automáticamente en orden inverso al de su construcción a través de sus respectivos destructores [1]:

   class X : /* lista de clases base */ {       // datos miembros    public:       // ...       ~X()       {          // cuerpo del destructor          // ...       } /* La destrucción del objeto empieza en este punto según el orden siguiente:          (a) se destruyen los datos miembros no estáticos de X          (b) se invocan a los destructores de las clases bases directas de X          Los datos miembros y las bases son destruidas en orden contrario a su construcción        */    };

A modo de ejemplo:

   class Y {    public:       ~Y();       // ...    };    class Base_1 {    public:       virtual ~Base_1();       // ...    };    class Base_2 {    public:       virtual ~Base_2();       // ...    };    class X : public Base_1, public Base_2 {       Y y1_,         y2_;    public:       X(Y const& y1, Y const& y2) : Base_1{}, Base_2{}, y1_{y1}, y2_{y2} { }       // ...       ~X()       {          // ...       } // en este punto empieza automáticamente la secuencia de destrucción:         // y2_.~Y(), y1_.~Y(), Base_2::~Base_2(), Base_1::~Base_1()    };

La clase X deriva públicamente de las clases Base_1 y Base_2 y posee dos atributos privados de tipo Y. El constructor de la clase X inicializa los subobjetos de las clases base y los atributos privados siguiendo la secuencia Base_1{}Base_2{}, y1_{y1}, y2_{y2}. Al finalizar la ejecución del cuerpo del destructor ~X(), dichos subobjetos son destruidos automáticamente en orden inverso a su creación, según la secuencia y2_.~Y(), y1_.~Y(), Base_2::~Base_2(), Base_1::~Base_1().


Referencias bibliográficas:
  1. Destructores en C++ - https://en.cppreference.com/w/cpp/language/destructor
  2. RAII - https://en.cppreference.com/w/cpp/language/raii
  3. Lock-free data structures - https://www.drdobbs.com/lock-free-data-structures/184401865

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

No hay comentarios:

Publicar un comentario