Refactorización de código legacy en C++: cómo convertir un «zoológico» en un proyecto mantenible
Trabajar con código heredado (legacy) en C++ es una realidad inevitable para muchos desarrolladores. A menudo, dicho código carece de pruebas, abunda en comportamiento indefinido (UB), variables globales y gestión manual de memoria. Sin embargo, la refactorización de código legacy en C++ no es simplemente «peinar» archivos, sino un proceso sistemático para aumentar la fiabilidad y el rendimiento. En este artículo, desglosaremos los pasos clave que le ayudarán a modernizar de forma segura un proyecto antiguo sin riesgo de romperlo todo.
1. Preparación: pruebas y aislamiento
El primer paso y el más importante es crear una «red de seguridad». Sin pruebas, cualquier refactorización se convierte en un juego de ruleta rusa.
- Pruebas de caracterización: Escriba pruebas que capturen el comportamiento actual del código. Incluso si parece erróneo, primero fíjelo.
- Análisis estático: Utilice herramientas como
clang-tidyoCppcheck. Ellas identificarán posibles fugas, código no utilizado y violaciones de estilo. - Sanitizers: Ejecute el código con
AddressSanitizeryUndefinedBehaviorSanitizer. Esto ayudará a encontrar UB ocultos antes de comenzar los cambios.
Ejemplo de adición de un sanitizer en CMake:
# CMakeLists.txtset(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined -fno-omit-frame-pointer")2. Eliminación de punteros «desnudos» y gestión manual de memoria
El C++ heredado a menudo sufre de fugas de memoria y dobles eliminaciones. El C++ moderno (C++11 y posteriores) ofrece envoltorios RAII.
- Reemplazar
new/deletepor punteros inteligentes:std::unique_ptrpara propiedad exclusiva,std::shared_ptrpara propiedad compartida. - Usar
std::vectoren lugar de arrays crudos: Esto reduce el riesgo de desbordamiento de límites.
Ejemplo de refactorización:
Antes:
class LegacyBuffer { char* data; size_t size;public: LegacyBuffer(size_t sz) : size(sz) { data = new char[sz]; } ~LegacyBuffer() { delete[] data; } // Olvidado el constructor copia y el operador de asignación (violación de la "regla de tres")};Después:
class ModernBuffer { std::vector<char> data;public: ModernBuffer(size_t sz) : data(sz) {} // El destructor, la copia y la asignación son generados automáticamente por el compilador};3. Eliminación de macros y estados globales
Las macros (#define) son una causa frecuente de errores difíciles de detectar. Deben reemplazarse por constexpr, enum class o plantillas. Las variables globales complican las pruebas y el paralelismo.
- Reemplazar
#define BUFFER_SIZE 1024porconstexpr size_t BUFFER_SIZE = 1024; - Encapsulación de globales: Saque el estado global a una clase singleton o, mejor aún, pase las dependencias a través del constructor.
Ejemplo con una macro:
// Mal: la macro no tiene ámbito#define MAX_CONNECTIONS 100
// Bien: constante con tipo y ámbitonamespace Config { constexpr int maxConnections = 100;}Uso de enum class en lugar de #define para banderas:
// En lugar de #define FLAG_READ 1enum class FileMode : uint8_t { Read = 1, Write = 2, ReadWrite = 3};4. Simplificación de funciones complejas y eliminación de código muerto
Las funciones heredadas a menudo son «sábanas» de código de más de 500 líneas con múltiples condiciones anidadas. Estrategia:
- Extracción de método: Divid