Mostrando entradas con la etiqueta scoped pointer. Mostrar todas las entradas
Mostrando entradas con la etiqueta scoped pointer. Mostrar todas las entradas

sábado, 29 de mayo de 2010

Shared Pointers de C++0x

En C usted puede hacer esto:
#include <stdlib.h>

struct Persona { };

int main()
{
  Persona* a = malloc(sizeof(Persona));
  free(a);
  return 0;
}

Un ejemplo equivalente en C++:
class Persona { };

int main()
{
  Persona* a = new Persona();
  delete a;
  return 0;
}

Por cada malloc existe un free (al menos que use reallocs), y por cada new existe un delete (y por cada new[] un delete[]).

¿Existe una forma por la cual C++ se "entere" que ya no quiero usar un puntero? La respuesta es: No, no existe. C++ no tiene un garbage collector. Pero existen clases que pueden ayudarnos, como el viejo y tan poco querido auto_ptr, o los mejorados unique_ptr, shared_ptr y weak_ptr del nuevo estándar de C++0x (o el TR1).

Un shared pointer es una clase que se encarga de guardar un puntero a un objeto (o tipo de dato), y cuenta la cantidad de referencias que se están haciendo a dicho objeto (es decir, la cantidad de shared pointers que apuntan al mismo objeto). El último shared pointer que se destruya (cuando las referencias llegan a cero), será el encargado de borrar el objeto apuntado (mediante un simple delete).

Un ejemplo:
#include <iostream>
#include <memory>  // Aquí debería estar shared_ptr<> (GCC 4.4)

using namespace std;

class Persona {
  int n;

public:
  Persona(int n) : n(n) {
    cout << "Nace la persona " << n << "\n";
  }

  ~Persona() { 
    cout << "Muere la persona " << n << "\n";
  }

  static shared_ptr<Persona> Crear(int n) { 
    return shared_ptr<Persona>(new Persona(n));
  }
};

int main()
{
  shared_ptr<Persona> a(new Persona(1));
  shared_ptr<Persona> b = Persona::Crear(2);
  shared_ptr<Persona> c;

  cout << "--- Aquí ambas personas existen ---\n";

  c = a; // Aquí c apunta a la persona 1
  b = c; // Ahora b apuntará a la persona 1 (la persona 2 muere porque
         // ya no existen referencias a ella)

  cout << "--- Aquí la persona 2 ya no existe ---\n";
  
  return 0;
}        // Aquí muere la persona 1 (a, b, c apuntaban a ella)
La salida del anterior programa es esta:
Nace la persona 1
Nace la persona 2
--- Aquí ambas personas existen ---
Muere la persona 2
--- Aquí la persona 2 ya no existe ---
Muere la persona 1
Como puede ver, en el anterior programa se llama sólo dos veces a "new Persona" y dos veces al destructor ~Persona. No debemos preocuparnos por usar "delete", el shared_ptr<> hace todo por nosotros.

¿Cómo hago para que la clase shared_ptr funcione en VS2008 SP1 Express? Debe definir _HAS_TR1 antes de incluir el archivo <memory>. Ejemplo:
#ifdef _MSC_VER      // Si estamos usando el compilador de Microsoft
  #define _HAS_TR1 1 // Esto hará que se incluyan las clases del TR1 (std::tr1)
#endif

#include <iostream>
#include <memory>

using namespace std;
#ifdef _MSC_VER
  using namespace std::tr1; // para tener shared_ptr<> disponible
#endif

// Resto del ejemplo...

viernes, 6 de noviembre de 2009

Punteros por ámbito (scoped pointers)

C++ no tiene un recolector de basura, aunque C++0x le va tomando el gustito, tenemos algunas opciones para liberarnos de liberar memoria, valga la redundancia.

Imaginemos un código como el siguiente:
#include <cstdio>

using namespace std;

class A {
public:
  A()         { printf("A\n");    }
  ~A()        { printf("~A\n");   }
  void hola() { printf("hola\n"); }
};

