Usando Git Flow

Como vimos en el artículo anterior (que es recomendable leer para entender correctamente este), podemos hacer uso de flujos de trabajo bastante completos y potentes con Git.

El problema es que la necesidad de ejecutar una serie de comandos cada vez que se quiere hacer una tarea cotidiana (integrar una rama de feature, lanzar una versión, ...) es muy dada a errores, ya que con que nos equivoquemos en alguno de ellos, o los ejecutemos con algún parámetro inadecuado nuestro repositorio puede dejar de funcionar tal como lo queremos.

Por ello, el creador del flujo de trabajo visto en el anterior artículo creo una herramienta llamada git-flow, que añade una capa por encima de git, facilitando todas las tareas descritas anteriormente.

Instalando git-flow

Esto es dependiente de la plataforma que estemos usando. Por ejemplo, si usamos Linux bastará con ejecutar el siguiente comando, que descargará un script de instalación y lo ejecutará:

$ wget --no-check-certificate -q -O - https://github.com/nvie/gitflow/raw/develop/contrib/gitflow-installer.sh | sudo bash

En el caso de usar MacOS, podemos instalarlo gracias a homebrew:

$ brew install git-flow

Una vez instalado no necesitaremos nada más, y podremos empezar a usarlo en nuestros nuevos repositorios (o en los ya existentes).

Usando git-flow

Como hemos dicho anteriormente git-flow no es más que una capa que se pone por encima de git, por lo que para usarlo, pasaremos el parámetro flow al comando git y a continuación indicaremos la operación que queremos realizar.

Si queremos ver todas las opciones posibles solo tenemos que pasar como parámetro help.

$ git flow help
usage: git flow <subcommand>

Available subcommands are:
   init      Initialize a new git repo.
   feature   Manage your feature branches.
   release   Manage your release branches.
   hotfix    Manage your hotfix branches.
   support   Manage your support branches.
   version   Shows version information.

Como podemos ver, la forma de usarlo es bastante sencilla, pero vamos a verla más en detalle.

Inicializando el repositorio

La primera orden que debemos realizar con git-flow antes de comenzar a usar el resto de las opciones es inicializar el repositorio. Este comando creará la rama develop, y configurará el prefijo que queremos que tengan los distintos tipos de ramas temáticas.

$ git flow init

Tras una serie de preguntas (que podemos dejar vacías para que se usen las opciones predeterminadas), nuestro repositorio quedará inicializado correctamente, y nos dejará puesto como HEAD la rama develop.

Desarrollando en ramas

Si llegado cierto punto queremos crear una rama de features para desarrollar cierta característica en particular, no tendremos más que crear la rama.

$ git flow feature start bug-144

Con el anterior comando git-flow creará una rama llamada feature/bug-144 y posicionará nuestro HEAD en ella. A continuación, no tendremos más que desarrollar y realizar commits de forma normal y corriente.

Llegado el momento de integrar el trabajo (con la rama develop, recordad) bastará con ejecutar el siguiente comando:

$ git flow feature finish bug-144

De esta manera, git-flow se encargará de integrar la rama en develop, borrar la rama bug-144 y por último situar nuestro HEAD de nuevo en la rama develop.

Este es el flujo normal con este tipo de ramas, pero como dijimos en el artículo anterior, podríamos querer que nuestra rama bug-144 fuese subida al repositorio central para que varios desarrolladores pudiesen trabajar en ella. Para ello, bastará con pasarle como parámetro la opción publish.

$ git flow feature publish bug-144

De esta manera se subirá a origin la rama bug-144. Para que cualquier otro desarrollador pueda bajarse esa rama habrá que hacer algo análogo, usando la opción track.

$ git flow feature track bug-144

Llegado a este punto ambos desarrolladores podrán realizar cambios y commits sobre la misma rama de desarrollo. Ya solo queda poder bajarse de forma regular los cambios subidos por la otra persona. Tarea que se realizará mediante un pull.

$ git flow feature pull origin bug-144

Mediante este último comando le indicamos a git-flow que se descargue todos los cambios realizados en el repositorio origin para la rama bug-144. Este último parámetro no es obligatorio, bajándose los cambios de todas las ramas en caso de no ponerlo.

Por último, y si vemos la ayuda (git flow feature help), podremos ver como existen un par de opciones más que poder realizar, pero debido a su sencillez, se dejan a la curiosidad del lector :)

¿Y el resto de ramas?

Pues el resto de ramas (las de releases y hotfix) funcionan de forma totalmente análoga a lo explicado en el anterior apartado.

$ git flow hotfix start
$ git flow release finish v2.2
...

Ambos tienen los mismos subcomandos (alguno menos en realidad), pero los principales (start, finish y publish) funcionan de igual manera a lo explicado anteriormente.

Concluyendo...

Y en conclusión no hay mucho más que contar. En esta serie de dos artículos hemos podido ver la potencia que tiene Git, la libertad que nos brinda a la hora de elegir como trabajar, y el jugo que le podemos sacar usándolo correctamente.

La mayor ventaja de git-flow sobre otros modelos de trabajo es que nos obliga a ser organizados en la forma en la que tratamos nuestro repositorio, creando un flujo de trabajo que se adapta muy bien a la mayor parte de los desarrollos.

Y esto es todo,

$ git push sysvar usando-git-flow

Entendiendo Git Flow

Una de las características que más me gustan de Git es que es una auténtica navaja suiza en cuanto a que permite seguir el flujo de trabajo que más nos convenga en nuestro proyecto.

La potencia que tiene la gestión de ramas, la forma en la que está diseñado y el hecho de que sea distribuido nos permite adoptar multitud de flujos; desde el más simple en el que imitamos un repositorio centralizado como subversion hasta otros más complicados en los que se crean multitud de ramas y servidores que nos ayudan a organizar y jerarquizar mejor todo.

Hoy vamos a hablar de uno de estos últimos, un modelo que ha cobrado muchísima fama últimamente y que a pesar de poder ser algo lioso al principio, veremos que nos puede ayudar muchísimo en el día a día.

Este artículo está dividido en dos partes (enlace a la segunda parte), siendo esta primera la encargada de explicar el flujo de trabajo a seguir y la segunda la encargada de explicar como usar git-flow, una herramienta creada para facilitar el uso de este modelo (se instala como un plugin de git).

Entendiendo este artículo

Para leer este artículo hay que tener una base (tampoco demasiado amplia) sobre gestores de versiones distribuidos (Git, Bazar, Mercurial, ...) y sobre terminología Git.

