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.

domingo, 18 de octubre de 2009

Interfaces vs. Conceptos

¿Qué son los conceptos? Vamos a ver con un simple ejemplo, cómo podemos hacer una equivalencia entre las conocidas interfaces y los conceptos.

Imaginen esta "interfaz" (clase abstracta):
class IPortero {
public:
  virtual IPortero() { }
  virtual void ir_a_piso(int piso) = 0;
  virtual int piso_destino() = 0;
};
Tenemos otra clase Ascensor que podemos "personalizar" con nuestro propio portero, así nuestra implementación de portero puede hacer lo que se le de la gana:
class Ascensor {
  IPortero* m_portero;
public:
  Ascensor(IPortero* portero) {
    m_portero = portero;
  }
  void apretaron_boton_en_piso(int piso) {
    m_portero->ir_a_piso(piso);
    mover_ascensor(m_portero->piso_destino());
  }
  void mover_ascensor(int piso) { ... }
};
Con programación genérica, podemos reformular la interfaz convirtiéndola en un "concepto" y la clase Ascensor en una clase plantilla:
template<class TipoPortero>
class Ascensor {
  TipoPortero m_portero;
public:
  Ascensor() { }
  void apretaron_boton_en_piso(int piso) {
    m_portero.ir_a_piso(piso);
    mover_ascensor(m_portero.piso_destino());
  }
  void mover_ascensor(int piso) { ... }
};
La pregunta es, ¿qué demonios es TipoPortero?. La respuesta es sencilla: TipoPortero puede ser cualquier tipo de dato que cumpla los siguientes requisitos:
  • Tenga un constructor por omisión (que se pueda construir un nuevo TipoPortero sin argumentos, o sea, TipoPortero()).
  • Tenga una función miembro TipoPortero::ir_a_piso(int), la cual recibe un "int" (o cualquier tipo de dato que se pueda construir desde un "int" implícitamente).
  • Y otra función miembro TipoPortero::piso_destino() que devuelve un entero.

¿Cómo especificamos la "interfaz" o los "requerimientos" de un concepto? Sencillamente no se puede, C++0x iba a soportar esto, pero ya no. Hoy en día la mejor respuesta es usar algunos comentarios en la clase Ascensor que especifiquen qué espera en sus parámetros de template. En este aspecto se podría decir que IPortero es mejor porque especifica explícitamente lo que el portero tiene que hacer (funciones a implementar, etc.).

¿Qué ventaja tiene el concepto con respecto a las interfaces? La clase genérica Ascensor ahora tiene el mismo portero adentro suyo (no un puntero a la interfaz). Las llamadas a las funciones miembro ir_a_piso y piso_destino son llamadas directas (no tienen el overhead de una llamada a una función virtual).

sábado, 17 de octubre de 2009

Clases que "desaparecen" luego de compilar

La magia de C++ es que, una vez compilado el código, algunas clases pueden desaparecer por completo (principalmente las que se usan en stack). O sea, aunque las clases abstraen al programador de los detalles de implementación, al final, el código termina siendo tan óptimo como si la clase no fuera utilizada en un principio.

Un ejemplo. Teniendo la siguiente clase Acumulador:
#include <cstdio>

class Acumulador {
  int v;
public:
  Acumulador()         { v = 0;                   }
  ~Acumulador()        { std::printf("%d\n", v);  }
  void acumular(int x) { v += x;                  }
};
Un código como el siguiente:
{
  Acumulador acum;
  acum.acumular(2);
  acum.acumular(4);
  acum.acumular(10);
}
Al compilarlo (optimizándolo), el código equivale a exactamente esto:
{
  int v = 0;
  v += 2;
  v += 4;
  v += 10;
  std::printf("%d\n", v);
}
La clase Acumulador ya no existe. Obtenemos el código más óptimo posible: sin llamadas a la función "acumular", ni ningún byte extra de memoria (Acumulador ocupa lo mismo de memoria que ocupa un "int").

Este ejemplo no ayuda a ver grandes ventajas, pero si el constructor y el destructor hacen tareas complicadas, y las funciones miembros también, el resultado puede llevarnos a dos puntos:
  • Nos abstrae de la complejidad de la implementación (e.j. para qué quiero saber cómo se acumula si sólo quiero acumular)
  • Obtenemos código tan óptimo como si no hubiéramos usado la abstracción (e.j. las operaciones se acercan al hardware tanto como sea posible).

jueves, 23 de julio de 2009

C++0x no va a tener "conceptos"

En la última reunión del ISO C++ Standards Committee decidieron quitar los "conceptos" del próximo estándar de C++ (que en realidad, en vez de C++0x va a ser C++1x y chirola). Un artículo de Bjarne Stroustrup habla al respecto:

[...] Básicamente, los "conceptos" tenían como meta hacer de la programación genérica algo más accesible a la mayoría de los programadores, pero esa meta ha sido para mí (Stroustrup) seriamente comprometida: En vez de hacer la programación genérica más accesible, los "conceptos" se fueron convirtiendo en otra herramienta más en las manos de los expertos (únicamente).
[...]
Para resumir y de alguna forma simplificar, he afirmado que:
  • Los "conceptos" como están actualmente definidos son muy difíciles de utilizar, lo que los llevará al desuso, dando la posibilidad a un desuso de los templates mismos y a la adopción completa de C++0x.
  • Un pequeño conjunto de simplificaciones [BS2009] pueden dejar a los "conceptos" lo suficientemente aceptables como para salir acorde a las fechas planificadas para C++0x o con sólo un retraso menor.
[...]

domingo, 12 de julio de 2009

STL vector en vez de los clásicos buffers de C

Algunas veces tenemos que crear buffers temporales. Por ejemplo, la función sprintf necesita de un buffer para poder dejar la cadena resultante. Ejemplo:
char buf[256];
sprintf(buf, "%d", 12345);
// usar "buf"...

Si queremos mantener el buffer en heap:
char* buf = malloc(256);
sprintf(buf, "%d", 12345);
// usar "buf"...
free(buf);

En C++ podríamos hacer uso de los operadores new[]/delete[]:
char* buf = new char[256];
sprintf(buf, "%d", 12345);
// usar "buf"...
delete[] buf;

Pero es aconsejable evitar la creación de arreglos con new[], y en su lugar hacer uso de std::vector:
std::vector<char> buf(256);
sprintf(&buf[0], "%d", 12345);
// usar "&buf[0]" en vez de "buf"...

El resultado: tenemos un buffer en el heap que se libera automáticamente (~std::vector) al salir del ámbito de la función que lo utiliza.

sábado, 21 de marzo de 2009

STL for_each

Para recorrer un arreglo de C, podemos utilizar punteros. Comenzamos apuntando al primer elemento y frenamos una vez procesado el último elemento:
#include <stdio.h>

int main()
{
  int array[] = { 5, 3, 4, 9, 1 };
  int array_size = sizeof(array) / sizeof(int); // cantidad de elementos de "array"
  int *array_begin = array;                     // comienzo del array
  int *array_end = array+array_size;            // final del array (+un elemento)
  int *it;                                      // el iterator

  for (it = array_begin;  it != array_end;  ++it) {
    printf("%d\n", *it);
  }
  return 0;
}
Hay que tener en cuenta que array_end es igual a &array[5]. Sabiendo que sólo podemos acceder a los elementos desde array[0] a array[4], decimos que array[5] está más allá del último elemento del arreglo, es decir, &array[5] es la posición de memoria que ya no es parte del mismo arreglo (de ahí "it != array_end" significa "mientras nos encontremos dentro del arreglo").

¿Cómo recorrer un contenedor de la STL? La idea es similar, sólo que en vez de usar punteros utilizamos iteradores, y observe como la sintaxis del for es exactamente la misma. (nota: en este caso utilizamos un vector).
#include <iostream>
#include <vector>

using namespace std;

int main()
{
  vector<int> array;
  array.push_back(5);
  array.push_back(3);
  array.push_back(4);
  array.push_back(9);
  array.push_back(1);

  vector<int>::iterator it;

  for (it = array.begin();  it != array.end();  ++it) {
    cout << *it << endl;
  }
  return 0;
}
¿Cómo recorrer un contenedor STL con el algoritmo for_each? (nota: lo siguiente también se puede hacer con copy y ostream_iterator)
#include <cstdio>
#include <vector>
#include <algorithm>

using namespace std;

void print_element(int a) { cout << *it << endl; }
    
int main()
{
  vector<int> array;
  array.push_back(5);
  array.push_back(3);
  array.push_back(4);
  array.push_back(9);
  array.push_back(1);

  for_each(array.begin(), array.end(), print_element);
  return 0;
}
La función pasada como tercer parámetro a for_each es llamada por cada elemento del arreglo:
template
Function for_each(InputIterator first, InputIterator last, Function f)
{
  for (; first != last; ++first)
    f(*first);
  return f;
}
¿Por qué for_each devuelve la función? Principalmente porque la función "f" podría no ser una función! Podría ser un functor (objeto función), es decir, una instancia de una clase que tiene sobrecargado el operator() (llamada a función). Pero este tema se merece su propio post.

¿Se puede usar el algoritmo for_each para los arreglos de C? Sí.
#include <cstdio>
#include <algorithm>

using namespace std;

void print_element(int a) { printf("%d\n", a); }

int main()
{
  int array[] = { 5, 3, 4, 9, 1 };
  int array_size = sizeof(array) / sizeof(int); // cantidad de elementos de "array"
  int *array_begin = array;                     // comienzo del array
  int *array_end = array+array_size;            // final del array (un elemento más)

  for_each(array_begin, array_end, print_element);
  return 0;
}