Você acabou de herdar uma base de código C++ legada. E agora?
(gaultier.github.io)- Se você assumiu uma base de código C++ legada grande e complexa, o objetivo não é “código limpo”, mas elevar segurança, experiência do desenvolvedor, correção e desempenho a um nível aceitável
- O ponto de partida é garantir alinhamento da organização e limite de tempo, documentar as plataformas suportadas no README e fazer com que build e testes locais passem de forma estável
- Melhorias na velocidade de build e testes devem começar por ganhos de baixa complexidade, medindo resultados ao remover testes de dependências, limpar targets desnecessários e experimentar linkers rápidos como
mold - Depois de reduzir código não usado e código para plataformas sem suporte, é preciso criar um loop de validação automatizado com
clang-tidy,cppcheck,clang-format,-fsanitize=address,undefinede CI - Reescrever tudo ou adotar padrões mais novos de C++ não é o objetivo, mas um meio; até uma reescrita em linguagem com segurança de memória só vale ser considerada quando houver motivos fortes
No começo, organize pessoas e processos antes do código
- Melhorar uma base de código C++ legada não deve ser um esforço solitário feito depois do expediente, nem uma “marcha da morte” de longo prazo, e sim um trabalho sustentável de engenharia de software
- É preciso explicar problemas, soluções e limites de tempo em linguagem simples para chefes, colegas e pessoas não técnicas
- Se uma nova contratação levou 3 semanas para conseguir compilar localmente e fazer a primeira contribuição, você pode propor reduzir isso para alguns minutos
- Se uma configuração simples de fuzzing derruba o app 253 vezes em poucos segundos, isso vira base concreta para explicar o risco em produção
- Se a implantação depende de um único servidor de build com FreeBSD 9 sem suporte há 8 anos, uma falha nesse servidor bloqueia o deploy
- Se um linter padrão da indústria encontra imediatamente comportamento indefinido que causava bugs em produção, há motivo de sobra para rodá-lo a cada mudança
- Se uma biblioteca criptográfica copiada manualmente e modificada impede saber o impacto de vulnerabilidades, é necessário organizar dependências e alertas automáticos
- Também é claro o que evitar
- Fazer uma atualização total para o padrão mais novo de C++ durante 2 semanas sem haver testes
- Criar mudanças enormes em um branch separado por meses esperando que algum dia sejam mescladas
- Começar uma reescrita completa do zero supondo que tudo terminará em poucas semanas
- Iniciar a “melhoria da base de código” sem saber o que será feito nem até quando
Como melhorar com segurança
- Toda mudança deve ser pequena e incremental, e antes e depois dela o app deve funcionar, com testes e linters passando
- Correções urgentes de bugs precisam continuar possíveis como antes, sem serem bloqueadas pelo trabalho de melhoria
- Cada mudança deve trazer uma melhora mensurável e ser explicável ou demonstrável até para quem não é especialista
- Mesmo que todo o trabalho pare no meio por prioridade ou orçamento, ainda deve restar um ganho líquido mensurável em relação ao ponto inicial
Declare as plataformas suportadas no README
- O README deve listar os pares
<architecture>-<operating-system>oficialmente suportados- Ex.:
x86_64-linux,aarch64-darwin
- Ex.:
- Essa lista serve de referência para verificar se tudo compila em todas as plataformas suportadas, e depois justifica remover código para plataformas não suportadas
- Se necessário, também dá para especificar versões de arquitetura como ARMv6 e ARMv7
- A lista de plataformas suportadas ajuda a responder perguntas como
- Dá para depender de suporte de hardware para ponto flutuante, SIMD e SHA256?
- É necessário suportar 32 bits?
- O sistema roda em plataformas big-endian?
- É preciso considerar a possibilidade de
charter 7 bits?
- As workstations dos desenvolvedores também devem estar nessa lista
Primeiro, estabilize build e testes locais
- Mesmo bases centrais de C++ em produtos bem-sucedidos muitas vezes não compilam de forma estável; a meta não é “às vezes compila”, mas sim compilar de forma consistente em todas as plataformas suportadas
- O melhor cenário é conseguir compilar e executar diretamente na máquina do desenvolvedor
- Se o projeto for grande demais e faltar RAM, é possível alugar um servidor maior só para builds
- Se forem necessárias APIs específicas de plataforma, como
io_uring, dá para implementar um shim ou compilar dentro de uma máquina virtual na workstation - Ainda assim, build local direto continua sendo a melhor opção
- Se não houver testes, eles devem ser escritos antes de mudar o código
- O ponto de partida mais fácil costuma ser capturar entrada e saída do programa em uso real para criar testes end-to-end
- Esses testes não garantem que o comportamento atual esteja correto, mas reduzem regressões nas mudanças
- Se já existir uma suíte de testes, mas parte dela falhar, desative esses testes primeiro e garanta antes que o conjunto completo passe, mesmo que demore horas
Documente como compilar e testar
- O README deve explicar como compilar e testar o app
- O formato ideal é um único comando de build e um único comando de teste
- Se no início o processo for complexo, dá para encapsular os comandos em
build.shetest.shpara esconder essa complexidade - O objetivo é que até alguém que não seja especialista em C++ consiga executar build e testes sem fazer perguntas
- Documentar estrutura do projeto ou arquitetura costuma ser melhor depois, quando o código desnecessário já tiver sido removido
Reduza tempo de build e testes com ganhos de baixa complexidade
- Sem trocar todo o sistema de build nem fazer otimizações heroicas, o ideal é medir primeiro os ganhos de baixa complexidade
- Itens a verificar primeiro
- Verifique se os testes das dependências estão sendo compilados e executados toda vez
- Houve caso em que, ao usar
unittest++como subprojeto do CMake, os testes do próprio framework também eram compilados e executados sempre
- Houve caso em que, ao usar
- Verifique se programas de exemplo das dependências estão sendo compilados e executados
mbedtlstambém permitia desativar a build dos exemplos por variável do CMake
- Verifique se seu projeto, ao ser incluído como subprojeto de outro, compila por padrão testes e exemplos
- Recomenda-se deixar variáveis de build como
MYPROJECT_TESTdesativadas por padrão e ativá-las apenas no desenvolvimento direto
- Recomenda-se deixar variáveis de build como
- Verifique se todas as dependências de terceiros estão sendo compiladas, embora na prática só uma pequena parte seja usada
mbedtlsoferece muitas flags de compilação para desativar partes desnecessárias
- Verifique se dependências de targets estão erradas e fazem tudo recompilar a cada pequena mudança
- Muitos sistemas de build conseguem imprimir o grafo de dependências
- Experimente linkers rápidos como
mold - Se possível, compare compiladores também
- Em alguns projetos, clang foi 2 vezes mais rápido que gcc; em outros, não houve diferença
- Verifique se os testes das dependências estão sendo compilados e executados toda vez
- Há outros itens que podem ser testados, mas cujo ganho pode ser pequeno ou até negativo
- LTO desligado/ligado/thin
- Separação de informações de debug
- Comparação entre Make e Ninja
- Tipo e configuração do sistema de arquivos
- Se a build demora demais, até modificar o código deixa de ser viável na prática
Remova código desnecessário
- Já houve caso em que mais de 30% da base era código completamente morto, e esse tipo de código só continua aumentando o custo de compilação e refatoração
- Formas de remover
- Use avisos do compilador como
-Wunused-xxx- Ex.:
-Wunused-function - Na maioria dos casos, basta apagar, recompilar e testar, mas em raras situações isso pode ser sintoma de um bug em que a função errada está sendo chamada, então é bom tomar cuidado com automação total
- Ex.:
- Use linters como
cppcheckpara encontrar funções não usadas ou campos de classe não utilizados- Em herança e funções virtuais pode haver muitos falsos positivos, mas ainda assim eles podem encontrar elementos não usados que o compilador não detecta
- Também existe a técnica de fazer o linker colocar cada função em uma seção separada e emitir as seções não usadas removidas, mas o ruído de funções da biblioteca padrão pode torná-la pouco prática
- Comparar assembly gerado com código-fonte não funciona tão bem para funções virtuais
- Use avisos do compilador como
- Use a lista de plataformas suportadas para remover código de plataformas não suportadas
- Código antigo de suporte a Solaris em um projeto que na prática só rodava em FreeBSD
- Código com gerador de números aleatórios próprio quando as plataformas reais sempre já têm um
- Código de compatibilidade com falta de suporte a POSIX 2001 em algo que só roda em Linux e macOS modernos
- Código que detecta CPU big-endian para fazer byte swap
- Código adicionado anos atrás que nunca se conectou a uma funcionalidade real
- PRs que removem muito código podem mostrar ao mesmo tempo queda no tempo de build e no custo de manutenção
Adicione linters, formatter e sanitizers
- Não é preciso ativar regras demais no linter; o importante é colocar algumas básicas no ciclo de vida de desenvolvimento
clang-tidyecppcheckpodem ser úteis, mas também lentos e barulhentos- Ficar sem linter nenhum não é opção; na primeira execução, eles costumam achar muitos problemas reais que nem avisos do compilador pegavam
- Formatação de código deve ser aplicada de uma vez só no momento apropriado
- Convém esperar não haver branches ativos para evitar conflitos de merge terríveis
- Em vez de gastar tempo discutindo estilo, use uma ferramenta como
clang-formatpara formatar toda a base sem exceções - Faça commit da configuração junto
- Sanitizers são necessários para encontrar bugs difíceis que podem afetar produção de verdade
- O padrão recomendado é
-fsanitize=address,undefined - Em geral, quase não há falsos positivos, então, se algo for detectado, deve ser corrigido
- Os testes também devem rodar com sanitizers ativados para encontrar problemas
- Se o orçamento de desempenho permitir, vale considerar até alguma execução em produção com certos sanitizers ativos
- O padrão recomendado é
- Mesmo que o compilador usado para release não suporte sanitizers, ainda é possível usar um compilador como clang em desenvolvimento e testes
- Ativar sanitizers pode expor bugs antigos e vazamentos de memória escondidos por muito tempo, e corrigi-los pode exigir bastante trabalho e refatoração
- Se possível, também é bom compilar dependências de terceiros com sanitizers durante os testes para encontrar problemas dentro das dependências
Automatize com CI
- A CI automatiza, em um ambiente limpo, tudo o que foi configurado até aqui: linter, formatação, testes etc.
- Deve ser possível gerar o binário de produção a cada mudança
- A maioria dos sistemas de CI suporta matrizes com várias plataformas, então é possível verificar se a lista de plataformas suportadas no README realmente compila
- Um pipeline comum pode ser tão simples quanto
make all test lint fmt - Problemas apontados por linter e sanitizer devem fazer o pipeline falhar; caso contrário, ninguém vai corrigi-los
Simplifique o código aos poucos
- Depois de existir um loop estável de build, testes e validação, dá para simplificar o código pouco a pouco
- Já houve caso em que uma classe complexa servia só para alocar um ponteiro e verificar se era nulo, quando na prática poderia ser substituída por um boolean
- Nessa etapa, quanto mais você simplifica, mais surgem novas oportunidades de simplificação, então é difícil definir limite de tempo e convém agir com conservadorismo
- O objetivo deve ser um valor concreto, como segurança, correção ou desempenho, evitando critérios subjetivos como “código limpo”
- Atualizar o padrão de C++ é um meio, não um fim
- Por exemplo, isso pode ajudar a trocar incrementos manuais de iterador por loops
for (auto x : items) - Se tudo o que você precisa é
std::clamp, talvez seja melhor implementar isso diretamente
- Por exemplo, isso pode ajudar a trocar incrementos manuais de iterador por loops
Reescrever em linguagem com segurança de memória exige análise separada
- Reescrever partes em uma linguagem com segurança de memória é uma opção possível, mas há muitos pontos de atenção
- Isso só deve avançar quando houver uma justificativa forte
Prefira gerenciar dependências com foco em build a partir do código-fonte
- C++ não tem uma gestão de dependências consistente, e muitos projetos usam o gerenciador de pacotes do sistema
- Problemas do modelo baseado em dependências de pacotes do sistema
- As instruções de instalação dependem do SO, da distribuição e da versão da distribuição
- Ao migrar de Ubuntu 20.04 para 22.04, pode acontecer de as versões dos pacotes mudarem e você ter de atualizar 100 dependências de uma vez
- Dependências de terceiros que não existem em pacote acabam tendo de ser compiladas do código-fonte de qualquer maneira
- Os pacotes podem não ter sido compilados com as flags que você quer
- Sanitizers, LTO,
-march, informações de debug, frame pointer, diferenças de ABI de C++ etc. podem virar problema
- Sanitizers, LTO,
- Fica difícil inspecionar o código-fonte exato da versão em uso para auditoria, desenvolvimento e depuração
- Fica difícil aplicar patch em dependências e recompilá-las
- É difícil usar exatamente as mesmas versões de pacote em macOS, Ubuntu e FreeBSD
- A geração automatizada de BOM fica mais difícil
- Pode não existir a variante de pacote necessária, como biblioteca estática ou dinâmica
- Gerenciadores de pacotes C++ como Conan e vcpkg podem ser uma melhora, mas têm limitações
- Dependências externas podem deixar a CI mais complexa e lenta
- Nem todas as versões de pacotes podem existir
- O mbedtls no Conan tem caso de salto de
2.16.12para2.23.0
- O mbedtls no Conan tem caso de salto de
- Eles podem não suportar o SO ou a arquitetura de que você precisa
- A abordagem recomendada é trazer dependências com git submodule e compilá-las do código-fonte
- Simplicidade
- Permite usar histórico e diff do git, ao contrário de vendor manual
- Dá para saber exatamente a versão da dependência no nível de commit
- Atualizar uma dependência única pode ser feito com
git checkout - Funciona em todas as plataformas
- Você pode escolher flags de compilação e compilador por dependência
- Mesmo sem experiência em C++, os desenvolvedores já conhecem git
- Funciona recursivamente
- É possível compilar cada submódulo com
add_subdirectorydo CMake ougit submodule foreach make - Se submódulos forem complicados, uma abordagem como a do Neovim — um script que baixa e compila dependências — também pode funcionar
- Se o grafo de dependências for muito grande, pode ser necessário um sistema híbrido de build local/remoto como Buck2, com reaproveitamento de artefatos
- Gerenciadores de pacotes de linguagens compiladas como Go e Rust adotam a abordagem de compilar a partir do código-fonte
Pontos reforçados pelas sugestões dos leitores
- Testes precisam de ainda mais destaque, e suítes de teste em C++ devem rodar sob sanitizers para evitar uma falsa sensação de segurança
- vcpkg pode ser melhor que git submodule se atender aos requisitos e ao cross-compilation do projeto
- Nix pode funcionar como gerenciador de dependências para C++, mas complexidade e lentidão são problemas
- Se você corrige só um bug por ano, investir em uma grande refatoração é questão de julgamento, mas remover código morto e usar sanitizers ainda tem valor mesmo com mudanças raras
- Remoção de código deve se concentrar em casos em que a análise estática mostra que ele não é chamado; em caso de dúvida, é mais seguro não remover
- Métodos virtuais têm o alvo da chamada decidido em runtime, então são difíceis de remover com análise estática
- Uma conversa de 15 minutos com vendas, gerente de produto ou usuários sobre uso de funcionalidade/plataforma pode reduzir bastante o trabalho técnico
- Colocar código em um LLM e fazer perguntas pode ser viável e legalmente seguro; se possível, rode localmente e trate os resultados com cautela
- Também há a ideia de usar ferramentas de análise de código para gerar diagramas e relações entre classes, ajudando a entender a estrutura geral
- Se não houver sistema de controle de versão, a etapa zero é colocar o código em um VCS
- Há quem considere a CI como etapa 1, o que também é um ponto válido, mesmo que o local seja mais rápido
- Remover plataformas quase não usadas pode reduzir a complexidade combinatória e permitir grande simplificação
- Builds totalmente reproduzíveis podem não ser realistas em bases C++ comuns, mas builds confiáveis são realistas
- Um commit de formatação de toda a base pode ser configurado para o git ignorar no blame, reduzindo o custo de rastrear histórico
- Há também a ideia de usar estatísticas do histórico do VCS para encontrar áreas muito alteradas e arquivos que mudam juntos
- Essa abordagem vale não só para C++, mas para outras bases legadas também, exceto pelas partes específicas de C++, como sanitizers
Working effectively with Legacy Codeé recomendado como livro com conselhos relacionados- O foco deve estar no valor real, mas ao olhar uma grande base C++ pela ótica de segurança, pode haver muitas vulnerabilidades, e isso representa valor na forma de redução de risco, não de ganho financeiro
1 comentários
Opiniões no Hacker News
Há bons conselhos e também alguns um tanto controversos. Depois de herdar vários projetos enormes em C++, algumas tarefas parecem trazer um benefício claro logo no início
Comece por criar builds reproduzíveis e envolva o ambiente de build com Docker ou sua ferramenta de empacotamento preferida, para que ferramentas e dependências fiquem explícitas e reproduzíveis
Fazer o projeto compilar sem avisos com
-Wallexpõe código ruim, comportamento indefinido e bugs, e permite ver imediatamente um aviso quando eu inserir código suspeito depoisVerificar erros de leitura/escrita com ferramentas como
valgrindno começo também é um ganho fácil em termos de estabilidade, e, antes de entender a estrutura inteira, é mais seguro manter as refatorações localizadasDockerfiles que apontam para tags de imagem mutáveis sem hashes de integridade, ou que chamam
apt-get, são comuns; ambos deixam muita margem para acabar em estados sutilmente diferentesSoluções como Bazel ou Nix são mais difíceis de aprender, mas dão uma base muito melhor
O passo 2 é rodar todos os testes sob sanitizers e marcar os que falham; o passo 3 é corrigir todas essas falhas de sanitizer; o passo 4 corresponde ao restante do trabalho
Contêineres são bons, mas são péssimos quando usados como forma de fingir que não há problemas
É preciso empacotar todas as dependências e evitar um estado em que o sistema só funcione se houver bilhões de coisas em
/usr/libinstaladas nas versões corretas-Wallé aceitável, mas sou fortemente contra colocar-Wall -Werrorem builds de produçãoAlguns avisos do compilador são mais uma questão de opinião, e, se uma nova versão do compilador adicionar um novo aviso, um código que antes estava limpo pode de repente passar a ser rejeitado
Se
-Werrorfor necessário, é melhor usá-lo apenas em builds de debugEu inverteria a ordem dos itens 2 e 3. Ter CI, linting, formatação automática etc. antes vem antes de arrancar qualquer coisa
Porque você ainda não sabe o que deve remover nem quais consequências a remoção terá
Linters e ferramentas de análise estática dão muitos insights sobre quais partes do programa precisam de trabalho
As áreas que a ferramenta de análise estática aponta hoje muitas vezes são lugares onde, mais tarde, será possível remover funções, classes ou arquivos inteiros que reimplementam conceitos da STL
Bibliotecas de iteradores caseiras, smart pointers caseiros, uso de funções de strings em C etc. podem ser trocados por algoritmos da STL, smart pointers de verdade e classes de string de C++
Mas isso não fica visível antes de escanear o código, e também é difícil avaliar os resultados antes de avançar em direção a builds de teste ou deploys rápidos
Onde há lacunas, adiciono testes básicos de fumaça
Isso não deixa o trabalho mais lento; pelo contrário, acelera. Com uma cobertura razoável, você consegue se mover muito mais rápido ao refatorar, então é um retorno grande para um investimento pequeno
Projetos grandes em C++ costumam ter não apenas scripts de geração de código, mas também scripts que fazem parsing do código para coletar dados usados na geração de código
A formatação automática pode quebrar isso, e eu já vi até projetos amaldiçoados em que os usuários faziam parsing de arquivos de cabeçalho públicos com scripts frágeis
É muito provável que você acabe mexendo em várias partes e, quando muitas mudanças se acumulam localmente, pode sem querer criar commits com preocupações misturadas
Nessa hora, a CI vira uma salvação
Se for seguir por esse caminho, recomendo ao menos comentar o código, em vez de apagá-lo imediatamente, até que o branch esteja pronto para squash e merge, para manter o diff mais simples
Faltaram ferramentas e técnicas para entender o código. Antigamente eu usava uma ferramenta chamada Source Navigator, escrita em Tcl/Tk, que era muito boa para indexar codebases
Dava para ver a hierarquia de chamadas do método atual e usá-la para criar diagramas de sequência UML
Uma ferramenta parecida é o Source Insight mencionado abaixo
E anotações são importantes. O essencial é escrever como se estivesse ensinando alguém
Ao longo dos anos, fiquei razoavelmente bom em entender código, e houve uma época em que eu sozinho dava suporte ativo e desenvolvia uma codebase de trading algorítmico em Java que negociava cerca de US$ 200 milhões por dia em 4 a 5 bolsas
Havia 35 MB de documentação sobre esse código e, deixando de lado o risco de pessoa-chave, a responsabilidade era boa
Sinceramente, a maioria das grandes codebases tem excesso de engenharia e muita duplicação
[1] References in "Source Insight" https://d4.alternativeto.net/6S4rr6_0rutCUWnpHNhVq7HMs8GTBs6...
No curso de arquitetura de computadores da universidade, nos obrigaram a usar essa relíquia porque ela tinha um conjunto estranho de recursos que não existia em outros lugares, além de suporte a emulador ARM
Nós a usávamos para programação em assembly ARM bare-metal
Senti o estômago embrulhar no trecho que diz que colocar
std::cmakena biblioteca padrão seria um divisor de águasEntendo o conselho de arrancar com uma motosserra tudo que não é absolutamente necessário para oferecer as funcionalidades que a empresa ou o projeto open source anuncia e vende, mas isso é extremamente perigoso
Essencialmente, é um problema da cerca de Chesterton: se você não entende completamente por que algo entrou no software e como o software é usado hoje, não pode removê-lo
No pior caso, quando lançar daqui a mais ou menos um mês, os usuários vão descobrir que uma funcionalidade importante quebrou de forma sutil, e você vai passar dias tentando rastrear exatamente como ela quebrou
Adicionar CI, linters, fuzzing, formatação automática etc. é uma boa ideia, mas também é difícil
Se uma pessoa usa principalmente VIM, outra emacs, outra QTCreator e outra VSCode, é muito difícil colocar todo mundo na mesma página
Se for uma etapa opcional que exige instalar uma ferramenta nova, na prática ela não vai acontecer; e um linter também não ajuda se, assim que você abre o projeto, aparecem mais de 2000 avisos
cpplint,clang-tidyou fuzzers integrados à IDE, e eles são lentos demais para rodar automaticamente a cada tecla pressionadaSó a formatação automática às vezes é integrada
Essas coisas podem ser feitas pela linha de comando, independentemente do ambiente de desenvolvimento de cada um; portanto, não desista antes mesmo de começar só porque são usados dois editores de texto, pelo menos tente
Em C++, se a equipe não quiser instalar ferramenta nenhuma, o caminho vai ser sofrido. Também vale considerar conteinerizar as ferramentas para facilitar
Temos que trabalhar como profissionais e, se o trabalho exige configurar IDE e cadeia de ferramentas antes de tudo, é preciso aprender e usar
Algo que compila e funciona no meu computador, na minha IDE e do jeito que eu gosto está mais perto de artesanato do que de software
Mesmo que instalar o hook local seja opcional, se o PR falhar no CI, a pessoa acaba fazendo
Isso é realmente básico, mas parece haver uma lacuna bem grande de conhecimento sobre como lidar direito com CI e infraestrutura
Testes, lint, fuzzing, formatação, validação de formato YAML, verificação de ausência de quebra de linha no EOF etc. devem poder ser executados localmente para ajudar o desenvolvedor a evitar uma falha no CI antes de dar push
Se o linter emite milhares de avisos ao abrir o projeto, quem estiver adicionando o linter deve fazer com que os avisos cheguem a 0 antes de mesclar essa alteração
Dá para fazer isso desativando avisos ou arquivos específicos, corrigindo parte deles, ou combinando essas abordagens
Acho que o primeiro passo é entrar em contato com o mantenedor anterior, ir encontrá-lo, pagar um chá ou uma cerveja e, no fim, conversar sobre a base de código
Esses magos chamados antigos mantenedores podem ensinar muita coisa
As outras sugestões, como fazer rodar em várias plataformas ou passar nos testes, me parecem bons testes de estresse que levam a robustez e entendimento
Mesmo assim, eu certamente iria primeiro colher esse fruto mais baixo: conversar com quem passou por ali antes
Se você primeiro trabalhar um pouco por conta própria, ficar bloqueado em vários pontos e só então falar com o mantenedor anterior, será muito mais produtivo
Eles também vão valorizar esse esforço
Se você herdou uma base de código sem testes, em que o build falha a cada duas tentativas, as informações de dependência são obscuras e que só compila em um único servidor com um sistema operacional terrivelmente antigo, não dá para ter certeza de que o mantenedor anterior é mesmo um mago
Também é preciso avaliar se todos os problemas surgiram por falta de tempo, ou se eram devidos a um “mago” que mantinha tudo quebrado para garantir estabilidade no emprego ou porque não queria aprender coisas novas
Esse é o melhor caso; no caso comum, a resposta para todas as perguntas é apenas “já estava assim quando eu recebi”
Durante o trabalho encontrei vários bugs e problemas, e tentei entrar em contato com os desenvolvedores que haviam escrito aquele software sob medida para o meu empregador
Descobri que ele tinha sido escrito por um único contratado, que havia morrido alguns anos antes
Isso é comum na indústria de defesa. Há muitos itens personalizados e descartáveis feitos para sistemas específicos e, especialmente no lado de hardware, não é raro que os engenheiros que construíram o equipamento já tenham saído há muito tempo ou se aposentado
Isso ajuda muito
Testes em uma base de código C++? Acho otimista
Não entendo muito bem por que há tanto foco em refatoração ou melhorias. Se dá para acrescentar uma funcionalidade ao código, faça isso sem mexer no resto
Se a mudança for grande o bastante, dá para extrair do código legado o que for necessário por chamadas a funções externas, introdução de uma camada de rede, separação do mesmo código em uma biblioteca, e fazer o restante em um novo ambiente
Eu não tentaria uma grande refatoração, a menos que várias pessoas precisem trabalhar nesse código daqui para frente e sejam necessárias certas premissas e padrões para que o grupo consiga colaborar com facilidade
A abordagem de ir acrescentando coisas funciona até certo ponto, mas chega uma hora em que, ao mudar algo, alguma coisa quebra de repente e está tudo tão bagunçado que você leva tempo demais para achar a causa
O ponto do texto original é mais algo como evitar uma grande reescrita, mas fazer uma arrumação para manter a coesão e facilitar mudanças
Às vezes era preciso adicionar a mesma funcionalidade às duas versões
A forma de tratar um código cujo último lançamento você sabe que está chegando logo é muito diferente da de um código que ainda será mantido e receberá funcionalidades por décadas
Na realidade, muitas vezes a nova funcionalidade é uma mudança em um comportamento já existente, e de repente você acaba tendo de fazer refatorações pesadas em vários lugares
É uma thread com muitos bons conselhos. Acrescentando algo que não se limita a C/C++: se você tem a possibilidade de usar um sistema de controle de versão, deve aproveitar bem o valor dele
Muitas equipes o usam apenas como ferramenta simples de colaboração, mas ele pode fazer mais do que isso
Basta pegar o histórico e criar um banco de dados simples. Não precisa ser um RDB; dá para começar com arquivos JSON ou uma planilha
Só uma abordagem orientada a dados já permite extrair quase imediatamente muitas informações úteis
Arquivos e funções que mudaram com frequência provavelmente serão hotspots de trabalho no futuro; então, se quiser introduzir testes unitários ou reduzir conflitos de merge, concentre-se neles
Se arquivos que parecem distantes entre si mudam frequentemente juntos, isso pode indicar uma estrutura implícita que não aparece só olhando o código
O modelo real de propriedade de cada módulo também pode ser inferido pelo histórico, e uma propriedade pouco clara pode ser um sinal de necessidade de refatoração
Em C/C++, melhorias no tempo de build também devem se concentrar, com base em dados, nos módulos importantes. Em vez de remover dependências de arquivos aleatoriamente, dá para dividir módulos que mudam com frequência e até pontuar o impacto real no tempo de build combinando isso com dependências de headers
Ao integrar o VCS a outras ferramentas de desenvolvimento, dá para fazer ainda mais; na era dos LLMs, também parece possível colocar o histórico e os metadados do projeto no modelo e pedir insights interessantes
Para fazer isso sem uma janela de contexto gigantesca, talvez seja necessária engenharia de modelo dedicada, mas tenho a intuição de que vale tentar
O conselho de adicionar CI, linters, fuzzing, formatação automática etc. precisa ser analisado em partes
CI deve garantir que o projeto também compila em outro lugar além da minha máquina, evitando regressões relacionadas à compilação
Avisos do compilador e analisadores estáticos em geral são mais espertos do que eu; portanto, se aparece um aviso assustador sobre fazer algo estranho com ponteiros, é um forte sinal para investigar
Testes unitários devem verificar se o código importante faz exatamente o esperado até em baixo nível e, como há boa chance de que não faça, é preciso entender o motivo
Ao consertar uma coisa, outra pode estourar, porque o código existente talvez tenha sido escrito pressupondo um comportamento com bug
Formatação automática não é prioridade, e acho melhor seguir o estilo dos mantenedores existentes
Também é bem provável que a ideia de que a etapa final de uma base de código C++ herdada seja reescrevê-la em uma linguagem com segurança de memória não se encaixe bem
É difícil conseguir recursos extras de trabalho para algo que não está quebrado; passaria a ser necessário conhecimento de outra linguagem além de C++, e os testes poderiam ficar mais complexos
Também há grande chance de que restrições de memória ou desempenho tornem inadequado usar várias linguagens e, para começo de conversa, herdar uma base de código legada é quase uma admissão de que faltaram recursos como tempo, dinheiro e conhecimento para reescrevê-la
“Reescrever em X” só acrescenta complexidade por estar na moda
Se você já está reescrevendo uma grande parte da base de código em C++, é melhor seguir um subconjunto mais restritivo de C++, e acho High Integrity C++ uma boa opção
Se puder obter o padrão MISRA mais recente, provavelmente também é uma boa
Em vez de fazer a equipe inteira reaprender as arestas afiadas de uma nova linguagem, é melhor usar a linguagem que já conhece, mas impor diretrizes para evitar armadilhas conhecidas
É estranho o autor criticar bastante automação de BOM, controle de versões de pacotes, origem de dependências etc. e depois sugerir git submodules como melhor do que um gerenciador de pacotes
Antes de fazer esse tipo de crítica, deveria experimentar o vcpkg
Ele tem algumas arestas, mas quase tudo isso é atendido de forma intuitiva pelo vcpkg
Atualizar dependências é um pouco mais difícil do que com git submodules, mas vejo isso mais como um recurso do que como um bug. As dependências são compiladas em sandboxes individuais e depois instaladas em um diretório especificado
O vcpkg permite definir um repositório interno como registro em vez do repositório oficial para manter o caráter vendored-in, fazer chainload da toolchain para compilar tudo com um conjunto fixo de flags e também permite personalizações por port
É porque essas abstrações são úteis que gerenciadores de pacotes são populares, e assim nem todo mundo precisa lidar diretamente com strings intermináveis contendo flags de compilação, macros, avisos etc.
O texto foi interessante e aprendi algumas coisas. Mas fiquei curioso sobre a que linguagem as pessoas se referem quando dizem “reescrever em uma linguagem com segurança de memória”
Fiquei me perguntando se querem dizer reescrever partes em Go, Java, C#, ou se é uma ironia do tipo reescrever em Rust, deixando margem para negação
A conclusão depende inteiramente da equipe e das restrições. Por exemplo, importa se garbage collection é aceitável; se for, se Go é uma boa escolha; se segurança é a prioridade máxima, e assim por diante
Acredito que a maioria dos desenvolvedores C++ em geral consegue usar Rust com facilidade e obter desempenho equivalente
Mas há casos em que, para começo de conversa, não havia um bom motivo para o projeto precisar ser em C++, e também já vi casos de reescrita bem-sucedida em Java
A Apple está reescrevendo parte do código C++ em Swift
Uma boa regra prática é escolher uma linguagem com a qual a equipe ou a empresa se sinta confortável
Chegamos ao ponto de suspeitar que o autor seja um programador Rust maligno e criptografado. Talvez porque Rust nem tenha aparecido, então não dava para reclamar diretamente dele