Como repaso rápido recordemos lo siguiente:

  • El término HEAD referencia al último commit existente en la rama en la que estemos.
  • Llamaremos origin al servidor central donde estará alojado nuestro proyecto.
  • Usaremos la nomenclatura estándar al nombrar ramas: servidor/nombre_rama.

Y poco más. Sabido esto podemos empezar... :)

El modelo de ramas acertado de Git

O como se llama en inglés A successful Git branching model (lo que es poco acertado es la traducción que he hecho).

Creado por Vincent Driessen, es un modelo de flujo de trabajo para Git que da muchísima importancia a las ramas y que de hecho las crea de varios tipos, de forma temática, tal que cada tipo de rama es creada con un objetivo en concreto.

A continuación vamos a ir explicando cada uno de estos tipos, con el uso y el objetivo que tienen en este flujo de trabajo.

La rama master

La rama master es la única rama existente que nos proporciona Git al crear un repositorio nuevo.

Esta rama (sincronizada en todo momento con origin/master) tiene como objetivo ser el contenido del servidor de producción. Es decir, el HEAD de esta rama ha de apuntar en todo momento a la última versión de nuestro proyecto.

../../static/images/0007-entendiendo-git-flow/master.png

Como podemos ver, cada commit (ilustrado por las pelotitas) es una nueva versión del proyecto, que a su vez está referenciada mediante un tag (representada por la nube).

No se va a desarrollar desde esta rama en ningún momento.

La rama develop

Esta rama funciona paralelamente a la master. Si la anterior contenía las versiones desplegadas en producción, esta (que también estará sincronizada con origin/develop) contendrá el último estado de nuestro proyecto. Es decir, esta rama contiene todo el desarrollo del proyecto hasta el último commit realizado.

../../static/images/0007-entendiendo-git-flow/develop.png

Cuando esta rama adquiera estabilidad y los desarrolladores quieran lanzar una nueva versión, bastará con hacer un merge a la rama master (no de forma directa, ya veremos como hacerlo) y asignarle un número de versión mediante un tag.

Esto será lo que cree una nueva versión de nuestro proyecto. Recordemos que queremos ser bastante estrictos en cuanto a que el master solo alojará commits que supongan nuevas versiones. Nada más. Para ver el estado del desarrollo usaremos la rama develop.

Las ramas de features

Que varias personas trabajen sobre la misma rama es bastante caótico ya que se aumenta el número de conflictos que se dan. A pesar de que los repositorios distribuidos faciliten esta tarea al guardar los commits solo localmente, tiene mucho más sentido usar la potencia de las ramas de Git.

Cada vez que necesitemos programar una nueva característica en nuestro proyecto crearemos una nueva rama para la tarea. De hecho, lo utópico sería que todo el desarrollo se realizase en este tipo de ramas.

Estas ramas pueden tener cualquier nombre que no empiece por release-, hostfix-, ni master o develop. El nombre deberá reflejar el propósito de la rama (ej. form_registro_usuarios, i18n, bug_1134, ...).

Ya que develop contiene la última foto de nuestro proyecto, crearemos la nueva rama a partir de aquí.

$ git checkout -b bug_1134 develop

Una vez finalizada la tarea, solo tendremos que integrar la rama creada dentro de develop. Para esto usaremos un merge normal y corriente con un parámetro extra, --no-ff.

Este flag obliga a Git ha generar un commit para el merge. Con esto se evita que Git haga un fast-forward si es posible (es uno de los métodos para realizar merges, en los que se pierde la historia de la rama).

De esta manera, en todo momento en la historia del repositorio se tendrá constancia de que hubo una rama donde se desarrolló cierta funcionalidad, que commits contenía, y cuando se integró en el trunk.

../../static/images/0007-entendiendo-git-flow/feature.png

Una vez integrada la rama en develop, podremos eliminarla y actualizar origin.

$ git checkout develop
$ git merge --no-ff bug_1134
$ git branch -d bug_1134
$ git push origin develop

Por último, estas ramas no tienen por qué estar necesariamente subidas a origin, aunque si se quiere compartir ese desarrollo entre varios programadores, puede hacerse sin problemas.

Las ramas de lanzamiento

Como hemos dicho antes, el desarrollo del proyecto ha de realizarse en la rama develop, para posteriormente lanzar una nueva versión desde la rama master.

Esto no se hará directamente con un merge desde la rama develop a master, si no que se usarán las ramas de lanzamiento para este fin.

Este tipo de ramas sirve para poder liberar cuanto antes la rama develop para continuar el desarrollo, y alojará todos aquellos commits que son de preparación para el lanzamiento de la versión: cambiar el número de la versión en los ficheros, compilar la documentación necesaria, empaquetar librerías, etc.

Es decir, todo aquello que no tiene que ver directamente con el desarrollo, si no con el lanzamiento de la siguiente versión del proyecto.

La creación de esta rama será análoga a las de otros tipos, teniendo en cuenta que tendrá que prefijarse con el nombre release-, seguido del número de versión que tendrá.

$ git checkout -b release-1.2 develop

Una vez terminada la preparación de la siguiese, habrá que realizar un merge del HEAD de la rama release con el master. De esta forma se creará una nueva versión.

Por último, se creará un tag en el commit del master para marcar la versión.

../../static/images/0007-entendiendo-git-flow/release.png

Como se puede ver en el gráfico, los cambios no sólo se integrarán en el master, si no que también lo harán en la rama develop, de esta manera no perderemos los cambios realizados en la rama release, y se integrarán en el desarrollo. Por último, y dado que ya no hace falta, podemos borrar la rama creada.

Así pues, las instrucciones para finalizar una rama de lanzamiento son las siguientes:

$ git checkout master
$ git merge --no-ff release-1.2
$ git tag -a 1.2
$ git checkout develop
$ git merge --no-ff release-1.2
$ git branch -d release-1.2

Como detalle adicional mencionar que en las ramas de lanzamiento no se puede añadir funcionalidad al producto. Es una rama cuyo objetivo es únicamente el realizar el trabajo necesario para lanzar una nueva release.

Si en este proceso se detectase un bug nuevo, se corregiría en la rama de lanzamiento. Al finalizarla, como el trabajo se integrará en la rama develop, el commit que arregló dicho fallo se incluirá igualmente.

Las ramas de bugs urgentes

Y por último tenemos las ramas de bugs urgentes. Estas ramas son muy parecidas en funcionamiento a las ramas de lanzamiento. Su propósito es arreglar algún fallo en el desarrollo que necesita de una nueva release del proyecto inmediatamente.

