Repaso a C++11: Referencias a un R-Value

Repaso a C++11: Referencias a un R-Value
Sin comentarios Facebook Twitter Flipboard E-mail

En este segundo artículo vamos a ver el concepto de referencia a un R-valor y vemos que utilidad aporta al lenguaje este tipo nuevo en nuestro repaso a C++11.

En el artículo anterior repasamos los conceptos básicos de R-Value y L-Value en él vimos que un objeto temporal no puede ser vinculado a una referencia, es decir, no podemos hacer algo como esto.

std::string &a = "Genbeta Dev";

Ya que una referencia es solo un alias para acceder a una posición de memoria y no podemos referenciar un objeto temporal. En C++11 se ha añadido un nuevo tipo de referencias que hace precisamente esto, poder referenciar objetos temporales. Esta cambio que puede parecer trivial es una de las bases del nuevo estándar.

Las referencias R-Value o también conocidas ya como R-Referencias son idénticas a las referencias normales, pero se declaran con && en lugar de &. Su funcionalidad es que podemos referenciar objetos temporales. Ahora el ejemplo de antes sí sería correcto.

std::string &&a = "Genbeta Dev";

Tenemos un objeto temporal ligado a una R-Referencia, la duda que podría surgir es ahora si el objeto temporal deja de serlo o que pasa con él. Funciona parecido a las referencias constantes la vida del objeto temporal queda ligada a la R-Referencia y existe mientras exista ésta.

Esto no aporta nada nuevo a lo que ya había en C++, lo verdaderamente útil viene cuando lo aplicamos a la sobrecarga de funciones. vamos a ver un problema que tiene C++ para entenderlo mejor.

Imagina que creamos una clase para el manejo de cadenas muy básica (Nunca hagas esto usa alternativas probadas como la clase string de la STL).

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

Aquí definimos varias funciones entre ellas un constructor copia, el operador de asignación un constructor que acepta cadenas char* y el operador suma para concatenar cadenas. Con este código podríamos hacer cosas como las siguientes.

Cadena f, x;
f = "foo";
x = f + "bar";

Con estas operaciones tan naturales el compilador generaría el siguiente código.

Cadena f, x;
{
   Cadena T1("foo");
   f.operator=(T1);
}
{
   Cadena T2("bar");
   Cadena T3(f.operator+(T2));
   x.operator=(T3);
}

Además crearía una variable auxiliar y el valor de retorno de operator+(), pero los compiladores modernos son capaces de eliminar ambos y asignarlos directamente a la variable T3 (La optimización RVO: Return Value Optimization).

Como vemos en este código para algo tan simple como crear una cadena y otra a partir de la concatenación de dos cadenas se hacen ¡hasta cinco llamadas a new!

T1 y T3 se crean, se copian y se destruyen sin aportar nada más. Se crean objetos temporales solamente para mover un valor de un lugar a otro, pero no tienen identidad 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 conserva 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 R-Referencias. Si añadimos la siguientes funciones a nuestra clase Cadena.

//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;
}

Esta sobrecarga del constructor copia y el operador de asignación solo son llamadas cuando se trata de un objeto temporal. Es un objeto temporal podemos modificarlo ya que no tiene consecuencias. Lo único es tener en cuenta que hay que dejar el objeto en un esta consistente ya que su destructor sí que será llamado.

Con añadir estas dos sobrecarga basadas en el nuevo tipo de referencias hemos conseguido tener funciones de movimiento para objetos temporales en lugar de copias pasando de crear cinco objetos a solo tres. Que es el mínimo que podemos crear.

En el siguiente artículo veremos que pasa con las clases que no son copiables.

En Genbeta Dev | Repaso a C++11: L-Values y R-Values

Comentarios cerrados
Inicio