jueves, 16 de agosto de 2012

C++0x + Debian 6

¿Es posible aprovechar algún nuevo recurso de C++, discutido e incluido en el nuevo estándar conocido inicialmente como C++0x y luego rebautizado como C++11, usando gcc/g++ 4.4.5, que es la predeterminada en Debian 6? Responder esta pregunta es el objetivo de este artículo.

Primero, la respuesta corta: sí, es posible.

Luego, la respuesta larga. En este sitio, podrán encontrar las características implementadas en la versión 4.4 del compilador GNU gcc. Pero ver sólo la página no dice mucho, por lo que nos entretuvimos en identificar, comprender y verificar las más atractivas.

  1. Variables con declaración automática de tipo (auto).
  2. Inicialización de colecciones.
  3. enum ahora fuertemente tipado.
  4. Permitidos varios caracteres > seguidos.
  5. Referencias a rvalue.
  6. Manejo de funciones predeterminadas.

Para ello construimos un proyecto de pruebas unitarias usando el framework lite UnitTest++. La compilación con g++ debe realizarse con la opción -std=c++0x para habilitar las nuevas extensiones del lenguaje.

Variables con declaración automática de tipo

Cuando necesitamos declarar una variable, ya sea en C o C++ es necesario especificar su tipo y de manera opcional su valor inicial. Una variable declarada como auto deduce su tipo a partir del tipo de la expresión con que se inicializa. Veamos la prueba:

#include <UnitTest++.h>
#include <string>
#include <iostream>
#include <vector>

using namespace std;

struct A
{
 int p;
};

TEST(Use_of_auto_type_supported)
{
 auto i = 5;
 int a = i + 5;
 CHECK_EQUAL(10, a);
 
 auto s = string("a");
 string b = s + "b";
 CHECK_EQUAL(0, b.compare("ab"));
 
 auto t = new A;
 t->p = 5;
 A sta;
 sta = *t;
 CHECK_EQUAL(5, sta.p);
 
 auto d = 5.6;
 double dd = 4.4 + d;
 CHECK_CLOSE(10, dd, 0.01);
 
 auto c = 'a';
 char cc[1];
 cc[0] = c;
 CHECK_EQUAL('a', cc[0]);
}

Como se puede ver, la variable i es de tipo int, s es string, t es un apuntador a la estructura A, d es un double y c un char. Cada tipo ha sido deducido a partir de la expresión de inicialización. Sin embargo, una expresión como la siguiente:

 auto p = null;

es inválida pues null no es expresión que defina un tipo particular. Con este recurso resolvemos los tediosos bloques como el siguiente:

for (vector<a_big_name_type>::const_iterator iter = collection.begin();
   iter != biometricTypes.end();
   ++iter)
{
 ...
}

Ahora podemos escribir:

for (auto iter = collection.begin(); 
   iter != biometricTypes.end();
   ++iter)
{
 ...
}

Para más información, ver este documento.

Inicialización de colecciones

Desde C# 3.0 se tiene algo como esto. Pues ya en C++ es perfectamente posible:

TEST(Init_lists)
{
 vector<int> a {1, 2, 3, 4 ,5};
 vector<string> s {"1", "2", "3"};
 CHECK_EQUAL(3, a[2]);
 CHECK_EQUAL(0, s[1].compare("2"));
}

El vector a contiene 5 números, desde el 1 al 5, mientras que s tres cadenas. Para más información, ver este link.

enum ahora fuertemente tipado

Desde tiempos immemoriales, los nombres de los campos de dos enumeraciones no podían coincidir. Es decir, que esto era ilegal:

 enum A { A1, A2 };
 enum B { A1, B1 }; // Ilegal, nombre A1 repetido.

Por lo general se resolvía el problema poniéndoles prefijos a los nombres de los campos, y así no "colisionaran" unos con otros. Pero dicha técnica no era otra cosa que un disimulado parche. Para resolver problemas de nombres, asignación/conversión directa a enteros, tamaño en memoria de los enum y otros, se tiene en C++ 11 lo siguiente:

enum class E1 : char { Vale1, Vale2 };     // No hay error de sintaxis
enum class E2 : uint32_t { Vale1, Vale2 }; // No hay error de sintaxis

TEST(Strongly_typed_enums)
{
 E1 a = E1::Vale1;
 E2 b = E2::Vale1;
 // int c = E1::Vale1; // Ilegal, imposible asignar enum a int.
 CHECK_EQUAL(1, sizeof(a));
 CHECK_EQUAL(4, sizeof(b));
}

Para que sea fuertemente tipada, enum debe sucederse con la palabra reservada class, seguida de dos puntos y el tipo cuyo tamaño en memoria tendrá la nueva enum. Para más información, ver este documento.

Permitidos varios caracteres > seguidos

