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

Comentarios