lunes, 5 de septiembre de 2011

Una nueva vida para auto en C++11


Introducción

En C++11, la palabra reservada auto ha resucitado con un nuevo significado. C++ heredó esta palabra reservada de C, donde originalmente implicaba almacenamiento automático, como contraposición al almacenamiento estático. Sin embargo, al convertirse el almacenamiento automático en el almacenamiento por defecto de las variables, se hizo innecesaria su utilización.

Cuando un lenguaje evoluciona (como es el caso de C++11) se debe poner especial atención en que las modificaciones al lenguaje no afecten al código ya existente. Esto hace que los diseñadores del lenguaje sean muy reticentes a la introducción de nuevas palabras reservadas. Por esta razón, el comité de normalización tomó la decisión de resucitar auto dotándola de nuevas semánticas.

En C++11 se puede dar dos usos a auto:

  • Para indicar la deducción automática de tipos en la declaración de una variable.
  • Para indicar la deducción automática de tipo de retorno en la declaración de una función.


Cada uno de estos usos presenta ventajas en la escritura de código. Hoy presentaré algunos ejemplos del primer caso y dejaré para un próximo post el caso de las funciones con deducción automática de tipo de retorno.

Deducción automática del tipo de variables

Cualquiera que haya escrito código usando la biblioteca estándar de C++ habrá visto alguna vez cosas como la siguiente:

vector<int> v = {1, 2, 3};
for (vector<int>::iterator i=v.begin(), e=v.end();i!=e;++i) {
    cout << *i << endl;
}

Claro que esto puede empeorar:

vector<list<string> > v = { { "Carlos", "Maria"}, {"Niño", "Niña"} };
for (vector<list<string> >::iterator i=v.begin(),e=v.end();i!=e;++i) {
for (list<string>::iterator j=i->begin(), e=i->end(); j!=e; ++j) {
cout << *j << " ";
}
cout << endl;
}

Para complicar las cosas un poco más, todos los contenedores ofrecen variantes de iteradores (como const_iterator) que ocasionan algunos pequeños dolores de cabeza al escribir código.

Sin embargo en los ejemplos anteriores, tener que escribir los tipos de las variables i y j. No es realmente necesario. Se puede simplificar el lenguaje siguiendo una máxima que a mí me gusta mucho: “Deja que el compilador haga todo lo que puede hacer y deja el resto para el programador”.

Antes de entrar en los detalles del uso de auto, veamos los ejemplos anteriores en C++11. Baste por ahora decir que cuando se especifica que el tipo de una variable es auto, el compilador determina su tipo a partir del valor que se usa para iniciar la variable.

Con esto nuestro primer ejemplo queda:

  vector<int> v = {1, 2, 3};
  for (auto i=v.begin(), e=v.end();i!=e;++i) {
    cout << *i << endl;
  }

Y el segundo ejemplo:

  vector<list<string> > v = { { "Carlos", "Maria"}, {"Niño", "Niña"} };
  for (auto i=v.begin(),e=v.end();i!=e;++i) {
    for (auto j=i->begin(), e=i->end(); j!=e; ++j) {
      cout << *j << " ";
    }
    cout << endl;
  }

Deducción de tipos en contextos de declaración de variable

El principal uso de la deducción automática de variables es la declaración de variables.
Probablemente el uso más simple de auto es la declaración de una variable en un bloque (dentro de una función, dentro de un bucle,…) o bien en un alcance de un espacio de nombres.

  auto x = 5; // x es int
  auto z = 2.5;  // z es double
  string s = "Daniel";
  auto lon = s.length(); // el tipo de lon coincide con el tipo de retorno de length

Otros usos equivalentes son la declaración de una variable en una sentencia de iniciación de un bucle for, en una condición de una sentencia de selección (if, switch) o de una sentencia de iteración (while, do, for).

  string s = "Daniel";
  for (auto i=s.length();i>0;--i) {
    cout << s[i-1];
  }
  cout << endl;

Estos usos facilitan la vida del desarrollador permitiendo escribir código más simple. En este casi no es necesario recordar que tipo concreto devuelve la función miembro length(), basta con indicar que la variable i debe tener el mismo tipo.

Algunos pueden ver esta utilización de auto como una simple conveniencia que no mejora la calidad del código. Sin embargo, incluso en estos casos tan sencillos, la deducción automática de tipos aporta ventajas.
Por una parte, permite expresar claramente la intención del desarrollador. Es decir, la variable i debe tener el mismo tipo que el valor devuelto por la función miembro length(). Por otra parte, este estilo permite evitar errores derivados de la conversión automática de tipos. Veamos:

  string s = "Daniel";
  for (short i=s.length();i>0;--i) {
    cout << s[i-1];
  }
  cout << endl;

¿Qué ocurre si el valor devuelto por length() no cabe en un short? Ciertamente, es una situación que puede calificarse como mínimo de desagradable.

Pero cuando auto se vuelve realmente útil es en la escritura de código genérico. En C++03, se hacía necesario recurrir a código innecesariamente largo para escribir una función que imprimiese los elementos de un contenedor.

template <typename C>
void imprime(const C & c) {
  for (typename C::const_iterator i=c.begin(), e=c.end();i!=e;++i) {
    cout << *i << endl;
  }
}

Con C++11 uno no se tiene que volver a preguntar si el tipo concreto del iterador tiene que ser iterator o const_iterator y tampoco hace falta cualificar el tipo con typename para indicar al compilador de que realmente se trata de un tipo dependiente.

template <typename C>
void imprime(const C & c) {
  for (auto i=c.begin(), e=c.end();i!=e;++i) {
    cout << *i << endl;
  }
}

Más sobre la deducción de tipos

Una pregunta que conviene hacerse sobre la deducción de tipos es qué ocurre con las referencias. Es decir:

int x = 3;
int & z = x;
auto t = z;  // ¿int o int&?

Es decir, si una variable declarada como auto si inicia con otra variable de tipo referencia ¿qué tipo se deduce? La respuesta se obtiene, una vez que se observa que lo que se utiliza como iniciador (en nuestro caso z) expresión y por tanto su tipo es int.

¿Y si se desea que t sea una referencia? La solución es simple, puesto que se puede combinar auto con cualquier otro especificador de declaraciones:

int x = 3;
int & z = x;
auto&  t = z;  // int&
const auto u = x; // const int
auto *p = &x;

Otros usos de la deducción automática de tipos

Probablemente, un caso más sorprendente (aunque no debería) es la deducción automática de tipos en expresiones asociadas al operador new.

auto p = new auto(1.5); // p es un double*

Además, se puede usar la deducción automática de tipos con variables miembro estáticas que se inician dentro de la definición de una clase.

class X {
public:
  static const auto n = 3;
};

En resumen

C++11 introduce un mecanismo que permite que el compilador pueda deducir el tipo de una variable a partir de la expresión con la que ésta se inicia. Este mecanismo simplifica la escritura de código genérico. Como complemento, el mecanismo permite que evitar errores comunes de programación derivados de conversiones implícitas no deseadas.

Próximamente comentaré dos características del lenguaje que están íntimamente relacionadas con esta: la deducción automática de tipos de retorno en funciones y la obtención del tipo de un objeto.

No hay comentarios:

Publicar un comentario