Esta rama se creará a partir de master ya que únicamente queremos resolver el fallo (sin incluir nada del nuevo desarrollo realizado en la rama develop). Su nombre deberá prefijarse mediante hotfix- y una vez creada la rama simplemente se corregirá el fallo desde la misma.

$ git checkout -b hotfix-1.1.2 master

Una vez corregido el fallo tendremos que integrar el arreglo en el master. Para ello, y como siempre, se hará un merge y se creará un tag para indicar que se ha creado una nueva versión del proyecto.

También se tendrá que integrar la corrección del bug en la rama develop, ya que no queremos perder el fix en el desarrollo de futuras versiones.

../../static/images/0007-entendiendo-git-flow/hotfix.png

Los comandos necesarios para llevar a cabo la finalización de la rama hotfix son los siguientes:

$ git checkout master
$ git merge --no-ff hotfix-1.1.2
$ git tag -a 1.1.2
$ git checkout develop
$ git merge --no-ff hotfix-1.1.2

Hay que tener en cuenta una pequeña excepción, el caso en el que exista una rama de lanzamiento cuando creamos la rama hotfix.

En este caso, se hará todo exactamente igual con una salvedad. Y es que a la hora de integrar los commits de la rama hotfix, por un lado lo haremos al master y por el otro, en vez de a la rama develop, lo haremos a la rama de lanzamiento.

De esta manera, el bug se verá corregido en la nueva versión que está a punto de lanzarse, y, cuando esta se integre en develop (recordad que las ramas de lanzamiento se integran en el master y en develop al finalizarse), se verá corregido en el resto del desarrollo.

Las ramas de soporte

Estas ramas sirven para ofrecer soporte a largo alcance de una versión del proyecto. Podemos tener varias versiones en producción, las 1.x y las 2.x.

En caso de que no queramos matar el desarrollo de la 1.x porque aún nos es necesario, podemos usar este tipo de ramas para facilitar el mantenimiento de ambos desarrollos en paralelo.

En cualquier caso, el soporte de estas ramas está aún en proceso muy experimental, por lo que a fecha de este artículo es mejor no usarlas.

Resumiendo...

Como podéis ver aunque este flujo de trabajo es algo rebuscado, se adapta muy bien al trabajo real que se hace en muchos desarrollos.

En el próximo artículo veremos git-flow, una herramienta creada para facilitar el seguir este flujo de trabajo. Como habréis visto, las formas de crear y finalizar las ramas son siempre prácticamente iguales, y, aparte, en el caso de la integración de los cambios, hay que pasar el flag no-ff siempre al comando. git-flow nació para evitar errores en estos aspectos y para facilitar el día a día al seguir este flujo (ejecutar un solo comando para finalizar una rama, en vez de cinco por ejemplo).

Y por último, y como chuleta para tener siempre cerca, un resumen de los tipos de ramas:

Master

Cada commit indica una nueva versión del proyecto.

  • Se crea a partir de: -
  • Se integra en: -
Develop

Contiene el desarrollo del proyecto

  • Se crea a partir de: -
  • Se integra en: -
Feature branch

Cada rama contiene el desarrollo de una funcionalidad.

  • Se crea a partir de: develop
  • Se integra en: develop
Release branch (release-XXX)

Se realiza el trabajo necesario para lanzar una nueva versión.

  • Se crea a partir de: develop
  • Se integra en: develop y master
Hotfix branch (hotfix-XXX)

Se corrige un fallo urgente.

  • Se crea a partir de: master
  • Se integra en: develop y master

Podéis ver un resumen general de todo lo contado aquí en un pdf creado por el autor de este modelo. En el apartado de referencias (a continuación de esto), tenéis igualmente el enlace al post original, donde se explica todo muchísimo mejor que aquí, pero en un castizo inglés ;)

Las novedades de C++0x - (2º parte)

Y seguimos desmenuzando las novedades que traerá este nuevo estándar de C++. En este segundo artículo veremos muchas de las novedades que nos dejamos en el tintero anteriormente.

Para los que hayan llegado a esta página de primeras, les recomiendo que se lean antes la primera parte del artículo.

Funciones eliminadas y predeterminadas (default / delete)

A partir de ahora podemos especificar en las funciones que queremos que tengan el comportamiento normal (mediante la palabra clave default) o que queremos que no puedan existir (palabra clave delete).

Gracias a esto podremos decidir por ejemplo que tipo de sobrecargas no queremos permitir en nuestro código

struct A {
    M(long long);
    M(long) = delete; // Se prohibe la conversión automática
};

También, algo muy usual hasta ahora era querer prohibir la copia de los objetos. Gracias a esto podremos hacerlo de una manera muy cómoda, poniendo como delete el constructor copia, y el operador asignación.

class X {
    X& operator=(const X&) = delete;
    X(const X&) = delete;
};

De esta manera no se podrán copiar los objetos de la clase X. El método default es análogo, permitiendo especificarle al compilador que queremos que realice la acción predeterminada.

Expresiones constantes (constexpr)

Se permite especificar que expresiones o funciones son totalmente constantes en el código. Gracias a esto daremos la oportunidad al compilador de realizar mejores optimizaciones; así como de poder usar estas expresiones en más lugares. Como ejemplo típico, el estándar de C++ obliga a que los arrays sean declaraciones mediante expresiones constantes, por lo que algo como

int getNum();
int array[getNum()]; // C++ invalido

Es una declaración totalmente inválida, a pesar de que GCC sí lo permita (en este caso no sigue el estándar).

constexpr int getNum();
int array[getNum()]; // C++11 valido

Gracias a la palabra clave constexpr podemos especificar que definiciones son constantes.

Al hacerlo, el compilador impone bastantes restricciones para asegurarse de que la función es realmente constante, y que puede hallar su valor y sustituirlo sin más en las zonas donde sea llamada. Estas restricciones son, entre otras, que la función solo pueda llamar a una función que también esté declarada como constante, no se permite el uso de variables globales...

Introspección de tipos (decltype)

Es algo que ya existía en algunas implementaciones (GCC lo tiene mediante la extensión typeof) y a partir de ahora pasa a formar parte del estándar.

Gracias a la palabra clave decltype podremos averiguar el tipo de una variable, pudiendo usarlo para declarar otras de forma dinámica.

int numero;
decltype(numero) otroNum;

En el ejemplo anterior vemos como se usa decltype para averiguar el tipo de numero (que es un int) y para declarar otra variable del mismo tipo.

Nuevo tipo básico (long long)

