sábado, 18 de septiembre de 2010

Iostream - Redireccionar clog para "loguear" en un archivo

La iostream es la librería de entrada y salida de C++. Existen algunos streams estándares de salida: std::cout, std::cerr y std::clog. El primero apunta a la consola, y los dos últimos... también. Las diferencias son estas:
  • cout apunta a la salida estándar STDOUT (texto de resultado esperado de un programa);
  • cerr y clog apuntan a STDERR (salida de errores y cualquier otra porquería).
Aunque a primera vista todo el texto se envía a la consola, uno puede redireccionar la salida a otros lados. Por ejemplo, imaginemos que tenemos el siguiente programa test.exe:
#include <iostream>

using namespace std;

int main()
{
  cout << "A\n";
  cerr << "B\n";
  clog << "C\n";
  return 0;
}
Al ejecutarlo, obtenemos por pantalla las tres líneas:
test.exe [ENTER]
A
B
C
Pero resulta interesante saber que podemos redireccionar el STDOUT a un archivo y el STDERR a otro. Ejemplo:
test.exe 1>stdout.txt 2>stderr.txt
¿Qué demonios es 1 y 2? Los archivos tienen un descriptor que los identifica, 1 es para la STDOUT, y 2 para STDERR. El signo mayor (>) significa que "quiero redireccionar toda la salida de texto que vaya para este descriptor a este archivo". En el anterior ejemplo logramos obtener dos archivos distintos, stdout.txt que contiene una línea (A), y stderr.txt que contiene dos líneas (B y C).

Generalmente, los logs van a un archivo, no a la pantalla. Aunque por defecto clog mande todo a STDERR, resulta útil redireccionar este stream a un archivo propio (por ejemplo, test.log). De esta forma, podemos hacer uso de clog para "loguear" todo lo que nuestro programa hace.

¿Cómo se redirecciona clog? Básicamente los streams de C++ tienen un streambuf asociado, y éste es el realmente encargado de leer y escribir datos (en la pantalla, en un archivo, en un string en memoria, etc.). Por lo tanto, si creamos un fstream y le colocamos su propio streambuf a clog, podemos usar clog como un "alias" del fstream original (clog va a estar compartiendo el mismo streambuf que el fstream). El código resultante es bastante sencillo:

#include <iostream>
#include <fstream>

using namespace std;

int main()
{
  // Creamos un archivo de salida para logging.
  ofstream test_log;
  test_log.open("test.log");

  // Obtenemos el streambuf actual de clog (esto
  // lo usaremos luego para restaurar el streambuf
  // a su valor original, por si las moscas).
  streambuf* old_rdbuf = clog.rdbuf();

  // Reemplazamos el streambuf de clog con el del archivo.
  // Ahora ambos streams utilizarán el mismo streambuf (es
  // decir, escriben en el archivo test.log).
  clog.rdbuf(test_log.rdbuf());

  // Hacemos lo mismo que el ejemplo original.
  cout << "A\n";
  cerr << "B\n";
  clog << "C\n";
  
  // Restauramos el viejo streambuf de clog.
  clog.rdbuf(old_rdbuf);

  // Cerramos el archivo.
  test_log.close();
  return 0;
}
Y listo, ahora podemos hacer lo mismo que antes:
test.exe 1>stdout.txt 2>stderr.txt
Con lo cual obtenemos tres archivos:
  • stdout.txt con la línea A.
  • stderr.txt con la línea B.
  • test.log con la línea C.

lunes, 30 de agosto de 2010

Miembros virtuales

Una función miembro virtual tiene la característica de poder ser sobrecargada por subclases para agregar un comportamiento propio. Pero existen dos lugares en donde las funciones virtuales toman un comportamiento especial: en los constructores y los destructores.

Veamos el siguiente ejemplo:
#include <iostream>

using namespace std;

class Lugar {
public:
  Lugar() {                     // El lugar es creado
    creacion();                 // Llamamos a la función virtual
  }
  virtual ~Lugar() {            // El lugar es destruido
    destruccion();              // Llamamos a la función virtual
  }
protected:
  virtual void creacion()    { }
  virtual void destruccion() { }
};

