El nuevo estándar de C, C11

Este año que termina nos deja con la actualización de dos grandes lenguajes de programación: C++ (vimos sus cambios en un artículo de dos partes y C, que lanzó C11 hace apenas dos semanas, con no demasiadas novedades que tratan de estandarizar características que muchos compiladores incluyen ya desde hace tiempo, y añadir una serie de mejoras al manejo de threads o de operaciones atómicas, así como añadir más compatibilidad con C++ (y en específico con su nueva versión, C++11).

Veamos algunos de los cambios con más detenimiento...

Funciones sin retorno (_Noreturn)

Se añade un modificador nuevo que permite al compilador optimizar el código de una función en caso de que nosotros especifiquemos que nunca va a retornar al llamante.

#include <nostdreturn.h>

void noreturn crash() {
    printf("Uops! :D\n");
    exit(-1);
}

Podemos usarlo en funciones que típicamente tratan casos de error que finalizan el programa por ejemplo.

Aparte, se añade un nuevo fichero de cabeceras, nostdreturn.h, donde se define la palabra clave noreturn como alias de _Noreturn.

Eliminada la función gets

Se elimina finalmente la función gets de la librería de C.

Esta función lee desde la entrada estándar un número indeterminado de carácteres (hasta un fin de línea). El hecho de que no se pueda especificar el máximo provoca que sea muy sencillo provocar un desbordamiento del buffer donde se están guardando esos carácteres.

#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Para evitar los enormes problemas de seguridad que esto conlleva, en este nuevo estándar se elimina la función (ya en C99 estaba des-recomendada), y se aconseja el uso de la nueva función gets_s, que posee la misma cabecera, añadiendo un parámetro en el que se especifica el número máximo de carácteres a leer (menos 1, para el \0).

Mayor soporte de cadenas unicode

Al igual que en C++, y de una forma compatible con él, se añaden nuevos tipos de datos y aliases para manejar cadenas unicode. Se da soporte a tres representaciones distintas de unicode: UTF-8, UTF-16, UTF-32.

Hasta ahora estos formatos se representaban usualmente mediante los tipos char, unsigned short y unsigned long. El problema es que el tamaño de esos tipos de datos es dependiente de la plataforma y el compilador, por lo que no se podía hacer código portable.

Este nuevo estándar (al igual que el de C++), crea dos nuevos tipos de datos, llamados char16_t y char32_t, creados con el objetivo de representar carácteres en UTF-16 y UTF-32 respectivamente. UTF-8 seguirá usando un char normal. Además, se permite crear literales en las tres representaciones mencionadas marcando la cadena de carácteres con uno de los siguientes tres prefijos: (u8, u, U).

Por último, se añaden nuevas funciones en la librería estándar (almacenadas en uchar.h) para realizar conversiones entre los distintos formatos.

Si quieres más información sobre esto puedes consultar la primera parte del artículo sobre C++11.

Aserciones estáticas (static_assert)

Al igual que C++11, esta nueva versión de C añade una nueva palabra clave, static_assert, que permite añadir aserciones en el código que se ejecutan a nivel de compilador.

Las aserciones se encargan de comprobar condiciones que tienen que cumplirse a la hora de ejecutar una sección de código. Lo que hace especiales a estas aserciones es que no se ejecutan en tiempo de ejecución (cuando el programa es ejecutado por el usuario), si no que son comprobaciones realizadas en la fase de compilación (en una fase tardía donde los tipos son conocidos).

static_assert(sizeof(void*) >= 8, "No eres de 64-bit :P");

Este tipo de aserciones completa a las dos existentes anteriormente. La que se ejecuta con el preprocesador, #error, y la que se ejecuta en tiempo de ejecución, assert().

Funciones con comprobación de fronteras

El título de esta sección queda un poco críptico (no sabía muy bien como traducirlo), pero no es nada nuevo. El nuevo estándar de C añade una serie de funciones a la parte de su librería estándar encargada de manejar cadenas de carácteres. Estas nuevas funciones, cuyos nombres terminan mediante el sufijo _s, se diferencian de sus hermanas por comprobar los límites de los arrays usados como buffers antes de realizar las operaciones pertinentes.

char* strcat(char *s1, const char *s2);
errno_t strcat_s(char *s1, size_t n, const char *s2);

Como puede verse, la diferencia entre strcat y strcat_s es el segundo parámetro, que indica el número máximo de elementos que puede albergar el buffer de destino. Gracias a ese parámetro la función puede hacer más comprobaciones en tiempo de ejecución, comprobando que no se sobrepase el límite de memoria que tiene asignado el buffer de destino, por ejemplo.

Al igual que para strcat, se han añadido versiones con comprobación de fronteras para otras tantas funciones de la librería estándar.

Estructuras anónimas

Tras ser durante mucho tiempo soportadas en C++ se añade a este nuevo estándar el soporte de estructuras y uniones anónimas.

Estas estructuras son idénticas a las normales, salvando que no tienen un nombre mediante las que ser identificadas. Son muy útiles, por ejemplo, para crear código menos verboso a la hora de incorporar estructuras anidadas.

struct Persona {
    char *nombre;
    union {
        long dni;
        char *pasaporte;
    }
}

struct Persona p1;
p1.dni = 7987271;

Como puede verse en el fragmento de código anterior, al ser el unión anónimo, para acceder a sus campos no es preciso anteponer ningún nombre, dejando un código más claro y legible.

Manejo de threads

Este es posiblemente el cambio más grande que nos da este nuevo estándar. Se ha añadido el soporte a threads en la librería estándar, sin necesidad de usar librerías externas como hasta ahora con, por ejemplo, los threads POSIX (pthread.h).

Se ha añadido un nuevo fichero llamado threads.h en el que han incluído nuevos tipos de datos y funciones para ayudar a programar de forma concurrente. Se incluye el manejo de threads, así como de mutex y variables condicionales (para sincronización).

Dejamos el uso de esta librería para otro artículo separado, que hay mucho de lo que hablar... :)

Macros genéricas

Se añade algo con el mismo olor (salvando las distancias) a los templates de C++. Se pueden crear macros genéricas, que permitan seguir uno u otro camino con respecto al tipo de los parámetros recibidos.

#define cbrt(X) _Generic((X), long double: cbrtl, \
                            default: cbrt, \
                            float: cbrtf)(X)

Este es el típico ejemplo para demostrar el funcionamiento de estas macros. La familia de funciones cbrt se encarga de hallar la raíz cúbica de un número, existiendo tres distintas funciones dependiendo de si el parámetro pasado es un long, un int o un float. Gracias a las nuevas macros genéricas podemos crear una macro que traduzca la llamada a la función correcta dependiendo del tipo del parámetro pasado.

Y hasta aquí...

Y poco más que contaros en este artículo. No son todas las novedades que da este nuevo estándar, pero si que son las más importantes a mi modo de ver. Si investigais un poco más podréis ver como se añaden igualmente ciertas funciones para facilitar el alineamiento de datos, así como para finalizar de forma segura un programa, mejor manejo de números complejos, ...

¡Feliz año nuevo para todos! :D

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 :)