Se añade un nuevo tipo básico al lenguaje.

long long num = 100LL;

Este tipo de datos que ya forma parte de C desde hace tiempo, y que g++ ya soportaba también, será ahora estandarizado. Se asegura que en C++11 tendrá al menos un tamaño de 64bits (8 Bytes).

Listas de inicialización

Se permite usar las llaves para inicializar cualquier tipo de estructura de C++. Hasta ahora, solo se podía hacer con las más simples. A partir de ahora, se permite con objetos, estructuras, objetos dentro de estructuras; y en general, con cualquier tipo de elemento.

map<string,vector<int>> ventas = {
    {"Juan", {10, 20, 30}},
    {"Paco", {20, 50, 100}},
    {"Miguel", {100, 150, 200}}
 };

Esto puede extenderse a la inicialización de casi cualquier tipo de dato. Gracias a esto se proporciona una forma unificada de inicialización en C++, priorizando esta sobre las otras formas de inicialización existentes hasta ahora.

Constructores llamables

Hasta ahora si teníamos dos constructores en una clase y queríamos que los dos hiciesen algo parecido teníamos que repetir el código en ambos.

Esto es cosa del pasado.

class Edad {
        int edad;
    public:
        Edad(int x) { if (edad < 0) edad = 0; else edad= x; }
        Edad() : Edad(18) {}
};

A partir de ahora, podemos realizar desde un constructor llamadas a otros constructores, permitiendo aglutinar en un mismo lugar el código que sea idéntico.

Inicialización de atributos

Los atributos de una clase no estáticos no podían declararse en el lugar de su definición, debido a que cada objeto dispone de una copia del mismo.

Esto cambia en la próxima versión.

class Edad {
    int e = 10; // Nuevo en C++11
    Edad : e(10) {}; // Analoga a la anterior
};

Como puede verse, se permite la inicialización de los atributos aunque no sean estáticos. Los dos métodos enseñados anteriormente (inicializar el atributo directamente, o desde el constructor) son totalmente equivalentes.

Aserciones a nivel de compilador (static_assert)

Se ha incluido una directiva para realizar aserciones. Estas consisten en evaluar una expresión, y en caso de que sea falsa, imprimir la cadena dada como segundo parámetro.

static_assert(sizeof(long)==8, "Requeridos enteros de 64bits.");

Lo curioso de estas, es que son a nivel de compilador. Es decir, que la expresión ha de ser totalmente constante, y se evaluará a la hora de compilar el código y no al ejecutarlo.

Funciones lambda

Usadas en muchos lenguajes de programación funcional, las funciones lambda permiten ser declaradas insitu, en un ámbito local (muy local), evitando tener que crear una nueva función para ello.

El formato de una función lambda en C++11 es el siguiente:

[captura](parametros)->valor_devuelto {codigo} // Esqueleto
[](int x, int y) -> int { return x + y; } // Suma dos parámetros

Suelen ser muy usadas en funciones como std::sort, donde, en este caso, uno de los parámetros es un puntero a función que define como realizar el ordenamiento del listado. En el siguiente ejemplo podemos ver como ordenamos una lista de números por su valor absoluto.

vector<int> v = {-10, 1,6,-30,-4,5}
std::sort(v.begin(), v.end(),
           [](int x, int y){return abs(x) < abs(y);});

Todos los componentes de las funciones lambda son bastante fáciles de entender, a excepción de posiblemente captura (que está vacío en los ejemplos que hemos hecho hasta ahora). Este campo indica la forma en la que son capturadas las variables del scope por la función lambda, pudiendo ser por valor o por referencia. Alguna de las posibilidades son:

[] // No se coge ninguna variable
[x, &y] // x se coge por valor, y por referencia
[=] // Se cogen todas por valor
[&] // Se cogen todas por referencia

En el siguiente ejemplo podemos ver como gracias a una función lambda realizamos la suma de todos los elementos de una lista, accediendo a la variable total por referencia, para que el valor no se pierda al terminar de ejecutarse la función lambda.

std::vector<int> lista = {1,2,3,4,5};
int total = 0;
std::for_each(lista.begin(), lista.end(),
                [&total](int x) {total += x;});

Por último, mencionar que las funciones lambda pueden guardarse bajo variables para poder ser usadas varias veces. Para simplificar esto, se puede hacer uso de la palabra auto tal como se explicó en la primera parte de este artículo.

Prohibición de excepciones (noexcept)

Se añade una forma de indicar cuando una función no puede lanzar una excepción. En caso de que la lance, en vez de propagarse, el programa finalizará con una llamada a la función terminate().

int func(const vector<int>& v) noexcept {
    vector<int> resultado(v.size()); // Posible excepción
    return 0;
}

Si la operación que crea el vector resultado no pudiese reservar suficiente espacio por falta de memoria, se lanzaría una excepción. Esta, en vez de propagarse hacia arriba en la pila de ejecución, conllevaría la terminación del programa.

Modificadores de métodos (override y final)

Se permite el uso de dos nuevos modificadores en los métodos. Influenciados por Java, tienen como objetivo evitar errores de código, en muchas ocasiones difícilmente detectables.

override por un lado obliga a que el método sobrescriba a otro, evitando que por accidente creemos uno nuevo, en vez de sobrescribir el deseado.

struct Base {
    virtual void f();
    void g();
    virtual void h() const;
};

struct Heredado : Base {
     void f() override; // Correcto, sobrescribe Base::f()
     void g() override; // Error, Base::g() no es virtual
     void h() override; // Error, Base::h() tiene otra cabecera
};

Como puede verse, en el segundo y tercer caso podría creerse que los métodos sobrescriben a los de la clase base, pero no es así. Gracias a override detectamos estos errores en tiempo de compilación.

Por otra parte final prohíbe que se pueda sobrescribir ese método en clases derivadas, haciendo que el compilador genere un error en caso de que suceda.

struct Base {
    virtual void f() final;
};

struct Heredado : Base {
    void f(); // Error, Base::f() es final
};

De esta manera evitaremos que por accidente podamos sobrescribir un método que no queremos.

Conclusiones

Y hasta aquí llegan las novedades que quiero comentar sobre esta nueva versión de C++. Como podéis ver, es una revisión muy profunda en muchos aspectos del lenguaje. Aún quedan muchas en el tintero que no he explicado por no hacer demasiado largo el artículo o no complicarlo demasiado, como las referencias r-values, la definición de los objetos básicos, externalización de templates...

