¿Qué es la Regla de Tres en C++? Analicémosla con ejemplos
Si alguna vez has escrito una clase en C++ que gestiona memoria dinámica (por ejemplo, que contiene new o malloc), seguramente te has encontrado con la Regla de tres (Rule of Three). Es una de las reglas más fundamentales del lenguaje, que evita fugas de memoria, dobles eliminaciones y comportamiento indefinido. Analicémosla con ejemplos reales para que no solo memorices la regla, sino que entiendas por qué funciona.
Imagina: has escrito una clase que asigna memoria en el constructor. El compilador amablemente genera por ti el constructor de copia y el operador de asignación. Pero lo hace de forma superficial (shallow copy). Y ahí empiezan los problemas. La regla de tres dice: si tu clase define explícitamente un destructor, un constructor de copia o un operador de asignación, lo más probable es que necesites definir los tres. ¿Por qué? Porque es una señal de que tu clase gestiona algún recurso (memoria, archivo, conexión de red) y la copia debe ser profunda.
El problema: copia superficial (shallow copy)
Veamos un ejemplo típico. La clase String, que almacena una cadena C en memoria dinámica:
class String {private: char* data; size_t size;public: // Constructor String(const char* str) { size = strlen(str); data = new char[size + 1]; strcpy(data, str); }
// Destructor ~String() { delete[] data; }
// Constructor de copia — generado por el compilador (superficial) // Operador de asignación — generado por el compilador (superficial)
void print() const { std::cout << data << std::endl; }};Todo parece bien, hasta que intentamos copiar el objeto:
int main() { String s1("Hello"); String s2 = s1; // Llamada al constructor de copia (generado) s1.print(); s2.print(); // ¡El programa fallará con un error de double free! return 0;}¿Qué ha ocurrido? El compilador generó un constructor de copia que simplemente copió el puntero data. Ahora s1.data y s2.data apuntan al mismo bloque de memoria. Cuando los objetos salen del ámbito, se llaman a sus destructores. El primer destructor libera la memoria, el segundo intenta liberarla de nuevo — double free. El programa falla o se comporta de manera impredecible.
Solución: La Regla de tres en acción
Para solucionar la situación, es necesario implementar las tres funciones miembro especiales. Esto es la Regla de tres. Escribamos la versión correcta de la clase:
class String {private: char* data; size_t size;public: // Constructor String(const char* str) { size = strlen(str); data = new char[size + 1]; strcpy(data, str); }
// Destructor ~String() { delete[] data; }
// Constructor de copia — copia profunda String(const String& other) { size = other.size; data = new char[size + 1]; strcpy(data, other.data); }
// Operador de asignación — copia profunda String& operator=(const String& other) { if (this == &other) return *this; // protección contra autoasignación
delete[] data; // liberamos los recursos antiguos
size = other.size; data = new char[size + 1]; strcpy(data, other.data);
return *this; }
void print() const { std::cout << data << std::endl; }};Ahora la copia funciona correctamente: cada objeto posee su propio bloque de memoria. La asignación también funciona sin