lunes, 12 de septiembre de 2011

decltype: ¿De qué tipo es esa expresión?

En mi anterior post sobre C++11 vimos que el uso de auto permite deducir el tipo de una variable, siempre que ésta se vaya a iniciar en el momento en que se define. Esto cubre un cierto número de casos, pero no todos.

hora obten_hora_actual() ;
coordenada obten_posicion_actual() ;

std::map<hora , coordenada> m;
m[obten_hora_actual()] = obten_posicion_actual();
//...

Bien. Parece que no habrá problemas. Bueno, realmente no habrá problemas mientras no se modifique el tipo de retorno de las funciones obten_hora_actual() y obten_fecha_actual. Lo que realmente me gustaría expresar es que m es un mapa que usa como tipo para la clave el tipo de retorno de la primera función y como tipo para el valor el tipo de retorno de la segunda función.

Aquí aparece en nuestra ayuda el operador decltype. Este operador, permite obtener el tipo de una expresión y se puede usar en cualquier contexto en el que se pueda usar un tipo.

hora obten_hora_actual();
coordenada obten_posicion_actual();

std::map<decltype(obten_hora_actual()),
 decltype(obten_posicion_actual())> m;
m[obten_hora_actual()] = obten_posicion_actual();
//...

Originalmente, algunos fabricantes habían implementado una extensión con un operador parecido: typeof. Sin embargo, la semántica de este operador era distinta, y el comité de ISO C++ decidió optar por una palabra reservada distinta. La elección de decltype puede parecerte poco afortunada, pero era la opción que menos afectaba al código ya existente.

Reglas para la evaluación de decltype

La expresión decltype(expr) se puede utilizar en cualquier tipo donde se pueda usar un especificador de tipo. De esta manera, se puede escribir el siguiente código:

int x;
long y;
decltype(x+y) z; // z es long

En este caso la expresión decltype(x+z) equivale al tipo long, puesto que el resultado de sumar un int y un long es un long.

En el caso de que la expresión pasada a decltype sea una variable, la regla es ligeramente distinta y el resultado es el tipo con el que se declaró la variable:

int x;
int & rx = x;
decltype(x) y = x; // int y
decltype(rx) ry = x; // int & y

Esta regla, hace que en este caso la deducción de tipos no funcione exactamente igual con auto que con decltype:

int x;
int & rx = x;
auto y1 = rx; // int y1. y1 es una copia de x
decltype(rx) y2 = rx; // int & y2 = rx. y2 es una referencia a x

Esto también es aplicable a los parámetros de una función:


template <typename T> class X { /*...*/};
void f(int x1, int & x2, const int & x3) {
  X<decltype(x1)> z1; // X<int> z1;
  X<decltype(x2)> z2; // X<int&> z2;
  X<decltype font="" int&>="" x>
  /*...*/
}


También se puede utilizar decltype sobre una invocación a una llamada a función. Es importante tener en cuenta que una invocación a función dentro de decltype no realiza una llamada a la función. Su único objetivo es determinar el tipo de retorno de la función.

string obten_valor(const string & clave, int indice);
void imprime() {
  list<decltype(obten_valor("usuario",0))> l;
  for (int i=0;i
    l.push_back(obten_valor("usuario",i));
  }
}

Una diferencia bastante relevante ocurre en el caso de que decltype se aplique a una variable que se encuentre entre paréntesis. En este caso el tipo determinado por decltype es siempre una referencia.

int x;
decltype((x)) y = x; // int & y = x

De forma general, esto ocurre con cualquier expresión que no sea exactamente un nombre de variable y que pueda actuar como un l-valor.

template <class C>
void f(C & c) {
  decltype(c[0]) t; // Error t es referencia sin iniciador
  // ...
}
//...
vector<string> v = { "uno", "dos", "res" };
f(v);

En este caso, el tipo de t se obtiene evaluando la expresión decltype(v.operator[](int)) que es un l-valor (en este caso una referencia a string). Por tanto el tipo de t acaba siendo string& y la primera línea de la función f() genera un error de compilación porque se estaría declarando una variable de tipo referencia sin darle un valor inicial.

Ahora bien, la mayoría de los ejemplos empleados hasta ahora (aunque no todos) pueden parecer artificiosos y poco útiles. Probablemente, sea cierto. Sin embargo, hay contextos en los que declttype manifiesta su verdadera utilidad como la deducción automática del tipo de retorno de una función o la especificación de excepciones mediante la nueva palabra reservada noexcept.

3 comentarios:

  1. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  2. ¡Interesante! Me viene muy bien leer posts como este para ponerme al día, que va tocando aprender C++11 :D

    En cuanto a contextos en que decltype se muestra útil, puedo decir por mi parte que yo lo he echado de menos al implementar librearías matemáticas.

    El ejemplo clásico es cuando se quiere hacer un operador no miembro sobre una clase templatizada.

    Por ejemplo, si tenemos una clase templatizada Vector3D (que es lo que parece) y queremos hacer el operator +, podemos hacer lo siguiente:

    ---------8<---------
    template
    Vector3D operator+( const Vector3D v1, const Vector3D v2 )
    {
    return Vector3D( v1.x() + v2.x(), v1.y() + v2.y(), v1.z() + v2.z() );
    }
    ---------8<---------

    El problema está en el valor de retorno, claro. Hemos puesto que Vector pero... ¿qué pasa si alguien hace Vector3D(1,1,1) + Vector3D(2,2,2) ?

    Se devolvería un Vector3D con componentes también float, con la correspondiente pérdida de precisión.

    Peor aún, ¿y si alguien quiere hacer vectores con tipos de dato propios, A y B, en que la expresión A + B devuelve un tipo de dato C? Esto puede parecer raro, pero es una forma de hacer lazy evaluation y permitir simplificación de expresiones en tiempo de compilación (librerías como Eigen lo usan). En este caso, nuestra template no funcionaría.

    Todo esto se soluciona combinando auto + decltype + late-specified return types:

    ---------8<---------
    template
    auto operator+( const Vector3D& v1, const Vector3D& v2 ) -> Vector3D< decltype( T1()+T2() ) >
    {
    return Vector3D< decltype( T1()+T2() ) >( v1.x() + v2.x(), v1.y() + v2.y(), v1.z() + v2.z() );
    }
    ---------8<---------

    Un poco lioso, pero gracias a ello ya sí que podemos usar cualquier tipo con nuestro Vector3D (mientras tenga definido el operador +, al menos).

    ResponderEliminar
  3. Muchas gracias, Javier.

    Exactamente, la siguiente entrega del blog tratará precisamente de ese problema. Intentaremos ver el problema, la solución y sus implicaciones tanto en código genérico como en código no genérico.

    ResponderEliminar