Por último, y porque no están directamente relacionadas con el lenguaje, quedan fuera de este artículo toda la ampliación que ha recibido la librería estándar (STL) de C++. Se han creado nuevos contenedores, se han aumentado el número de algoritmos disponibles, se ha creado una API consistente para manejo de threads, expresiones regulares, punteros inteligentes... y muchísimo más.

Pero eso, señores, queda para otro artículo ;)

Las novedades de C++0x - (1º parte)

La historia del siguiente estándar de C++ bien podría llamarse la historia interminable.

C++ es un lenguaje de programación de propósito general, cuyo objetivo radica en añadir programación orientada a objetos a C.

Lejos de esto, decir que C++ es simplemente un C con objetos es quedarse muy lejos de la realidad. A lo largo de los años (y en sus dos estándares), se han ido añadiendo distintas funcionalidades al lenguaje que nada tienen que ver con la POO, pero que hacen más potente al lenguaje (templates, sobrecarga de operadores, manejo de excepciones, ...).

Y esta es su mayor virtud, y su mayor problema. La complejidad de C++ es muy alta debido al propio diseño del lenguaje y a todas las posibilidades que ofrece. Tanto que muchas empresas que lo usan deciden incluso obligar a usar solo un subconjunto del lenguaje para facilitar el desarrollo y sobretodo el mantenimiento de sus aplicaciones. Google, por ejemplo, descarta usar excepciones.

Pues la historia que nos atañe en esta serie de artículos es el desarrollo de C++0x, que es el próximo estándar de C++, que se empezó a desarrollar mucho tiempo ha, y que, como su propio nombre indica, se preveía que saldría durante la primera década de este siglo. Aunque no pudo ser así, hace apenas un par de meses (en Marzo de 2011) se anunció que el estándar ya estaba acabado, a la espera de aprobación final por parte de ISO, lo que significa que ya tenemos nueva versión de C++, llamada C++11, con muchísimas novedades que trataré de explicar.

