La regla del 5 en C++

C++ soporta Programación Orientada a Objetos lo que significa que puedes expresar entidades con clases y estructuras (structs) y hacer instancias de ellas.

Estas clases agrupan las operaciones disponibles para trabajar con los datos que clases y estructuras encapsulan.

En una clase, los datos son representados por miembros los cuales son campos que contienen datos expresados en sus tipos de dato y funciones miembro las cuales operan con los datos.

Las clases se construyen y destruyen. Si la clase no es compleja, solo tiene campos con tipos primitivos y no asigna memoria en el área de memoria dinámica, C++ nos provee de constructores y destructores implícitos.

Estos constructores y destructores son métodos que son ejecutados cuando el objeto de construye o se destruye respectivamente.

Estos métodos, constructor y destructor son funciones miembro especiales que se llaman exactamente como la clase y en el caso del destructor va precedido del carácter tilde ~.

Si alguna vez necesitamos implementar uno de estos métodos debemos saber que podemos implementar muchos constructores diferentes pero solo un destructor.

Los constructores pueden tener o no parámetros.

Hay 3 tipos de constructores.

El primer tipo es un constructor para crear una instancia a partir de parámetros o sin ellos.

class MiClase
{
public:
	MiClase(){/* tu implementación */}

	MiClase(int param1, int param2){/* tu implementación */}
}

MiClase my_object(1, 2);
El segundo tipo de constructor se llama constructor de copia.
class MiClase
{
public:
	MiClase(const MiClase& inst){/* tu implementación */}
}

MiClase mi_objeto;


MiClase otro_objeto(mi_objeto);

Este es un tipo especial el cual acepta una instancia de la misma clase como único parámetro y es para crear una copia del objeto que se pasa como parámetro.

El tercer tipo es el constructor de mover.

class MiClase
{
public:
	MiClase(MiClase&& inst){/* tu implementación */}
}

MiClase mi_objeto;


MiClase otro_objeto(std::move(mi_objeto));

Es similar al constructor de copia, acepta una instancia de la misma clase como único parámetro, pero usa semánticas de mover y es para extraer los recursos de la instancia que se pasa como parámetro y dejarla en un estado vacío.

Hay otras dos operaciones especiales o mas bien operadores. Uno es la asignación de copia y el otro la asignación de mover.

La asignación de copia es para copiar los datos de un objeto en otro objeto que ya existe.

class MiClase
{
public:
	MiClase& operator = (const MiClase &) {/* tu implementación */}
}

MiClase mi_objeto,  otro_objeto;

mi_objeto = otro_objeto;

La asignación de mover is como la de copia, copia los datos en un objeto que ya existe pero deja el objeto original en un estado vacio.

class MiClase
{
public:
	MiClase& operator = (MiClase &&) {/* tu implementación */}
}

MiClase mi_objeto,  otro_objeto;

mi_objeto = std::move(otro_objeto);

Si C++ provee de estos 5 operadores ¿porque necesitamos tener los nuestros propios?

Si tienes un objeto con punteros y no defines un constructor de copia, C++ proveerá un constructor de copia por defecto el cual solo hace una copia poco profunda.
Una copia poco profunda es que copiará los punteros pero no los buffers o objetos a los que apuntan, por tanto el nuevo y el viejo objeto apuntarán a los mismos recursos y cuando una de las dos instancias sea destruida, muy probablemente va a des asignar esos recursos y ahora el otro objeto va a apuntar a memoria no asignada.

En ese caso necesitas definir tu constructor de copia y manualmente copiar esos recursos.
También, si un objeto tiene recursos, tienes que decirle a C++ como liberarlos al final de la vida del objeto, de otra manera permanecerán bloqueados e inaccesibles y no queremos eso.

Basado en lo dicho hasta ahora sobre lo que son el constructor de copia, el constructor de mover, la asignación de copia, la asignación de mover y el destructor, voy a explicar la regla del 5.

La regla del 5 dicta que si necesitas implementar una de estas 5 operaciones, entonces tienes que implementar las 5.

Esto es porque si necesitas un destructor porque tienes que liberar recursos manualmente, si copias o mueves el objeto también tienes que hacer algo al respecto.

Otra opción es borrar esas operaciones de modo que evitas que el objeto sea copiado o movido.

class MiClase
{
public:
	MiClase(int param1, int param2){/* tu implementación */}

	MiClase(const MiClase& inst) = delete; // borrado del constructor de copia
	MiClase& operator = (const MiClase &) = delete; // borrado de la asignación de copia
	MiClase(MyClass&& inst) = delete; // borrado del constructor de mover
	MiClase& operator = (MiClase &&) = delete; // borrado de la asignación de mover
	
	~ MyClass() {/* tu implementación */}
}

La cuestión es que no dejes la operación por defecto disponible sino una personalizada o eliminada.

Espero que sea de ayuda.

Comentarios

Entradas populares