Esta posibilidad es bien sencilla, pero que molestaba un poco a los que trabajamos con tipos genéricos, STL y demás. Y es que ya se puede hacer:

TEST(Right_angle_brackets)
{
 vector<vector<int>> v;
 v.push_back(vector<int>());
 v.push_back(vector<int>());
 v.push_back(vector<int>());
 CHECK_EQUAL(3, v.size());
}

En la declaración del vector v no hay error de sintaxis, pues el doble angular >> ya no provoca error en tiempo de compilación al no ser detectado como si fuera operador binario. Este operador de redirección se usa por ejemplo en la segunda sentencia del listado siguiente:

 char a;
 cin >> a;

Para indicarle al objeto cin contenido en el namespace std y parte de la STL que la entrada estándar será almacenada en la variable a. Para más información, ver este link.

Referencias a rvalue

Con la concepción del lenguaje C++, se introdujo el uso de refencias para permitir usar un nombre de variable por otro sin necesidad de recurrir a la indirección (apuntadores). Tomemos el siguiente ejemplo:

 void a(int* p) { *p = 5; }
 void b(int& p) { p = 5; }
 ...
 int myp = 0;
 a(&myp);
 b(p); 

Ambas funciones, a y b producen el mismo efecto, sin embargo a es mucho más ineficiente. Para poder cambiar el valor de p a 5, la función a necesita un apuntador a una variable int y especificar que se cambiará el valor apuntado. Por otro lado, cuando se pasa myp a a debe aplicarse una indirección (operador &). Sin embargo, la función b solo le da un "alias" a la variable myp en su parámetro formal p y lo modifica sin tanto trauma.

Sin embargo, no es posible pasar por referencia un rvalue (expresión a la que no puede asignársele nada). Es decir, lo siguiente es ilegal:

 void b(int& p) { p = 5; }
 int myp() { return 6; }
 ...
 b(myp());

Con las referencias a rvalue, denotadas con el operador &&, es posible hacerlo:

string foo() { return "a"; }

TEST(Rvalue_reference)
{
 // string& f1 = foo();  // Error de sintaxis
 string&& f2 = foo(); // Se mueven los campos del string devuelto por foo, no se copian!
 string f3 = foo();   // Se copian los campos del string devuelto por foo
 CHECK_EQUAL('a', f2[0]);
}

Aunque la lógica sea similar, por debajo lo que se realiza es "mover" la memoria generada por lo que devolvió foo() hacia f2. En el caso de f3 se hace una copia campo por campo del string devuelto, por lo que es ineficiente. Para más información, ver este link.

Manejo de funciones predeterminadas

C++ por defecto brinda cuatro funciones miembro de clases/estructuras:

  • destructor
  • constructor por defecto
  • constructor de copia
  • operador de asignación por copia =

Además, también proporciona otros operadores como new y delete, por ejemplo. Sin embargo, cuando el usuario define un constructor propio, el por defecto desaparece, por lo que habría que implementarlo. Esta implementación es muy posible sea menos eficiente que la que brinda el compilador. Es por ello que se propone lo siguiente:

struct type
{
    int a;
    type() = default; // la reduncia ahora es eficiente
    type(int a) { this->a = a; } // Otro constructor
    virtual ~type(); // virtual requiere una declaración
    type( const type & ); // una simple declaration
};
inline type::type( const type & ) = default; // ahora eficiente
type::~type() = default; // una definición por defecto no inline

struct type2
{
    type2 & operator =( const type2 & ) = delete;
    type2( const type2 & ) = delete;
    type2() = default;
};

TEST(Default_And_Delete_Func)
{
 type t1;
 t1.a = 5;
 type2 t2, t3;
 // t3 = t2; // Inválido, el operador = se ha eliminado
 // type2 t4(t2) // Inválido, el constructor copia se ha eliminado
 type t5(t1); 
 CHECK_EQUAL(5, t5.a);
}

Como se puede ver en caso del tipo type, se han mantenido los constructores y el destructor por defecto, de manera que se puede crear otros constructores sin problemas.

El caso de type2 es otra nueva funcionalidad: eliminar las definiciones del constructor copia y de la asignación por copia (aunque se puedan eliminar otras funciones y sobrecargas de operadores), de manera que las operaciones con el tipo quedan prohibidas. Para más información, ver este link.

Conclusiones

Con estos recursos de nuestro lado, ganamos mucha del azúcar sintáctica y capacidades de las que hoy gozan C# y Java, nada más y nada menos que en C++. Existen otras nuevas mejoras, como las expresiones lambda, tan populares hoy en ambientes .NET, pero vienen en versiones más modernas del compilador g++ (ver aquí), por lo que no han sido abordadas.

También brindamos el vínculo para conocer cuál es el estado del desarrollo de las nuevas funcionalidades en la STL y una referencia online actualizada de los lenguajes C/C++.

No hay comentarios:

Publicar un comentario