En líneas generales esta nueva versión tiene como objetivos coger lo mejor de otros lenguajes (veremos características claramente influenciadas por C#, Java o Python), y simplificar el uso de este lenguaje (cosa que, a mi juicio, han conseguido muy malamente, la verdad).

Veamos las que más me han llamado la atención a mi (en un orden totalmente aleatorio).

Deduciendo tipos (auto)

Una de las primeras mejoras que han implementado en C++ (y que de hecho, ya tiene soporte actualmente en muchos compiladores) es la deducción de tipos.

C++ es un lenguaje tipado, y como tal, fuerza a declarar las variables y su tipo antes de poder usarlas. A partir de ahora, mediante la palabra auto, se nos permitirá no decir de que tipo es; siendo el compilador el encargado de averiguarlo a partir de la información del código fuente.

Esto, que lo más seguro es que provoque un montón de spaguetti code, puede llegar a ser muy útil en ciertas construcciones de C++ que son demasiado literales, y que solo complican la lectura del código. Como por ejemplo, la declaración de iteradores, que suele ser tan agradable a la vista como lo siguiente:

for (vector<vector<int>>::iterator i = v.begin();
            i != v.end(); ++i) {
     // Aquí hacemos algo interesante
}

A partir de ahora, y gracias a esta nueva funcionalidad, podremos hacerla de la siguiente manera

for (auto i = v.begin(); i != v.end(); ++i) {
     // Aquí hacemos algo interesante
}

Lo cual hace el código bastante más legible.

Como detalle adicional en el código presentado anteriormente, podemos ver otra de las nuevas funcionalidades de C++, que sin lugar a dudas provoca muchísimos dolores de cabeza actualmente.

Vemos que en el primer fragmento de código, no hemos tenido que separar los caracteres >. Actualmente es obligatorio, y lo que habría que escribir sería vector<vector<int> >. Esto ocurre debido a que los parsers actuales de C++ interpretan >> como el operador de desplazamiento, y no como la especialización del template que se está llevando a cabo.

A partir de ahora esto queda solucionado, y se podrán declarar variables de la forma vector<vector<int>> sin ningún problema.

Nuevos tipos enumerados (enum class)

Los enumerados de C y de C++ tienen dos problemas difíciles de resolver actualmente. El primero es que el nombre de los valores del enumerado se exportan al scope actual, lo que hace que dos enumerados totalmente distintos no puedan tener un valor con el mismo nombre.

Aparte, los enumerados son en el fondo enteros, lo que implica que dos enumerados totalmente distintos se puedan comparar entre ellos; o que, puedan ser comparables igualmente con un entero. Por último, el tipo subyacente que se usa para los enumerados no estaba definido por el estándar, por lo que era específica de la implementación de cada compilador.

Para resolver estos problemas se ha añadido un nuevo tipo de enumerado de tipado fuerte, que simula de cierta manera a los enumerados existentes en Java por ejemplo. A partir de ahora se nos permite especificar qué tipo queremos que tengan esos enumerados por debajo, y los distintos valores no se exportarán al scope actual, teniendo que prefijarlos con el nombre del enumerado para poder usarlos.

enum class Color : char {Amarillo, Azul, Negro};
enum class RGB {Rojo, Verde, Azul}; // No hay colisión con Azul

Color c = Color::Amarillo;
Color c2 = Azul; // FALLO: Azul no existe, sería Color::Azul

if (Color::Amarillo == 0) // FALLO: No comparable con un int
    printf("Blah!\n");

En este ejemplo podemos ver estos nuevos tipos de enumerados. El primero, Color, decidimos que tenga como tipo subyacente char, por lo que ocuparán 1B. Por su parte, el enumerado RGB no tiene tipo fijado, por lo que predeterminadamente será un int (usualmente 4B).

También podemos ver como no se genera un error aunque el valor Azul esté en ambos enumerados (esto provocaría un fallo en compilación con los enumerados normales), y por último, vemos como hay más restricciones a la hora de comparar los valores con otros tipos.

Bucle foreach (for (it : lista))

Cuando tenemos listados de elementos, una de las operaciones más básicas que podemos hacer es recorrer esa lista entera (con algún pretexto más o menos interesante). Para ello, lo más lógico actualmente es realizar un bucle for en el que dado el número de elementos que sabemos tiene la lista, empecemos a recorrerlos uno a uno.

Bien, la idea de esta nueva característica es emular a los bucles foreach de otros lenguajes como Java o C#. Es una extensión de los bucles for que facilita iterar por toda la lista, y que en cada paso te da una referencia al elemento que se está visitando en ese instante.

int numeros[] = {1,2,3,4,5};

// Incrementamos en +1 todos los elementos
for (auto it : numeros) // El tipo implícito es int&
    ++it;

El anterior es un ejemplo muy simple en el que vamos incrementando el valor de todos los números de la lista. Esta característica promete facilitar la lectura del código y hacerlo menos propenso a errores.

El nuevo puntero nulo (nullptr)

En C y C++ la constante NULL es representada por el valor numérico 0 (en C algo más complejo, ya que se castea a un void*). Esto genera problemas con las funciones o métodos sobrecargados, debido a que se dan comportamientos no esperados y muy difíciles de prever en el código.

void func(int n);
void func(char *n);

func(0); // Llamará a func(int)
func(NULL); // Llamará a func(int)

En este caso tenemos una función sobrecargada (puede recibir un entero o un carácter). El primer ejemplo vemos claramente que llamará a la primera función, ya que 0 es un entero normal y corriente. En cambio, el segundo ejemplo, que podríamos esperar que llamase a la segunda, no lo hace; y llama igualmente a la primera, o directamente falla al compilar por resultar ambigua (dependiendo de la implementación del compilador).

Para resolver este problema se ha añadido una constante llamada nullptr.

void func(int n);
void func(char *n);

char *c = nullptr;
char *c2 = NULL;

func(nullptr); // Llamará a func(char*)
bool t = (c == c2); // nullptr == NULL

Como podemos ver, esta constante soluciones los problemas generados en la sobrecarga y son totalmente compatibles con lo existente en C y C++.

Nuevas cadenas de carácteres

En C y C++ tenemos dos tipos de cadenas de caracteres. Los char, que representan caracteres de la tabla ASCII, y ocupan 1B; y los wchar_t, que se supone iban a facilitar el almacenamiento de caracteres Unicode. El problema es que el ancho no está definido en el estándar (en C por ejemplo wchar_t suele ser un alias de un int) y esto hace que no se puedan hacer implementaciones portables ya que no se asegura que tengan el suficiente ancho para alojar un carácter en UTF-8, UTF-16 u UTF-32.

Para resolver esta situación C++ crea dos nuevos tipos, char16_t y char32_t, que se usarán respectivamente para alojar caracteres de UTF-16 y UTF-32. De esta manera se asegura que estos tipos tengan un ancho suficiente (2B y 4B) para alojar los caracteres UNICODE.

const char *cad1 = u8"Esta cadena está en UTF-8";
const char16_t *cad2 = u"Esta cadena está en UTF-16";
const char32_t *cad3 = U"Esta cadena está en UTF-32";

Por último, y como podemos ver en el ejemplo anterior, podemos usar u8, u y U, para crear cadenas de caracteres en los tres sets dichos anteriormente.

Junto a estos tipos de datos se proporciona una nueva API con la que poder usarlos.

También se añade una nueva forma de escribir cadenas de caracteres en las que se toma literalmente cada carácter como es; es decir, no se escapan. Esto es muy útil a la hora de escribir expresiones regulares por ejemplo, donde cada vez que se escribe el carácter \, hay que escaparlo.

string c1 = "\\w\\\\\\w";
string c2 = R"[\w\\\w]";  // c1 y c2 son la misma cadena
string c3 = R"del(Buu!)del";  // La cadena es: Buu!

// La cadena es: Esta es una cadena raw y UTF-16
const char16_t *c4 = uR"*(Esta es una cadena raw y UTF-16)*";

Como se puede ver, para crear una raw string tenemos que empezar y terminar la cadena con el mismo delimitador (el cual puede ser cualquier cosa con como mucho 16 caracteres). La cadena resultante será la cadena completa, quitando estos delimitadores. Aparte, y como se puede ver en el último ejemplo, el uso es compatible con las cadenas Unicode que vimos anteriormente. Para marcar la cadena como raw string bastará con escribir una R previamente.

Y hasta aquí lo dejamos por hoy. En la segunda parte de la serie veremos el resto de novedades que nos proporcionará esta nueva versión de C++.

Listas Circulares en Linux

En este post vamos a ver uno de esos pequeños detalles que te hacen esbozar una sonrisa cuando los ves. Código que parece que literalmente, hace magia. Vamos a analizar una pequeñísima porción del código fuente de Linux.

A lo largo de las miles de lineas de código que tiene Linux se usan continuamente listas circulares. En el principio de los tiempos, cada programador se hacía su propia implementación, haciendo que el código fuese poco mantenible.

Hasta que llegado cierto momento en el desarrollo de la v2.1 se decidió realizar una implementación genérica, y portar todo el código existente a este nuevo sistema. Estas listas se usan actualmente en todo el kernel (lista de procesos, drivers, sistemas de ficheros soportados, ...). Pero antes de ver por qué el diseño que usan en Linux es tan bonito, hagamos una pequeña introducción sobre lo que es una lista circular, y como se suelen implementar.

Si todo esto ya te lo sabes, puedes saltar a la siguiente sección.

Listas Circulares

Las listas circulares son una de las estructuras de datos más básicas que existen. Consisten en una sucesión de elementos (una lista normal y corriente), en la que el último elemento apunta al primero, de tal manera que se puede recorrer de forma circular.

|filename|images/0004-listas-circulares-linux/lista-simple.png

Dentro de estas listas circulares, podemos subdividirlas mediante muchos criterios. Uno de los más usados es el número de enlaces que tiene. Las que son enlazadas simples (permiten recorrerse solo en un orden), y las que son doblemente enlazadas (a partir de un elemento, podemos visitar al anterior y al siguiente).

A diferencia de los arrays estáticos (como pudiese ser uno de C), nuestra lista circular no necesita saber cuantos elementos va a tener en tiempo de compilación, debido a que gracias al tratamiento de punteros y a la memoria dinámica del sistema, podremos ir variando su tamaño a nuestro antojo.

Típicamente las listas enlazadas se suelen implementar mediante un struct que contiene, aparte del elemento en cuestión, un puntero a una estructura de su mismo tipo, mediante el cual apuntar al elemento siguiente.

En caso de que quisiésemos implementar una lista doblemente enlazada, sólo tendríamos que añadir un puntero más, en este caso apuntado al elemento anterior.

struct nodo {
    int elemento;
    struct nodo * siguiente;
};

Así de simple. Para usar esta lista, nos bastaría con crear un par de elementos e iniciar sus atributos de forma correcta.

Como con una lista se suelen realizar tareas muy comunes (añadir, eliminar, iterar, ...) lo lógico sería crear un par de funciones que nos facilitasen la tarea.

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

typedef struct nodo {
    int elemento;
    struct nodo * siguiente;
} nodo;

int add_nodo(nodo **l, int e) {
    nodo *nuevo = (nodo *) malloc(sizeof(nodo));

    if (!nuevo)
        return -ENOMEM;

    nuevo->elemento = e;
    nuevo->siguiente = *l;
    *l = nuevo;

    return 0;
}

void imprimir_lista(nodo *l) {
    printf("Los elementos de la lista son: ");

    while (l != NULL) {
        printf("%d, ", l->elemento);
        l = l->siguiente;
    }

    printf("\n");
}

int main(void) {
    nodo *l1 = NULL;

    add_nodo(&l1, 10);
    add_nodo(&l1, 20);
    add_nodo(&l1, 50);
    imprimir_lista(l1);

    // No estamos liberando la memoria solicitada... ¬¬
    return 0;
}

En este código podemos ver como tenemos dos funciones (es un ejemplo muy escueto).

La primera, add_nodo, nos sirve para añadir una elemento a la lista circular. Este elemento se reserva en el heap del sistema, usando para ello la llamada malloc(3). Una vez reservada la memoria, se introduce delante del nodo que le pasamos como parámetro.

La segunda, imprimir_lista, simplemente itera a partir de un nodo recorriéndolos todos hasta llegar al pasado como parámetro de nuevo.

Esta implementación, que es muy parecida a la que se sigue en muchísimos casos, tiene como problema que no se puede generalizar la solución para otros tipos de datos.

Es decir, en nuestro anterior ejemplo, los elementos que guardamos son enteros (en la estructura que identifica nuestra lista podemos ver como el elemento es un int). Si queremos crear otra lista con otro tipo de elementos, tendremos que implementar un nuevo struct (con otro nuevo set de funciones para añadir, eliminar, iterar...).

Como podéis ver, este es un trabajo muy repetitivo, que al final hace que se repita muchísimo código, con pequeñas diferencias.

Formas de solucionar esto hay varias.

Por ejemplo, podríamos usar punteros genéricos, lo que nos permite guardar en la lista cualquier tipo de objeto.

Básicamente consiste en guardar un puntero a algo (void*), de tal manera que al añadir un elemento en la lista no guardaremos el elemento en sí, si no un puntero al mismo. Debido a que un puntero siempre ocupa lo mismo (independientemente del tipo del dato apuntado), este sistema nos permitiría guardar cualquier tipo de dato.

Esta solución, sin embargo, aunque arregla este problema, crea otros tantos, como que hay que castear el objeto antes de poder usarlo, o que al pasar a guardar un puntero del objeto, en vez del objeto en si, tenemos que tener cuidado con que a lo largo de la vida de la lista ese objeto no deje de existir (porque fue creado en la pila de otra función por ejemplo).

La solución de Linux

Linux implementa una lista doblemente enlazada circular.

|filename|images/0004-listas-circulares-linux/lista-circular.png

Aparte, no existe el concepto de primer y último nodo. No le dan ningún tipo de importancia a esta información. Facilitan formas de recorrer la lista entera, independientemente de que nodo cojamos como cabecera de la lista.

Pero lo que hace especial a la solución de Linux es que pasa por embeber la propia lista dentro de nuestra estructura... y no al revés; es decir, le dan la vuelta a la tortilla.

En vez de tener nuestro elemento guardado dentro del struct, lo que se hace es meter la estructura que representa a un nodo, dentro de nuestro propio dato.

Así explicado suena un poco raro, así que pasemos a ver código real. El struct que representa estas listas se llama list_head. Y si miramos como está implementado veremos que tiene únicamente dos elementos:

struct list_head {
    struct list_head *next, *prev;
};

Un puntero al siguiente elemento, y otro al anterior (como vimos anteriormente).

Para usar estas listas únicamente tendremos que insertar un elemento struct list_head en nuestra propia estructura. Vamos a realizar un ejemplo parecido al anterior, pero ahora usando las listas circulares de Linux.

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include "list.h"

typedef struct {
    struct list_head lista;
    int elemento;
} nodo;

int main(void) {
    struct list_head *iter;
    nodo n_head, *aux;

    // Inicializamos la lista
    INIT_LIST_HEAD(&n_head.lista);

    for (i=0; i<5; i++) {
        aux = (nodo*) malloc(sizeof(nodo));

        if (!aux)
            return -ENOMEM;

        aux->elemento = i;

        // Añadimos un elemento más a la lista
        list_add(&aux->lista, &n_head.lista);
    }

    // Recorremos todos los elementos
    printf("Los elementos de la lista son: ");

    list_for_each(iter, &n_head.lista) {
        // Extraemos nuestro struct de la lista
        aux = list_entry(iter, nodo, lista);
        printf("%d, ", aux->elemento);
    }

    printf("\n");

    // Volvemos a olvidarnos de liberar la memoria
    return 0;
}

Es un ejemplo muy sencillo en el que podemos ver la potencia de la API de estas listas. Como podemos ver, tenemos una estructura nodo, que aparte de guardar nuestro elemento (igual que en el caso anterior), contiene un elemento de tipo struct list_head, que será el que guarde la referencia a nuestra lista circular.

Mencionar, que se pueden insertar tantos list_head como se quiera; es decir, que un mismo elemento puede pertenecer a infinitas listas circulares.

En este caso, creamos un nodo que usaremos como cabecera de la lista (aunque como ya dijimos, para estas listas todos los nodos tienen la misma importancia), y a partir de él, inicializamos la lista con la macro INIT_LIST_HEAD.

A continuación, con list_add, añadimos un total de 5 elementos. Esta macro recibe dos parámetros. Como primer parámetro, el nodo a insertar. Y como segundo, el nodo tras el cual queremos insertar el elemento.

Y por último, imprimimos los números insertados. Especial atención a las dos macros usadas aquí. list_for_each, que se expande a un bucle for normal y corriente (con prefetching), que a partir del pivote usado (el segundo parámetro) empieza a recorrer todos los nodos de la lista (guardándolos en el iterador, el primer parámetro) hasta llegar al nodo pivote de nuevo.

Y list_entry, que es la macro que hace realmente la magia (la analizaremos con más calma después). Gracias a esta macro conseguiremos extraer el elemento (nuestro struct nodo), a partir del list_head que tenemos guardado en la variable iter.

La API de estas listas es muy rica, proporcionando funciones (macros realmente) para insertar elementos antes o después de nuestro pivote, para recorrerlas del derecho o del revés, para activar o desactivar el prefetching, ...

Es un módulo a usar siempre que necesitemos listas, independientemente de que estemos desarrollando para el kernel o no (todo el código está contenido en un fichero que no tiene dependencias externas).

¿Y cómo demonios hace eso?

Pasemos a ver un poco el código del kernel. La implementación de las listas circulares la podemos ver en el fichero include/linux/list.h

Merece la pena leer todo el código de la librería, llamando poderosamente la atención una cosa. ¿Cómo consigue la lista devolver la estructura contenedora a partir de la referencia a su propio campo? Es decir, y usando nuestro ejemplo anterior... ¿Como puede C, a partir de una referencia al struct list_head, sacar el struct nodo en el cual está contenido?

La encargada de ello es la función list_entry, que a parte de que resulta ser una macro, tiene un código muy sencillo. Únicamente llama a la macro container_of.

Esta macro es la que realiza toda la magia (no solo se usa para las listas circulares), y podemos encontrarla desarrollada en el fichero include/linux/kernel.h.

Recibe tres parámetros (los mismos que list_entry):

ptr
El puntero al campo de la estructura.
type
El tipo de estructura que queremos sacar (a la cual pertenece el campo anterior)
member
El nombre del campo que estamos pasando desde el primer parámetro.

Analicemos paso a paso el código de la macro container_of.

#define container_of(ptr, type, member) ({          \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

Es muy poco código (apenas dos líneas), pero veremos como condensa mucha información.

Para empezar, la estructura principal es bastante rara. Contiene dos instrucciones rodeadas mediante un par de paréntesis y llaves. Para entender que es esta estructura, vamos a poner un ejemplo muy simple

#include <stdio.h>

int main(void) {
    int num = ({int i = 0; i+100;});
    printf("%d\n", num);
    return 0;
}

Como podemos ver, en la línea 4 se ejecutan dos instrucciones, y finalmente el printf imprime por pantalla el valor 100. Pues esta extraña estructura no es más que una extensión de GCC (sí, el código de Linux no es, por lo general, portable a otros compiladores), llamada Statement Expression

Básicamente, permite ejecutar varias expresiones como si fuesen una sola. La última de la lista, que deberá terminar en un punto y coma, será el valor que devolverá la expresión.

En el ejemplo anterior, podemos ver como inicializamos una variable llamada i, y a continuación le sumamos a su valor (que es 0), el número 100. El resultado de esta operación es la que se devolverá al entero num. Es decir, que valdrá 100.

Pues análogamente, nuestra macro container_of contiene dos instrucciones con este formato, de tal manera que en la primera instrucción creamos una variable, y en la segunda operamos con ella, devolviendo como valor de la expresión el resultado de esta operación.

Pero antes de ver que operación se hace concretamente sigamos viendo con calma el código de la macro, en concreto podemos ver como se hace uso de otras dos funciones: typeof y offsetof.

La primera, typeof, no tiene mucho misterio, es otra extensión de GCC que en base a una variable, nos devuelve el tipo del que es. De esta manera podremos crear una variable del mismo tipo que otra, aunque no sepamos a priori de que tipo es la primera.

La segunda, offsetof, es una macro definida en el estándar ANSI C, pero que el kernel redeclara. Esto es debido a que esta macro realiza operaciones que no están aseguradas por el estándar de C, si no que el resultado puede variar de un compilador a otro.

Por ello, la mayor parte de los compiladores definen una función (por lo general llamada __builtin_offsetof). Linux simplemente comprueba si tal función existe; y en caso afirmativo, la usa sobre la predeterminada.

Lo que hace esta macro es devolver el offset (la distancia) que separa un campo de un struct, del inicio del mismo.

El código suele ser parecido al siguiente:

#define offsetof(st, m) \
     ((size_t) ( (char *)&((st *)(0))->m - (char *)0 ))

Esta macro realiza las siguientes operaciones:

  1. Castea el elemento 0 (NULL), a un puntero del tipo de estructura requerido, y a continuación accede al elemento especificado. Por último, obtiene la dirección. Es decir, devuelve el número de bytes que hay entre el inicio de la estructura (que está en la posición 0 en este caso), y el elemento que queremos devolver.
  2. A continuación, hace lo propio con el valor 0.
  3. Castea ambos elementos a punteros a carácter. Esto es debido a que el estándar de C asegura que un carácter ocupa 1 Byte.
  4. Por último, resta ambos valores, obteniendo el offset del campo que queremos (el número de bytes que lo separa del inicio de la estructura).

Con estas aclaraciones ya estamos listos para entender que hace la macro container_of. Vamos a volver a poner el código para tenerlo más cerquita.

#define container_of(ptr, type, member) ({          \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

Como vemos, concatena dos expresiones en una.

  • La primera crea un puntero a una variable (llamada __mptr) de la estructura interna, list_head en nuestro ejemplo. Para ello, accede al miembro (lista en nuestro caso), y a continuación usa typeof para crear una nueva variable de este tipo. A continuación la inicializa con el valor del primer parámetro, que recordad, en el caso de las listas circulares, es el elemento sobre el que estamos iterando.

    Es decir, si en cierto momento de la ejecución de nuestro código anterior llamamos a la macro container_of(&nodo1, nodo, lista) pasándole como primer parámetro la dirección del nodo1, la primera línea lo que hará será crear una variable de tipo list_head, que apunte al elemento lista de ese nodo1. Esa variable, a grandes rasgos, guarda la dirección de memoria donde está el elemento list_head de nodo1 (un puntero no es más que una dirección, un numerito).

  • La segunda, obtiene el offset del campo list_head en nuestra estructura (con la macro offsetof tal como explicamos anteriormente), y a continuación, le resta este valor a la dirección de la variable creada anteriormente. Por último, castea este valor a un puntero a nuestro tipo de estructura.

Y el valor devuelto es el resultado de esta última operación, que no es más que la dirección de memoria donde empieza nuestra estructura nodo.

Es decir, que, poniendo números concretos, la primera línea de esta macro devuelve la dirección donde está la estructura contenida (la 0x200 por ejemplo). Y la segunda, obtiene el offset de esta estructura contenida, sobre la exterior (un offset de 0x4 bytes por ejemplo, que es lo que suele ocupar un int).

|filename|images/0004-listas-circulares-linux/struct.png

Para terminar, si queremos acceder a la estructura exterior, no tendremos más que restar 0x4 a 0x200, dando 0x1FC, que es la dirección de memoria donde está la estructura exterior.

Y eso es todo... como dije al principio, auténtica magia :)