class Universo : public Lugar {
protected:
  void creacion()    { cout << "Big Bang\n"; }
  void destruccion() { cout << "Big Crunch\n"; }
};

int main()
{
  Universo i_am_god;  // ¿Qué imprime este programa?
  return 0;
}

La idea de todo método virtual es poder proporcionar puntos de extensión a las clases derivadas. En este caso, ¿Lugar::creacion funciona como punto de extensión para clases derivadas? La respuesta es: no. El anterior programa no imprime nada.

Aunque creamos una instancia de Universo, ni el método Universo::creacion ni Universo::destruccion se llamaron. ¿A qué se debe esto? Imaginemos que al crear un Universo, primero debemos crear un Lugar en su totalidad, para luego comenzar a crear el universo. Es por eso que si llamamos a creacion mientras estamos construyendo el Lugar, no podemos alcanzar el método Universo::creacion ya que el Universo todavía no comenzó a ser construido (no tiene un Lugar completamente construido donde existir).

La secuencia correcta es:

  1. Alojamos un cacho de memoria suficiente como para que entre el universo.
    Algo así como hacer un: this = malloc(sizeof(Universo)) en C.
  2. Construimos el Lugar usando la memoria recién obtenida como puntero this.
  3. Luego construimos el Universo.
  4. Y recién ahí somos capaces de llamar a Universo::creacion y asegurarnos que estaremos usando el método especializado de la subclase.

¿Puedo llamar a creacion dentro del constructor de Universo? La respuesta es sí, todo es posible. ¿Es correcto? Mmmhh, depende, si alguien hace una subclase de Universo y sobreescribe creacion, nuevamente estará en el mismo problema que estamos mostrando aquí.

¿Por qué en el destructor de Lugar no llama a Universo::destruccion? Porque el Universo ya está destruido (¿cruncheado?) para cuando llegamos al destructor del Lugar.

¿Cómo lo soluciono? Usar puntos de extensión en los constructores y destructores presentan más problemas que ventajas. La solución es no usarlos. Solución (obvia):

#include <iostream>

using namespace std;

class Lugar {
public:
  Lugar() { }
  virtual ~Lugar() { }
};

class Universo : public Lugar {
public:
  Universo()  { cout << "Big Bang\n"; }
  ~Universo() { cout << "Big Crunch\n"; }
};

int main()
{
  Universo i_am_god;
  return 0;
}

En un próximo post voy a dar un ejemplo más complejo donde esto no sirve.

¿Y qué sucede si llamo una función miembro abstracta en el constructor o destructor? Es el desastre total. Completamente ilegal. Imposible. Una violación absoluta a la razón y el sentido común. Imagine este código:

#include <iostream>

using namespace std;

class Lugar {
public:
  Lugar() { creacion(); }        // Ja llamo a creacion()
protected:
  virtual void creacion() = 0;   // Jaja y no lo defino ;)
};

class Universo : public Lugar {
protected:
  void creacion() { }            // Jajaja sólo puede llamarme a mí!
};

int main()
{
  Universo i_am_god_x2;
  return 0;
}

Al compilar este código con gcc obtenemos un warning porque estamos llamando un método abstractor en el constructor, y un error de enlace al crear el ejecutable ya que Lugar::creacion no está definido:

test.cpp: In constructor 'Lugar::Lugar()':
test.cpp:7:22: warning: abstract virtual 'virtual void Lugar::creacion()' called from constructor
C:\temp\ccstr9Hr.o:test.cpp:(.text$_ZN5LugarC2Ev[Lugar::Lugar()]+0x16): undefined reference to `Lugar::creacion()'
collect2: ld returned 1 exit status

Aunque dijimos explícitamente que Lugar::creacion() es abstracto (=0), eso no significa que no debamos definirlo en este caso tan particular.

Referencias:
Sutter, Herb & Alexandrescu, Andrei (2004). Item 49: Avoid calling virtual functions in constructors and destructors. C++ Coding Standards: 101 Rules, Guidelines, and Best Practices. Boston, MA: Addison-Wesley Professional, (ISBN 0321113586).

domingo, 6 de junio de 2010

Descargar MinGW con gcc 4.5 automáticamente

Este post se podría llamar:
¿Cómo instalar MinGW?
¿Cómo instalar y usar gcc 4.5 en Windows?
¿Cómo compilar programas de C o C++ en Windows?
O también, ¿puedo portar MinGW en un pendrive?
Cualquiera de los casos se responde con el siguiente archivo: MinGW-Downloader-0.2.zip.

Los pasos a seguir son los siguientes:
  1. Cree una carpeta donde descomprimir MinGW-Downloader-0.2.zip (por ejemplo, C:\Compilers, o C:\GNU, lo importante es que la carpeta no tenga espacios!)
  2. Descomprima MinGW-Downloader-0.2.zip en esa carpeta
  3. Ejecute el archivo "C:\Compilers\MinGW-Downloader\MinGW-4.5-Downloader.bat"
  4. Espere...
  5. Una vez finalizado deberá ver algunos mensajes en la pantalla diciéndole si tuvo éxito (SUCCESS), inclusive el script intenta compilar un programa en C y otro en C++.
  6. Listo, ya tiene MinGW con gcc 4.5 disponible en su máquina.
Puede usar el script "Run-cmd-with-MinGW-4.5.bat" para ejecutar la línea de comandos de Windows y tener el compilador disponible (gcc, g++, etc.) en la variable PATH. Lo importante es que el proceso de descarga no modifica ninguna variable de entorno suya (PATH), por lo tanto, podría borrar la carpeta "C:\Compilers" y empezar desde cero siempre que así lo desee.

Puede mover la carpeta "C:\Compilers\MinGW-Downloader\MinGW-4.5" a "C:\MinGW-4.5" y borrar "C:\Compilers\MinGW-Downloader" completamente. Si quiere tener el compilador disponible desde cualquier aplicación (por ejemplo, desde un editor de texto o IDE), podría agregar la ruta "C:\MinGW-4.5\bin" a su PATH.

Cabe destacar que la carpeta "MinGW-4.5" ("C:\Compilers\MinGW-Downloader\MinGW-4.5") y el archivo "Run-cmd-with-MinGW-4.5.bat" los puede mover donde usted desee (por ejemplo, llevarlos en un pen drive), sólo tenga en cuenta que para hacer esto los dos tienen que estar en el mismo directorio (e.j. "C:\MinGW-4.5" y "C:\Run-cmd-with-MinGW-4.5.bat").

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...

miércoles, 31 de marzo de 2010

C++0x Final Committee Draft (FCD)

Ya está disponible el documento N3092, Programming Languages — C++, Final Committee Draft. Este documento es el que más podría corresponder al nuevo estándar ISO de C++ (lo que viene de acá a uno o dos años pueden ser correcciones menores):

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3092.pdf

Más información acá.

miércoles, 27 de enero de 2010

STL string, parseando un número de versión

En esta ocasión vamos a ver cómo usar la clase std::string para convertir una cadena de caracteres (como "1.2.3") en una clase "Version" que tenga una interfaz simple de utilizar (por ejemplo, que permita comparar versiones).

Los pasos son simples:
  1. tenemos como entrada una cadena de caracteres,
  2. dividimos la cadena según el caracter '.' (punto),
  3. convertimos cada parte a entero,
  4. guardamos cada número entero de forma ordenada en un vector.
El punto 2 es en el cual nos vamos a enfocar más. El código mostrado aquí es a modo de ejemplo, y está lejos de ser el más óptimo.

Antes de comenzar ¿Podemos usar strcmp para comparar dos versiones? No, no podemos. Ejemplo: strcmp("1.9", "1.10") > 0, cuando en realidad la versión 1.9 es menor a 1.10.

Imaginemos que tenemos una cadena:
std::string ver = "1.10.2.3";
La clase std::string tiene la función miembro find, la cual podemos usar para buscar casi cualquier cosa dentro de la cadena. ¿Qué debemos buscar? Los puntos, sabiendo donde está cada punto, podemos ir recortando la cadena en sus distintas partes ("1", "10", "2", y "3"). Buscamos la ubicación del primer punto:
size_t i = ver.find('.');

if (i != std::string::npos)
  std::printf("El punto fue encontrado en la posición %d\n", i);
else
  std::printf("No hay punto en la cadena\n");
Ejecutando el código anterior, deberíamos obtener el mensaje:
El punto fue encontrado en la posición 1
¿Qué es size_t? Es como el tipo "unsigned int", el tipo de dato retornado por el operador sizeof() y utilizado como índice en las funciones de std::string.

¿Qué es std::string::npos? Es el máximo valor posible de un size_t y se utiliza para indicar (en este caso) que la función find falló (no encontró el punto).

Una vez que tenemos la posición del punto, podemos obtener la porción de texto que contiene la primer cifra, para eso usamos la función miembro substr:
std::string primer_cifra = ver.substr(0, i);
Esto significa: che vos, función substr, devolveme el pedazo de cadena de caracteres que va desde el índice 0, y tiene una longitud de i-caracteres.

El proceso puede ser repetido tantas veces como queramos para seguir obteniendo cifras. Por ejemplo, para la siguiente cifra debemos buscar (find) el siguiente punto, pero comenzando desde el que encontramos hace un rato:
i++;     // ir a la posición siguiente del punto '.'
size_t j = ver.find('.', i);
¿Qué es (o hace) el segundo argumento de find? Le indica a la función desde donde debe comenzar a buscar. La primera vez que usamos find este argumento no se especificó, porque por omisión toma el valor 0, es decir, buscar desde el inicio de la cadena.

¿Por qué i++? Porque si comenzáramos a buscar un punto desde i, nos devolvería la misma posición i (porque justamente, en i, está el primer punto que encontramos). Entonces debemos avanzar una posición.

Ahora, debemos recortar la segunda cifra:
std::string segunda_cifra = ver.substr(i, j-i);
La segunda cifra, comienza desde i y tiene una longitud igual a j-i. Para comprender esto, vea la siguiente figura:


De esta forma, ver.substr(i, j-i) nos devuelve la cadena "10". La versión completa del algoritmo puede quedar algo así:
#include <vector>  // Por std::vector
#include <string>  // Por std::string
#include <cstdlib> // Por std::strtol
#include <cstdio>  // Por std::printf

int main()
{
  std::string ver = "1.10.2.3";

  // Vector con cada cifra.
  // Luego del procesamiento esto debería ser = { 1, 10, 2, 3 }
  std::vector<int> cifras;

  size_t i = 0;   // Comenzamos desde i=0
  size_t j = 0;

  // Repetir hasta no llegar al final de la cadena
  while (j != std::string::npos) {
    // Buscar próximo punto
    j = ver.find('.', i);

    std::string cifra;

    // Si se encontró un punto
    if (j != std::string::npos) {
      // Recortamos desde i hasta j
      cifra = ver.substr(i, j-i);

      // El nuevo comienzo para buscar puntos será "j+1"
      i = j+1;
    }
    // Si no se encontró un punto
    else {
      // Recortamos desde "i" hasta el final de la cadena
      cifra = ver.substr(i);
    }

    // Obtener el valor entero de la cifra
    int cifra_int = std::strtol(cifra.c_str(), NULL, 10);

    // Agregar la cifra al vector
    cifras.push_back(cifra_int);
  }

  for (size_t i=0; i<cifras.size(); ++i)
    printf("cifras[%d] = %d\n", i, cifras[i]);

  return 0;
}
La salida del anterior programa es:
cifras[0] = 1
cifras[1] = 10
cifras[2] = 2
cifras[3] = 3
Con este código, podríamos implementar una clase "Version" con la siguiente interfaz:
class Version
{
public:
  Version(const char*);
  Version(const std::string&);

  bool operator==(const Version& u) const;
  bool operator<(const Version& u) const;
  // ...
};
En un próximo post voy a colocar una posible implementación de la clase "Version".