int main()
{
  A* a = new A;
  a->hola();
  return 0;
}
El anterior programa, por más minimalista que parezca, tiene un memory leak. Aunque los recursos suelen ser liberados por el mismo sistema operativo al finalizar el programa, si el proceso se ejecuta por mucho tiempo (e.j. un servicio que se ejecuta por días, semanas o meses dentro de un servidor), los recursos se van agotando poco a poco hasta que la computadora queda hecha una pasa.

Para arreglar el leak, deberíamos escribir:
int main()
{
  A* a = new A;
  a->hola();
  delete a;
  return 0;
}
¿Cómo hacemos para liberar memoria sin llamar delete nosotros mismos? Podemos crear una clase utilitaria que haga el trabajo por nosotros en su destructor. Por ejemplo, en el siguiente código veremos que el delete es llamado en el destructor de PunteroA:
class PunteroA {
  A* ptr;
public:
  PunteroA(A* p) { ptr = p; }
  ~PunteroA()    { delete ptr; }
};

int main()
{
  PunteroA a(new A);
  a->hola();          // error, "a" no es un puntero ni sobrecarga operator->
  return 0;
}
El anterior programa nos dará un error de compilación ya que el tipo PunteroA no soporta el operador flecha. Debemos agregarlo en la definición de la clase PunteroA:
class PunteroA {
  A* ptr;
public:
  PunteroA(A* p)  { ptr = p; }
  ~PunteroA()     { delete ptr; }
  A* operator->() { return ptr; }
};

int main()
{
  PunteroA a(new A);
  a->hola();        // ahora sí, PunteroA::operator->() nos devuelve
                    // el verdadero puntero A* y A::hola() finalmente
                    // es llamado
  return 0;
}
¿Cómo obtengo el A* desde un PunteroA? Debemos definir el operador de conversión hacia A*:
class PunteroA {
  A* ptr;
public:
  PunteroA(A* p)  { ptr = p; }
  ~PunteroA()     { delete ptr; }
  A* operator->() { return ptr; }
  operator A*()   { return ptr; }
};
Así podemos usar un PunteroA en funciones que reciban un A*. Ejemplo:
void func(A* a) { ... }

int main()
{
  PunteroA a(new A);
  func(a);
  return 0;
}
Generalizando: Podemos generalizar nuestro PunteroA para cualquier tipo de dato:
template<class T>
class ScopedPointer {
  T* ptr;
public:
  ScopedPointer(T* p) { ptr = p; }
  ~ScopedPointer()    { delete ptr; }
  
  operator T*()       { return ptr; }
  T* operator->()     { return ptr; }
};
Así podemos usar el mismo ScopedPointer para liberar instancias de cualquier clase:
int main()
{
  ScopedPointer<A> a(new A);
  ScopedPointer<B> b(new B);
  a->hola();
  return 0;
}
¿Y si quiero liberar otro tipo de recurso? Si el recurso no es memoria, y es liberado con otra función en vez de delete, podemos generalizar nuestro ScopedPointer con un nuevo parámetro de template llamado Destroyer:
struct DefaultDestroyer {
  template<class T>
  static void free(T* ptr) { delete ptr; }
};

template<class T, class Destroyer = DefaultDestroyer>
class ScopedPointer {
  T* ptr;
public:
  ScopedPointer(T* p) { ptr = p; }
  ~ScopedPointer()    { Destroyer::free(ptr); }
  
  operator T*()       { return ptr; }
  T* operator->()     { return ptr; }
};
En este caso, el destructor ~ScopedPointer llama a la función estática Destroyer::free. Esta función básicamente puede hacer lo que nosotros queramos. Podría ser útil para liberar ficheros o cualquier otro tipo de recurso:
struct FileDestroyer {
  static void free(FILE* ptr) { fclose(ptr); }
};

int main()
{
  ScopedPointer<FILE, FileDestroyer> file(fopen("hola.txt", "rt"));
  char buf[256];
  fread(buf, 1, 256, file);
}
¿No existen punteros de este tipo ya implementados? Boost tiene su propio scoped_ptr. También existen punteros más avanzados que cuentan referencias, como los smart pointers.