martes, 24 de junio de 2008

Excepciones, RAII y auto_ptr

Una de las mejores formas de realizar un control de errores es por medio de excepciones. Lamentablemente, si no se toman las precauciones adecuadas, se pueden obtener leaks de algunos recursos. Por ejemplo, imagine el siguiente caso:
int main()
{
  try {
    char *ptr = new char[256];
    double *matrix = new double[100000*100000]; // 74 Gigabytes
    // ...
    delete matrix;
    delete ptr;
  } catch (const std::bad_alloc& e) {
    std::cout << "No hay memoria suficiente" << std::endl;
  }
  return 0;
}
Aquí, con el segundo new una excepción bad_alloc es lanzada. El problema es que la memoria asignada con el primer new nadie la libera. Esto es un memory leak. Una opción para solucionarlo es mediante un bloque finally "a la Java":
int main()
{
  char *ptr = NULL;
  double *matrix = NULL;
  try {
    ptr = new char[256];
    matrix = new double[100000*100000];
    // ...
    delete matrix;
    delete ptr;
  } catch (...) { // finally
    if (matrix) delete matrix;
    if (ptr) delete ptr;
  }
  return 0;
}
Claro que esto no hace al manejo de errores una tarea transparente.

En C++ se puede utilizar la técnica RAII (la adquisición de recursos está en la inicialización), lo que ayuda a evitar leaks. La idea básica detrás de esto es que los constructores obtengan recursos, y los destructores los liberen. Al ocurrir una excepción, se van llamando los destructores de los objetos creados.

Para solucionar nuestro problema, podemos utilizar la clase auto_ptr que se encuentra en <memory>. Un auto_ptr guarda un puntero que es liberado automáticamente en el destructor.
int main()
{
  try {
    std::auto_ptr<char> ptr(new char[256]);
    std::auto_ptr<double> matrix(new double[100000*100000]);
    // ...
  } catch (const std::bad_alloc& e) {
    std::cout << "No hay memoria suficiente" << std::endl;
  }
  return 0;
}
Los deletes no son necesarios. Al salir del ámbito del bloque try, ya sea normalmente o por excepción, se llamará al destructor de los auto_ptrs creados, así no se incurre en ningún tipo de leak.

Advertencia: Los auto_ptr deben ser utilizados sabiendo cuál es su verdadero funcionamiento: La copia de un auto_ptr a otro no los deja equivalentes. La última copia se queda con el puntero, los demás lo van perdiendo, y el delete lo invoca el último auto_ptr destruido. Por ejemplo:
{
  std::auto_ptr<Persona> otra_persona;
  {
    std::auto_ptr<Persona> una_persona(new Persona);
    otra_persona = una_persona;
    // ahora "una_persona" apunta a NULL
  }
} // aquí la persona es destruida

Pregunta: ¿Por qué no usar objetos alojados en el stack en vez del heap para evitar los leaks? Porque el stack es mucho más pequeño. Por ejemplo, el siguiente código provoca un SIGSEGV ya que el arreglo no entra en el stack:
double matrix[1000000];
Pero sí entra en el heap:
double *matrix = new double[1000000]; // 7.6 Megabytes