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