1 pontos por GN⁺ 2024-03-01 | 1 comentários | Compartilhar no WhatsApp
  • 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,undefined e 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
  • 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 char ter 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.sh e test.sh para 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
    • Verifique se programas de exemplo das dependências estão sendo compilados e executados
      • mbedtls també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_TEST desativadas por padrão e ativá-las apenas no desenvolvimento direto
    • Verifique se todas as dependências de terceiros estão sendo compiladas, embora na prática só uma pequena parte seja usada
      • mbedtls oferece 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
  • 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
    • Use linters como cppcheck para 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 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-tidy e cppcheck podem 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-format para 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
  • 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

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
    • 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
    • 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_subdirectory do CMake ou git 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

 
GN⁺ 2024-03-01
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 -Wall expõe código ruim, comportamento indefinido e bugs, e permite ver imediatamente um aviso quando eu inserir código suspeito depois
    Verificar erros de leitura/escrita com ferramentas como valgrind no 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 localizadas

    • Se você quer explicitude e reprodutibilidade, é melhor evitar começar pelo Docker. Se você não tomar bastante cuidado, o Docker oferece apenas a forma mais diluída de reprodutibilidade e ainda pode dar uma falsa sensação de segurança
      Dockerfiles 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 diferentes
      Soluções como Bazel ou Nix são mais difíceis de aprender, mas dão uma base muito melhor
    • O passo 0 é um build reproduzível, e o passo 1 é rodar todos os testes e marcar os testes instáveis
      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
    • Docker é bom para microsserviços, mas nem sempre é possível implantar tudo com Docker. Isso vale especialmente para alguém que quer usar o app dentro do Docker, e Docker-in-Docker é uma situação que não deveria existir
      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/lib instaladas nas versões corretas
    • Compilar limpo com -Wall é aceitável, mas sou fortemente contra colocar -Wall -Werror em builds de produção
      Alguns 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 -Werror for necessário, é melhor usá-lo apenas em builds de debug
  • Eu 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

    • Já tive bastante experiência em trabalhos de agência entrando para salvar codebases, e a primeira coisa que faço é rodar os testes unitários e verificar a cobertura
      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
    • Por outro lado, formatação automática estraga o histórico de versões e dificulta analisar quando e por que determinada linha foi adicionada
    • Eu jamais recomendaria aplicar formatação automática a uma codebase legada
      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
    • Aqui, CI é diferente dos outros itens. O primeiro passo é criar ao menos um harness de testes de caminho feliz que rode com resultados reproduzíveis e em todo commit, e isso também ajuda a entender a codebase
      É 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
    • Indo aos detalhes, o escopo que o texto original manda arrancar se limita a código morto guiado por avisos do compilador e arquiteturas sem suporte
      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...

    • Não acredito que encontrei alguém por aí que realmente usou o Source Navigator
      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::cmake na biblioteca padrão seria um divisor de águas
    Entendo 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

    • As coisas que o autor listou normalmente não são integrações com IDE. Em ambientes de desenvolvimento C++, nunca vi cpplint, clang-tidy ou fuzzers integrados à IDE, e eles são lentos demais para rodar automaticamente a cada tecla pressionada
      Só 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
    • Acho que dizer que uma pessoa usa VIM e outra usa emacs, QTCreator ou VSCode já não é uma resposta aceitável
      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
    • Também dá para fazer formatação automática fora do editor. Basta configurar pre-commit e rodar no CI; é algo trivial
      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
    • Parece que deixaram passar o lembrete de que você pode simplesmente se recusar a cumprir requisitos básicos do chefe. Sabe, a pessoa que paga seu salário
    • Hooks de pre-commit que são opcionais localmente devem ser sustentados por etapas obrigatórias no CI
      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

    • Eu não faria disso o primeiro passo. Assim, há grande chance de desperdiçar o tempo deles
      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
    • Pode ser melhor primeiro fazer uma varredura rápida na base de código, encontrar os maiores WTFs e então fazer perguntas
      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
    • Se você já perguntou algo ao mantenedor anterior de um código legado, sabe que, mesmo que a transferência tenha sido ontem, ele dificilmente vai conseguir dizer algo útil que tenha feito há mais de 6 meses
      Esse é o melhor caso; no caso comum, a resposta para todas as perguntas é apenas “já estava assim quando eu recebi”
    • Já trabalhei implantando software em uma rede militar fechada, em cima de um sistema operacional customizado antigo. Não era um programa grande, tinha cerca de 50 mil linhas
      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
    • Não é tão fácil quanto parece. Meu passo 0 é jogar tudo em uma ferramenta UML para extrair diagramas de classes e outros diagramas
      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

    • O texto original é contra grandes refatorações. Os trabalhos incrementais propostos caminham para tornar o código cada vez mais fácil de lidar
      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
    • A resposta certa depende do futuro. Já trabalhei em código C++ em que um substituto já estava no mercado, mas ainda era preciso lançar mais algumas versões do código antigo
      À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
    • Em alguns poucos casos de sorte, dá para simplesmente acrescentar a funcionalidade
      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

    • Fico curioso se há dicas de software que ajudem a automatizar esse tipo de análise
  • 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

    • Na prática, só eliminar o C++ sem segurança de memória e impor diretrizes já pode atingir esse objetivo dentro de uma base de código C++
      “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

    • Falando como autor, pretendo tratar desse tema no segundo artigo
      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
    • É interessante ver como, mesmo sendo um texto sobre C++, em que a palavra “Rust” não apareceu nenhuma vez e que só mencionou linguagens “memory safe”, das quais há dezenas, ainda assim alguém encaixa o meme do Rust para desmerecer
      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