RAII en C++

RAII es un acrónimo que significa "Resource Acquisition Is Initialization". ¿Y de qué se trata? Es un patrón de diseño de bajo nivel, lo que algunos teóricos llaman "idiom". El problema que busca resolver es facilitar y asegurar la liberación de recursos (archivos, bitmaps, conexiones a base de datos, memoria del heap, etc).

La creación de RAII corresponde a Bjarne Stroustrup, líder del equipo creador de C++. Pese a haber nacido en ese lenguaje, RAII es tan general que puede usarse fácilmente en cualquier lenguaje orientado a objetos con manejo de excepciones. Incluso puede implementarse en otros lenguajes, pero no es tan trivial y por ende puede no valer la pena.

La idea es muy simple: adquirir el recurso SÓLO en el constructor y liberarlo SÓLO en el destructor. De ahí el nombre RAII: al inicializar (construir) el objeto, adquirimos el recurso. Esto ata el uso del recurso a la vida del objeto, y nos evita andar pensando cuándo llamar al método que pide el recurso, y cuándo al que lo libera.

Además de simplificar el código, RAII lo hace más robusto. En la mayoría de los lenguajes orientados a objetos con manejo de excepciones, se garantiza que cuando ocurre una excepción, los destructores de los objetos locales (en el caso de C++, creados en el stack) son invocados. Por lo tanto, los recursos serán liberados incluso si ocurre una excepción. Si usamos RAII consistentemente, esto nos evita escribir unos cuantos catch.

Veamos un ejemplo de código sin RAII:

void foo() {

    try{
        CFile logfile;
        logfile.Open("logfile.txt");
        logfile.Write("hello logfile!");
        // Seguir usando logfile ...
        logfile.Close()
    catch(std::exception){
        logfile.Close();
    }
}

¿Qué problemas tiene el código de arriba? Varios, y no menores.

  • Hay que llamar explícitamente a close(). Puede ocurrir perfectamente que alguien haga una función basada en ésta y se olvide de llamar a close().
  • close() es invocado dentro del try() y del catch(); esto es porque C++ no tiene una cláusula finally como C#, si no al menos nos ahorraríamos eso.
  • Lo peor: Si salta una excepción que no herede de std::exception, el archivo no se cierra. Si de entrada hacer un catch de std::exception es mala idea, hacerlo general (con ...) es aún peor.

Para aplicar RAII en este ejemplo, debemos hacer privados el constructor por defecto y los métodos open() y close() de CFile. Luego, definimos un constructor que reciba el file name y llame a open(), y hacemos que el destructor llame a close(). Luego, actualizamos foo así:

void foo() {

    CFile logfile( "logfile.txt" );
    logfile.Write("hello logfile!");
    // Seguir usando logfile ...
    // close() es invocado por el destructor al irse de scope logfile, o si hay una excepción
}

Saqué el try-catch asumiendo que sólo estaba para atrapar excepciones relacionadas al uso de logfile. Dado que sólo nos interesaba capturar esas excepciones para cerrar el archivo, ahora que se cierra solo salte una excepción o no, no hace falta el try-catch.

Comments

Popular posts from this blog

VB.NET: Raise base class events from a derived class

Apache Kafka - I - High level architecture and concepts

Upgrading Lodash from 3.x to 4.x