Referencias a R-valor (1ª parte)
En un post anterior comentaba que en C++ no se puede vincular una referencia a un objeto temporal. En C++0x existe un nuevo tipo de referencias diseñadas para hacer precisamente esto: las referencias a R-valor. Este cambio, aparentemente sutil, es uno de los más importantes del nuevo estándar, y que condiciona muchos de los añadidos adicionales. Por eso es importante comprenderlas bien.
Definición
Una referencia a R-valor (o R-referencia) es idéntica a una referencia ordinaria (o L-referencia), pero sin la restricción de que no pueda vincularse a un R-valor, en particular, puede vincularse a un objeto temporal. Se declara con && en lugar de &.
std::string &&txt = "foo";
Naturalmente, al igual que con las L-referencias constantes, la vida del objeto temporal se prolonga a la vida de la referencia.
¡Y eso es todo! Un cambio casi trivial en la especificación del lenguaje, pero con una gran repercusión.
En el ejemplo anterior la R-referencia no aporta realmente nada nuevo, lo verdaderamente interesante viene con la sobrecarga de funciones. Pero para comprender por qué eso es útil, veamos primero el problema un problema que existe en C++ desde el principio…
Un ejemplo
Supongamos la siguiente mini-clase para manejar strings (no la uses en un proyecto real, usa std::string, esto es solo ilustrativo):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | class Cadena { public: Cadena() :m_txt(0) {} Cadena(const char *txt) { if (txt) Dup(txt); else m_txt = NULL; } Cadena(const Cadena &o) { Dup(o.m_txt); } ~Cadena() { delete []m_txt; } Cadena &operator=(const Cadena &o) { delete []m_txt; m_txt = NULL; // por si Dup() falla Dup(o.m_txt); return *this; } Cadena operator+(const Cadena &o) { Cadena res; res.m_txt = new char[strlen(m_txt) + strlen(o.m_txt) + 1]; strcpy(res.m_txt, m_txt); strcat(res.m_txt, o.m_txt); return res; } const char *c_str() const { return m_txt? m_txt : ""; } private: char *m_txt; void Dup(const char *txt) { m_txt = new char[strlen(txt) + 1]; strcpy(m_txt, txt); } }; |
Esta clase define, entre otras cosas, el constructor de copia (línea 14) el operador de asignación (línea 22), un constructor de conversión (línea 7) y el operador suma (línea 30). Con esto se pueden escribir expresiones tan naturales como:
Cadena f, x; //... f = "foo"; x = f + "bar";
Pero para darle sentido a esto el compilador crea varios objetos temporales. El código generado equivale a:
1 2 3 4 5 6 7 8 9 10 | Cadena f, x; { Cadena T1("foo"); f.operator=(T1); } { Cadena T2("bar"); Cadena T3(f.operator+(T2)); x.operator=(T3); } |
Además está la variable auxiliar y el valor de retorno del operator+(), pero la mayoría de compiladores son capaces de eliminar ambos y asimilarlos a la variable T3 (una optimización llamada RVO: Return Value Optimization).
Este código no es particularmente eficiente, hay hasta 5 llamadas a new. T1 se crea, se copia y se destruye, a lo tonto; lo mismo con T3.
Lo notable en este ejemplo es que se crean objetos temporales solamente para desplazar un valor de un sitio a otro, pero en sí no tienen identidad. De hecho es imposible conseguir un puntero a alguno de ellos, ya que no tienen nombre y se destruyen al final de la expresión.
Estos temporales son candidatos a hacer una copia destructiva, es decir, una copia que no preserva el valor original. Claro que entonces ya no es una copia, sino un movimiento. El problema es cómo distinguir cuándo hacer una copia normal y cuándo hacer un movimiento. Y aquí entran las referencias a R-valor. Si añadimos la siguientes funciones miembro:
//constructor de movimiento Cadena(Cadena &&o) { m_txt = o.m_txt; o.m_txt = NULL; } //operador de movimiento Cadena &operator=(Cadena &&o) { delete []m_txt; m_txt = o.m_txt; o.m_txt = NULL; return *this; }
Estas sobrecargas se llamarán solamente si el argumento es un objeto temporal, con lo que nos podemos permitir modificarlo libremente (es un temporal, a nadie le importa). Hay que tener cuidado, eso sí, de dejar el argumento en un estado consistente, porque sí se va a ejecutar su destructor.
Con este sencillo truco hemos convertido varias operaciones de copia, potencialmente muy costosas, en funciones triviales. El ejemplo anterior ha pasado de llamar a new 5 veces a hacerlo solo 3 (una por cada string, “foo”, “bar” y “foobar”. ¡No puede hacerse en menos!
Continúa en la 2ª parte.
Artículos relacionados:
- Referencias a R-valor (2ª parte) En el post anterior describía cómo las referencias a R-valor...
- Tipos automáticos en C++0x con auto La palabra reservada auto es casi con toda seguridad la...
- Referencias Hay quien considera que las referencias de C++ no son...
- L-valores y R-valores error: se requiere un l-valor como operando izquierdo de la...
- Referencias a referencias ¿Es posible en C++ declarar una referencia a referencia? Y...
RSS
Deja un comentario