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