Refatoração de código legado em C++: como transformar um "zoológico" em um projeto sustentável
Trabalhar com código legado em C++ é uma realidade inevitável para muitos desenvolvedores. Muitas vezes, esse código carece de testes, é repleto de comportamento indefinido (UB), variáveis globais e gerenciamento manual de memória. No entanto, a refatoração de código legado em C++ não é apenas "pentear" arquivos, mas um processo sistemático para aumentar a confiabilidade e o desempenho. Neste artigo, vamos analisar as etapas principais que ajudarão você a modernizar com segurança um projeto antigo sem o risco de quebrar tudo.
1. Preparação: teste e isolamento
A primeira e mais importante etapa é criar uma "rede de segurança". Sem testes, qualquer refatoração se transforma em um jogo de roleta-russa.
- Teste de caracterização: Escreva testes que capturem o comportamento atual do código. Mesmo que pareça errado, primeiro o fixe.
- Análise estática: Use ferramentas como
clang-tidyouCppcheck. Elas identificarão vazamentos potenciais, código não utilizado e violações de estilo. - Sanitizers: Execute o código com
AddressSanitizereUndefinedBehaviorSanitizer. Isso ajudará a encontrar UBs ocultos antes de iniciar as alterações.
Exemplo de adição de sanitizer no CMake:
# CMakeLists.txtset(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address,undefined -fno-omit-frame-pointer")2. Eliminação de ponteiros "nus" e gerenciamento manual de memória
C++ legado frequentemente sofre de vazamentos de memória e deleções duplas. O C++ moderno (C++11 e posteriores) oferece wrappers RAII.
- Substituição de
new/deletepor smart pointers:std::unique_ptrpara posse exclusiva,std::shared_ptrpara posse compartilhada. - Uso de
std::vectorem vez de arrays brutos: Isso reduz o risco de estouro de limites.
Exemplo de refatoração:
Antes:
class LegacyBuffer { char* data; size_t size;public: LegacyBuffer(size_t sz) : size(sz) { data = new char[sz]; } ~LegacyBuffer() { delete[] data; } // Construtor de cópia e operador de atribuição esquecidos (violação da "regra dos três")};Depois:
class ModernBuffer { std::vector<char> data;public: ModernBuffer(size_t sz) : data(sz) {} // Destrutor, cópia e atribuição são gerados automaticamente pelo compilador};3. Eliminação de macros e estados globais
Macros (#define) são uma causa comum de bugs difíceis de rastrear. Elas devem ser substituídas por constexpr, enum class ou templates. Variáveis globais complicam os testes e o paralelismo.
- Substituição de
#define BUFFER_SIZE 1024porconstexpr size_t BUFFER_SIZE = 1024; - Encapsulamento de globais: Mova o estado global para uma classe singleton ou, melhor ainda, passe as dependências através do construtor.
Exemplo com macro:
// Ruim: macro não tem escopo#define MAX_CONNECTIONS 100
// Bom: constante com tipo e escoponamespace Config { constexpr int maxConnections = 100;}Uso de enum class em vez de #define para flags:
// Em vez de #define FLAG_READ 1enum class FileMode : uint8_t { Read = 1, Write = 2, ReadWrite = 3};4. Simplificação de funções complexas e remoção de código morto
Funções legadas frequentemente são "lençóis" de código com mais de 500 linhas e muitas condições aninhadas. Estratégia:
- Extração de método: Quebre