Seguimos con el repaso a C++11, en el artículo anterior vimos las referencias a R-Values y la optimización que nos proporciona al evitar la copia de objetos temporales. En este artículo veremos como podemos aplicar técnicas de movimiento a objetos que no copiables.
Recordemos que para que una clase sea copiable debe tener dos cosas: el constructor copia y el operador de asignación. Si, como vimos en el artículo anterior, la hacemos también movible con los respectivos métodos visto en el articulo anterior entonces haremos una optimización cuando tratamos objetos temporales. Ahora bien no aclaramos en el artículo anterior que pasa cuando queremos que nuestra clase sea movible, pero no copiable.
Vamos a imaginar que tenemos la siguiente clase para manejar archivos.
class File
{
public:
explicit File(FILE *file = NULL)
:m_file(file)
{
}
~File()
{
if (m_file)
fclose(m_file);
}
FILE *Get() const
{
return m_file;
}
//copia
File(const File &o) = delete;
File &operator=(const File &o) = delete;
//movimiento
File(File &&o)
{
m_file = o.m_file;
o.m_file = NULL;
}
File &operator=(File &&o)
{
if (m_file)
fclose(m_file);
m_file = o.m_file;
o.m_file = NULL;
return *this;
}
private:
FILE *m_file;
};
El siguiente código puede resultar extraño:
//copia
File(const File &o) = delete;
File &operator=(const File &o) = delete;
Esta sintáxis es nueva en C++11 y lo que hace es decirle al compilador que no genere automáticamente el constructor copia ni el operador de asignación. En las versiones anteriores del lenguaje esto lo hacíamos declarando ambos métodos como privados y sin definirlos.
Sin embargo vemos que si declaramos los métodos equivalentes de movimiento por lo que la clase sería movible, pero no copiable. Lo hacemos así porque no tendría sentido tener esta clase como copiable ya que si tuviéramos una copia el destructor se llamaría dos veces llamando dos veces a fclose sobre el mismo fichero, algo que no se puede hacer.
Con esta sintaxis podemos hacer algo como lo siguiente:
File OpenForWrite(const std::string &name)
{
FILE *f = fopen(name.c_str(), "w+");
if (!f)
throw ...; //error
return File(f);
}
int main()
{
File fich;
//...
fich = OpenForWrite("log.txt"); //bien: se mueve
//...
File copia(fich); //error: no es copiable!
}
La función como vemos crea un objeto temporal y se usa el operador correspondiente para movel ese objeto temporal retornado por la función a la variable fich sin problemas. Sin embargo no podemos usar el constructor copia ya que no está definido. Tiene la gran ventaja de que es casi imposible usar mal la clase, aunque aún hay casos como este:
File uno = OpenForWrite("a");
File dos(uno.Get()); //compila, pero está mal!
Podemos evitar esto declarando el constructor que recibe el FILE* privado y la función OpenForWrite() o bien miembro estático de la clase, o como una función amiga. Entonces sería conveniente declarar varias funciones más, del tipo OperForRead(), OperForAppend(), etc.
Visto esto imagina ahora que queremos mover un objeto no copiable de una variable a otro sin ser éste temporal. Lo que debemos hacer es forzar la conversión de uno de ellos a R-Value.
File uno, dos;
uno = OpenForWrite("a");
dos = uno; //error: no es copiable!
dos = static_cast(uno); //bien, se escoge el movimiento
Esta operación se ha convertido en algo habitual así que la biblioteca estándar nos da una función que se encarga de hacer esto y es std::move.
File uno, dos;
uno = OpenForWrite("a");
dos = std::move(uno);
Ella se encarga de mover el objeto de una variable a otra usando correctamente su operador de movimiento funcionando tanto en objetos copiables como no copiables.