Introducción: cuando C++ deja de ser "simplemente un lenguaje"
Ya has escrito una docena de programas de consola, aprendiste a usar std::vector e incluso entendiste las plantillas. Pero C++ es un lenguaje que nunca perdona un enfoque superficial. Un desarrollador principiante pasa al siguiente nivel justo cuando se enfrenta al comportamiento indefinido (UB) y comienza a entender que el compilador no es un amigo amable, sino un estricto contraparte.
En este artículo analizaremos errores no triviales como un ; olvidado o la confusión entre = y ==. Hablaremos de las trampas que esperan a quienes ya conocen la sintaxis, pero aún no han dominado la filosofía del lenguaje. Estos errores son la causa del 90% de los bugs difíciles de detectar en código industrial.
1. Ignorar la Regla de los Cinco y la Regla del Cero
Problema: funciones miembro especiales generadas implícitamente
Muchos desarrolladores que vienen de Java o Python creen que el compilador "hará todo bonito" con la copia y el movimiento de objetos. Esta es una peligrosa falacia.
Consideremos una clase con un recurso dinámico:
class Buffer {public: Buffer(size_t size) : data(new char[size]), size(size) {} ~Buffer() { delete[] data; } // ¿Todo bien? ¡No! ¡No hay constructor de copia ni operador de asignación!private: char* data; size_t size;};
void process() { Buffer a(100); Buffer b = a; // ¡Copia superficial! Dos punteros a la misma memoria.} // ¡Doble liberación — UB!La Regla de los Cinco dice: si defines al menos uno de los cinco métodos especiales (destructor, constructor de copia, operador de asignación por copia, constructor de movimiento, operador de asignación por movimiento), debes definirlos todos.
Solución: Usa std::unique_ptr o std::vector para gestionar recursos. Este es el enfoque de la Regla del Cero: si tu clase no gestiona recursos directamente, el compilador generará automáticamente las funciones especiales correctas.
class Buffer {public: Buffer(size_t size) : data(std::make_unique<char[]>(size)), size(size) {} // Regla del Cero — ¡el destructor, la copia y el movimiento funcionan correctamente!private: std::unique_ptr<char[]> data; size_t size;};2. Uso incorrecto de la semántica de movimiento
Problema: move no hace que el objeto esté "vacío" automáticamente
Uno de los errores más engañosos es creer que después de std::move, el objeto original se vuelve "seguro" para usar. En realidad, std::move es simplemente una conversión a una referencia rvalue. El estado del objeto movido depende de la implementación.
std::string a = "¡Hola, Mundo!";std::string b = std::move(a);std::cout << a; // ¿Técnicamente UB? No, ¡pero el contenido de a es indefinido!// Puede ser una cadena vacía, o puede ser "¡Hola, Mundo!"El estándar solo garantiza que el objeto movido se encuentra en un estado válido pero indefinido. Puedes llamar a a.clear() o a = "nuevo valor", pero no debes confiar en su contenido.
Consejo: Después de un movimiento, restablece el objeto a un estado conocido o deja de usarlo. Para tipos personalizados, siempre documenta las postcondiciones del constructor de movimiento.
3. Fugas de memoria mediante punteros crudos y excepciones
Problema: las excepciones "rompen" la cadena de liberación de recursos
Incluso los desarrolladores experimentados a veces escriben código con gestión manual de memoria:
void process() { int* ptr1 = new int(42); int* ptr2 = new int(100); // Si aquí se lanza una excepción — ¡fuga de ptr1! dangerous_function(); delete ptr2; delete ptr1;}En C++ no hay finally