Charlando sobre RAII

"Resource Acquisition Is Initialization" es uno de los idiomas característicos de C++. Quizás no muchos lo conozcan como tal y lo estén usando en de todas formas.
La idea es simple, aprovechar que la creación y la destrucción de objetos es determinística para manejar recursos, protegiéndose de la perdida o leakage de estos. En lenguajes basados en Maquinas Virtuales, la destrucción del objeto depende del momento en el que el Garbaje Collector lo destruye y no del momento en el que el objeto sale de scope. El idioma no se puede aplicar en estos lenguajes con tanta facilidad y se tiene que recurrir a otras estructuras de programación (En C# se ha introducido la palabra using al lenguaje para poder usar RAII, pero el código no queda tan limpio como en el caso de C++).

Veamos un ejemplo simple. Supongamos que tenemos que abrir un archivo, realizar alguna operación con el y después cerrarlo, o sea liberar el recurso asociado a el. Para esto disponemos de un API para trabajar con el file system basada en handlers.

Una primera aproximación para encarar el programa seria:

int open_file(const std::string & path);
...

void
close_file(int file_handle);

Supongamos ahora que según el contenido del archivo nuestra función realice distintas tareas:
void foo()
{

int
file_handle = open_file(" file.ext");
...

if
( no_procesar )
{

close_file(file_handle);
return
;
}
...

if
( caso_excepcional )
{

close_file(file_handle);
throw
( excepcion );
}
...

close_file(file_handle);
}

En este caso vemos como manejar correctamente el recurso, es decir, liberarlo cuando ya no sea necesario comienza a tornarse complicado.
La mantenibilidad del código es un dolor de cabeza.
El problema crece cuando se tiene en cuenta que en el medio de la función pueden aparecer excepciones. Tendremos entonces que tener en cuenta la liberación del recurso dentro de cada uno de los bloques catch. El código se volverá muy fragil. Java y C# tienen una estructura especial para evitar este problema que permite ejecutar un bloque de código siempre, aun cuando aparezca una excepción. C++ no posee tal estructura ya que mediante el uso de RAII se vuelve innecesaria.
El limite de fragilidad al que se puede llegar es muy alto:
void foo()
{

int
file_handle = open_file("file.ext");
...

if
( no_procesar )
{

close_file(file_handle);
return
;
}
...

if
( caso_excepcional )
{

close_file(file_handle);
throw
( excepcion );
}
...

try

{
...
}

catch
( excepcion_a e )
{

close_file(file_handle);
throw
(e);
}

catch
( excepcion_b e )
{

close_file(file_handle);
return
;
}
...

close_file(file_handle);
}

Aparece RAII. La idea entonces es hacer uso de la vida de los objetos.
Una de las formas de lograr mejorar el código es crear un wrapper que maneje el handle.
Por ejemplo:
class File
{

public
:
File(const std::string & path) :
file_handle( open_file(path) ) {};

~
File() { close_file(file_handle); }
...

private
:
int
file_handle;
};

El código anterior queda escrito como:
void foo()
{

File file("file.ext");
...

if
( no_procesar )
{

return
;
}
...

if
( caso_excepcional )
{

throw
( excepcion );
}
...

try

{
...
}

catch
( excepcion_b e )
{

return
;
}
}

El problema ha desaparecido. Cuando file salga de su scope, liberara el recurso que tiene asociado.
La robustez que se ha ganado es muy importante. Este método también tiene otras ventajas: podemos agregar al wrapper chequeos para asegurarnos que el archivo ha sido correctamente abierto por ejemplo. Es posible también ocultar completamente el API basado en handler agregando las funciones necesarias a esta clase, haciendo el código resultante independiente de este sistema de archivos en particular. Si en algun momento cambia el API, solo tendremos que modificar el wrapper. Todo esto puede ser logrado con overhead cero sobre el API ya que el compilador puede hacer inlining de las llamadas a cada una de las funciones.

De todas formas, hay ocasiones en donde no resulta cómodo realizar tal wrapper. Por ejemplo, cuando el API es demasiado grande y el esfuerzo en realizar los mismos es excesivo.
Podemos en este caso seguir utilizando RAII usando guardas.
Si tenemos el siguiente template:
template<class Handle, class Releaser>
class
handle_guard
{

public
:
handle_guard( Handle h, Releaser r ) :
handle(h), releaser(r) {}

~
handle_guard() { releaser(handle); }

private
:
Handle handle;
Releaser releaser;
};

Podríamos codificar la función anterior como:
class file_releaser
{

public
:
void
operator(int file_handle) const
{

close_file(file_handle);
}
};


void
foo()
{

handle_guard<int,file_releaser> guard(
open_file("file.ext"),
file_releaser() );
...

if
( no_procesar )
{

return
;
}
...

if
( caso_excepcional )
{

throw
( excepcion );
}
...

try

{
...
}

catch
( excepcion_b e )
{

return
;
}
}

Cuando la guarda salga de scope, utilizara el releaser para liberar el recurso que tiene asociado.

Los beneficios de este idioma son muy grandes y mejoran la robustez del código. Utilizar RAII es indispensable para la salud de un proyecto en C++.

C++ en español

No existen muchos recursos en español donde se discuta sobre lenguajes, y menos sobre C++. Parece ser que la idea le gusto a la gente de Blogger. El 24 de octubre apareció como "Blog of Note" del día y unas 6000 personas dieron una vuelta por la pagina.
En estos momentos estoy dedicándome a cerrar los últimos detalles de Boost.Bimap para poder presentarlo en una review formal en Boost. Va a estar bastante divertida la presentación, aviso cuando comience.
El 13 de octubre presente una charla en las "Sextas Jornadas de Software Libre" sobre mi experiencia en el GSoC 2006. El lunes 30 voy a repetir esta charla en La Plata, Argentina para estudiantes de Ingeniería Informática. Después de la charla voy a hablar sobre Boost y C++. Si alguien anda por ahí, se pueden acercar a la Facultad de Ingeniería Informática.
Seguiremos charlando sobre C++ entonces, ahora que parece haber gente interesada en aprender conmigo...

El camino de un ciclo

El código necesario para recorrer un vector de enteros y, por ejemplo, sumarle dos a cada uno de los valores que contiene ha sufrido grandes cambios a lo largo del tiempo.
Este es quizás uno de los ejemplos que demuestran la continua evolución del lenguaje. Veamos pues, un poco de historia.


El legado de C

C++ tomo sus raíces en C. Aun en estos días, la compatibilidad con código C es uno de los objetivos del comité para el estándar, aunque ultimamente han dejado de ser tan estrictos al respectos. En los primeros días de vida del lenguaje, los programadores hubieran escrito:

int * v = new int[N];
...


for
( int k = 0; k < N ; k++ )
{

v[k] += 2;
}



Aparición de la STL

Con la incorporación de templates, C++ fue capaz de crear contenedores genéricos para darle al programador herramientas de alto nivel que producían código mas seguro y mantenible. Con el tiempo, el uso de templates se fue perfección y dio lugar a la creación de la STL (Standard Template Library), una colección de contenedores genéricos incluyendo vectores, listas, conjuntos y mapeos. Ya hablaremos de la STL en su momento, una pieza de diseño impresionante que vale la pena estudiar.
Cuando hizo su aparición, el ciclo for se transformo para siempre. Con la STL de nuestro lado, ahora la iteración anterior se codifica como:
typedef vector<int> container;
container v(N);
...

for
( container::iterator i = v.begin(), i_end = v.end();
i != i_end; i++ )
{
*
i += 2;
}


Usando algoritmos

Si alguien compara la iteración de nuestro nuevo vector con el ciclo for original, a simple vista parecería ser que no hemos logrado nada. Lo que es peor, el nuevo código resulta menos legible y por lo tanto mas difícil de mantener. Si miramos mejor nos vamos a dar cuenta de que en realidad hemos dado un gran paso. Por ejemplo, ahora es posible cambiar el tipo de contenedor a una lista, y no tendríamos que modificar absolutamente nada del nuevo ciclo for. Este es uno de los principales beneficios de la programación genérica. Hay otras razones por las cuales el nuevo ciclo es un avance, en otro momento las analizaremos. De todas formas, ningún programador de C++ que conozca la stl codificaría el ciclo de esta forma. Una de los éxitos de la STL, es la introducción de algoritmos genéricos para trabajar con los contenedores. Usaremos uno de los algoritmos mas simples para reescribir nuestro ciclo for. Vamos a necesitar una función auxiliar para esto, que sera ejecutada en cada uno de los valores del contenedor.
void sumar_dos(int & numero)
{

numero += 2;
}


De ahora en mas, suponemos que ha sido definido un contenedor de enteros llamado v. El ciclo queda escrito como:

for_each( v.begin(), v.end(), sumar_dos );


Agregando flexibilidad

Ahora el código es mucho mas legible. Existe un problema con este
planteo, ¿Que pasa si queremos sumar tres en lugar de dos? ¿Que pasa si utilizamos punto flotante en lugar de enteros? La idea de crear una función auxiliar para cada caso no es aceptable. Introduzcamos un poco de flexibilidad mediante el uso de functores, objetos que definen el operador parentesis, operator() y que por lo tanto pueden ser utilizados como funciones. Además lo haremos genérico para no depender de un tipo de dato en particular.
template<class T>
Sumador
{

public
:
Sumador(T sn) : n(sn) {}
void
operator()(T & numero)
{

numero += n;
}

private
:
T n;
};

Ahora el ciclo queda reescrito como:
for_each( v.begin(), v.end(), Sumador<int> (2) );


Programación funcional

Hemos llegado a un ciclo no solo legible, sino también flexible. Sin embargo, podemos mejorarlo aun mas. La pregunta es: ¿Existe alguna forma de evitar la creación de clases como el sumador? ¿Que pasa si ahora queremos realizar una resta, o una asignación, o cualquier otra función? En el planteo actual tendríamos que crear un functor para cada una de estas operaciones. La STL define los functores mas utilizados pero estos no cubren la totalidad de los casos. La respuesta a nuestro problema se encuentra en una de las características del paradigma funcional: el calculo lambda. La idea es crear funciones anónimas para ser consumidas por el ciclo. Utilizando una librería como Boost.Lambda nuestro ciclo for se puede transformar en:
for_each( v.begin(), v.end(), _1 += 2 );


Un ultimo paso

Con el nuevo estándar, C++ incorporara una nueva herramienta: los conceptos. Esto permitirá crear código genérico en forma mas sencilla y posibilitara la creación en la STL de algoritmos basados en rangos en lugar de en dos iteradores. En unos años, escribiremos nuestro ciclo de la siguiente forma:
for_each( v, _1 += 2 );


Ahora si, definitivamente hemos avanzado.

Que es boost?


Un poco de historia

En 1998 cuando los tipos que decían: "Quiero que haya namespaces!" en las reuniones del Comite para el standard de C++, se dieron cuenta que si no hacían nada, lenguajes como Java a C# se los iban a comer vivo. La principal razón de este pánico justificado era que si alguien en esos lenguajes decía: "quiero hacer tal cosa", seguro que las librerías standard (millones y bien documentadas) ya lo soportaban. En esa epoca los esfuerzos para construir librerias para C++ estaban fragmentados. Además no podían simplemente esperar sentados 10 años hasta las proximas reuniones del comité (para las que ya no falta mucho) principalmente porque se iban a aburrir :)

Entonces decidieron crear Boost. Básicamente un repositorio de librerías de C++ en donde seguir trabajando y principalmente experimentando. (Habían visto nacer la stl hacia muy poco y se estaban escribiendo los primeros papers sobre programacion genérica en serio, policies, metaprogramacion con templates y otras yerbas bastante impresionantes, que de todas maneras en ese momento nadie comprendia del todo). En los inicios, los mismos miembros del comité fueron los que empezaron a hacer rodar los engranajes, después se les fue de las manos cuando muchos de los tipos mas grosos del ambiente de C++ empezaron a participar. Actualmente hay alrededor de unas 40 librerías que complementan (y se llevan muy bien) al standard. Son Grosas con mayúscula. Casi todas usan metaprogramacion y programación genérica para lograr algo que solo se puede hacer en C++, polimorfismo estático, una opción a la OO, teniendo como principal característica la reutilizacion real de código, y overhead cero sobre implementaciones hechas en C por ejemplo. (básicamente se pasa todo el laburo posible desde el runtime al tiempo de compilacion). El TR1 es uno de los principales documentos que se van a tratar en las proximas reuniones del comité para el standard, de modo de ver que cosas son promovidas al namespace std. Para que se den una idea de la importancia de esta librería en el mundo del C++ (talvez en el futuro, porque hoy en día mucha gente no la conoce igual) de las 13 propuestas de librerías del TR1, 9 son librerías de Boost.

Cuando se quiere encarar un desarrollo en C++, hay que dejar de reinventar la rueda. Los pasos para serian los siguientes:
  1. Ver si lo que necesito esta en el estandard.
  2. Sino, buscar en Boost.
  3. Sino esta ahi entonces buscar en otros proyectos importantes como ASL o POCO (ya voy a hablar de estos proyectos tambien)
  4. Sino, buscar en la web. Hay muchos proyectos open source que estan llegando a un nivel muy interesante.
  5. Si no hay nada en todos los lugares anteriores, hay que arremangarse y codificar un poco.
Les recomiendo que vayan a ver la documentacion que tienen las librerias de boost. Realmente hay mucho esfuerzo.

Una sola cosa mas, y muy importante. La licencia de estas librerias es Boost Software License, que es mucho mas permisiva que GPL y permite que las librerias sean utilizadas en programas comerciales sin problemas.

Cuando vean las cosas que se pueden hacer con Boost se van a dar cuenta de lo que es C++ moderno.

Mito I - Portabilidad



Mito I - Portabilidad


"Java es mejor que C++ porque es mas portable."



Java utiliza el concepto de maquina virtual. El código que se genera no es especifico a una plataforma en particular. Un programa nativo: la maquina virtual (VM) se encarga de traducir este código para que la maquina pueda ejecutarlo. De esta manera un código generado en Java puede correr en cualquier plataforma, en donde se haya portado la VM. Una VM es una buena idea ya que permite ahorrar el paso de compilación y linkeo, por el cual por ejemplo un código en C++ se convierte en código nativo. Otros beneficios adicionales tienen que ver con controles adicionales que se pueden implementar dentro del VM pero que no hacen a la portabilidad por lo tanto los trataremos en otro momento.

Como dije, es una buena idea. Pero no es la única forma de encarar un problema e incluye también compromisos que programador tiene que estar dispuesto a aceptar. Es por esto que el esquema de maquinas virtuales es solo un jugador mas en el mundo de la programación, y no es el futuro. El futuro parece favorecer a un esquema en donde se trata de favorecer a la diversidad. Ya han pasado los días en donde las innovaciones eran controladas por los grandes monopolios.

Que es lo que se gana con una VM en cuanto a portabilidad?
El esfuerzo de portar a una nueva plataforma se hace un poco mas simple ya que solo hay que portar la VM. De todas formas en la actualidad los compiladores de C++ (por ejemplo gcc) han sido portados a mas plataformas que Java.

Analicemos el peor de los problemas que tienen estos esquemas. Podemos hacer una analogía con un proceso de traducción. Supongamos que el código del lenguaje es español, y el código maquina es ingles. Una maquina entonces no entiende español y necesita que algo lo traduzca para poder ejecutarlo. Al traductor se le paga en tiempo de maquina, que finalmente implica dinero.

La metodología que utiliza C++ es la siguiente:
Contrata un traductor (el compilador) y le paga una sola vez para que le entregue una copia traducida de su código. Desde ese momento se olvida del problema y le da a la maquina la versión traducida. Este proceso se puede comparar con la traducción de un libro o una película.

La elección de Java:
Contrata un traductor de por vida (el VM), pagándole cada vez que necesita utilizar el código. El traductor traduce en vivo el código una y otra vez. Esto implica que el código se va a ejecutar mas lento.

Porque ha triunfado el esquema de VM ha un nivel tal de creer que cualquier otra forma de encarar el problema es anticuada. En mi opinión hay tres razones fundamentales:
  1. Publicidad agresiva por parte de las empresas detrás de estos lenguajes y desinformación de los usuarios.
  2. Estas empresas realizaron inversiones considerables en crear frameworks y librerías sobre el lenguaje básico para atacar cada uno de los problemas que enfrentan los programadores. Los esfuerzos por realizar la misma clase de infraestructura en C++ han sido dispersos, sin embargo esto esta cambiando y muy rápido.
  3. Desconocimiento de la forma adecuada en la que hay que utilizar un lenguaje como C++. Muchos programadores han reportado que al pasar su código a Java o C# han logrado aumentar la performance. Esto se debe a que estos nuevos lenguajes son mas fáciles de utilizar y por lo tanto al programador no le es permitido cometer tantos errores. Sin embargo el precio que hay que pagar por esta facilidad de uso es muy alto. La facilidad en Java y C# se logro eliminando aquellas estructuras de C++ que no eran sencillas de utilizar como herencia múltiple y templates. Se han hecho muchos avances en el entendimiento de estas herramientas y actualmente son la base del C++ moderno. Mientras tanto Java y C# han tenido que reincorporar algunas de estas estructuras, en especial los templates. Sin embargo para poder ser introducidos el poder de los mismos ha sido nuevamente mermado (Ver por ejemplo, diferencias con C#. Un amigo me ha comentado que este ultimo párrafo es ofensivo. Yo pienso que los generics de C# son buenas estructuras, son mucho mas fácil de utilizar que los templates, eso es seguro. La idea no es desacreditar a los demás lenguajes, todos tienen lo suyo. Hay diferencias entre estos y no es malo conocerlas)
Como conclusión, repito mi postura: el esquema de VM es una buena idea pero no es la única y al encarar un problema hay que hacer una elección teniendo en cuenta los compromisos de cada uno de los lenguajes que tenemos a nuestra disposición. El futuro se basa en la diversidad y en protocolos estándar que comuniquen a los diferentes jugadores. Los monopolios han caído, es hora de aprender cuales son nuestras opciones y ejercer nuestro derecho a elegir.

Sobre este blog



C++ es en la actualidad uno de los mejores lenguajes disponibles, sin embargo en Argentina ha perdido popularidad principalmente debido a agresivas campañas publicitarias dirigidas a Java y .Net. Las empresas han migrado en los últimos años a estas plataformas deslumbradas por las promesas de productividad, portabilidad y seguridad que estas ofrecen. Otra de las razones de esta migración masiva, que surge directamente de los trajeados en altas posiciones, es que los productos se benefician también de la publicidad de estos gigantes.
Java y .Net, ambos basados en VMs no son en ningún sentido lenguajes débiles. Aun mas, muchas de las promesas se convierten en realidad si son utilizados correctamente. El problema es que no son la panacea, ningún lenguaje lo es. Cada lenguaje creado hasta el momento a tenido su lugar en la cadena productiva del software. La variedad es muy importante, hasta el mas simple lenguaje de scripting tiene su nicho. En argentina lamentablemente el mercado IT esta olvidando este hecho. Los programadores tienden a seguir las necesidades de sus empleadores y por lo tanto están invirtiendo todo su tiempo en estos nuevos lenguajes.

La opinión de nuestros programadores es que C++ esta muriendo y que no vale la pena dedicarle tiempo. La verdad es que están equivocados. C++ es uno de los lenguajes mas poderosos de la actualidad y se ha reinventado a si mismo varias veces en su larga historia. Empezando desde sus inicios como un mejor C introduciendo el paradigma de OO al lenguaje mas utilizado de la historia, entrando en la era de programación genérica con la stl en los 90' y llegando al C++ moderno, con nuevas técnicas como la metaprogramacion. En los próximos años un nuevo estándar introducirá nuevas herramientas y librerías.

Como todo lenguaje, C++ también su lugar y no pretende competir con Java o .Net en desarrollos como ERPs, estos lenguajes poseen frameworks muy trabajados para encararlos. C++ es muy efectivo cuando se requiere alta performance. Un ejemplo es procesamiento de señales o imágenes. En general C++ es muy útil para crear el engine de la aplicación que se expone mediante una API. Java, .Net o hasta un lenguaje de scripting como python.

Este blog tratara de vencer algunos de los mitos sobre C++ que plagan la mente de los programadores argentinos. Veremos que termina saliendo :)