Aeca

Capítulo 03 — Quebrando nosso programa de propósito

Série: Do zero ao jogo comercial com C++ e SFML

Capítulo 03 — Quebrando nosso programa de propósito
aeca

Erros de compilação não são inimigos: vamos aprender a ler, investigar, corrigir e validar nossos primeiros problemas em C++

No capítulo anterior fizemos algo fundamental.

Pegamos nosso pequeno programa:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

e deixamos de enxergá-lo apenas como um conjunto de símbolos que precisávamos copiar.

Começamos a entender o papel de:

  #include
<iostream>
int
main
()
{}
std
::
cout
<<
std::endl
;
return 0

Também realizamos alguns pequenos experimentos.

Removemos um ;.

Alteramos <iostream>.

Retiramos uma chave.

Observamos o Visual Studio reclamar.

Corrigimos.

E continuamos.

Hoje vamos transformar essa experiência em um processo consciente.

Porque existe uma habilidade que acompanhará praticamente toda a nossa jornada em Game Engineering:

saber investigar quando alguma coisa deixa de funcionar.

Quando estivermos trabalhando com um único main.cpp, os problemas serão pequenos.

Quando tivermos uma Game Engine com rendering, input, áudio, física, resources, scenes, gameplay, ferramentas e testes, os problemas poderão ser consideravelmente mais difíceis.

A metodologia, entretanto, começa agora.

1. Hoje queremos que o Build falhe

Sim.

Nos capítulos anteriores, nosso objetivo era encontrar:

  Build succeeded.

Hoje vamos procurar propositalmente:

  Build FAILED.

Parece estranho.

Mas existe uma razão.

Quando estamos começando a programar, um erro frequentemente provoca esta reação:

“Eu fiz alguma coisa errada.”

Depois:

“Como faço essa mensagem desaparecer?”

Essa segunda pergunta é perigosa.

Porque podemos começar a modificar coisas aleatoriamente até o código voltar a funcionar.

Uma pergunta muito melhor é:

O que essa mensagem está tentando me dizer?

E depois:

Qual é a causa do problema?

Esse será nosso foco.

2. Nosso laboratório continua propositalmente pequeno

Não criaremos nenhuma nova classe.

Nenhuma pasta.

Nenhum sistema.

Continuamos apenas com:

  CppGameEngine/
└── main.cpp

E com nosso programa conhecido:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

Antes de quebrá-lo, faça:

  Build
↓
Build Solution

ou:

  Ctrl + Shift + B

Confirme que o estado inicial é válido.

Queremos partir de:

  Build succeeded.

Isso é importante.

3. Nunca investigue sem conhecer o estado inicial

Imagine que você altere cinco coisas e depois descubra que o projeto não compila.

Qual delas causou o problema?

Talvez:

  Alteração A
Alteração B
Alteração C
Alteração D
Alteração E

Uma delas.

Duas delas.

Todas.

Ou talvez o problema já existisse antes.

Por isso nossa experiência de hoje seguirá:

  Estado conhecido e funcionando
            ↓
     Uma alteração
            ↓
        Build
            ↓
       Observação
            ↓
      Investigação
            ↓
       Correção
            ↓
        Build
            ↓
       Validação

Essa disciplina parece exagerada para cinco linhas de código.

Não será exagerada quando estivermos investigando um bug dentro de uma Game Engine.

4. Experimento 1 — o famoso ponto e vírgula

Nosso código válido possui:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

Vamos remover somente:

  ;

Teremos:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl
    return 0;
}

Não altere mais nada.

Agora faça Build.

5. Não corrija imediatamente

O Visual Studio provavelmente destacará o problema.

A Error List poderá apresentar uma ou mais mensagens.

A janela Output também exibirá informações relacionadas ao processo de Build.

Sua primeira reação talvez seja:

“Já sei. Está faltando ;."

E está.

Mas não coloque de volta ainda.

Queremos aprender a trabalhar como investigadores.

Observe:

  • qual arquivo aparece;

  • qual linha é indicada;

  • qual código de diagnóstico aparece;

  • qual descrição foi fornecida;

  • quantos erros foram relatados;

  • se existem warnings;

  • o que aparece na janela Output.

Dependendo da versão do Visual Studio, MSVC e das configurações utilizadas, os diagnósticos exatos podem variar.

Por isso não queremos decorar uma mensagem específica.

Queremos aprender a interpretar diagnósticos.

6. O que é um diagnóstico do compilador?

Quando o compilador encontra algo incompatível com as regras que consegue processar, ele produz informações para nos ajudar a localizar o problema.

Conceitualmente:

  Código-fonte
     ↓
Compilador
     ↓
Problema encontrado
     ↓
Diagnóstico

O diagnóstico poderá conter informações como:

  arquivo
linha
coluna
código
descrição

Algo conceitualmente parecido com:

  main.cpp
linha 5
error ...
expected ';'

A mensagem real pode variar.

O importante é começar a enxergá-la como informação técnica, não como uma tela vermelha que precisa desaparecer.

7. Error List versus Output

No Visual Studio existem duas áreas que começaremos a consultar com frequência.

Error List

A Error List organiza diagnósticos encontrados pelo ambiente e pelo processo de desenvolvimento.

Ela facilita visualizar:

  • erros;

  • warnings;

  • mensagens;

  • arquivos;

  • linhas.

É extremamente conveniente.

Mas existe outra janela importante.

Output

Durante o Build, a janela Output mostra informações produzidas pelo processo de construção.

Procure:

  Show output from:
Build

À medida que nossa jornada avançar, essa janela ficará cada vez mais importante.

Quando chegarmos a problemas de compilação, linking e configuração, não dependeremos apenas dos sublinhados vermelhos do editor.

8. O editor pode saber antes do Build?

Você provavelmente já percebeu que o Visual Studio consegue marcar alguns problemas enquanto digitamos.

Isso acontece porque a IDE possui mecanismos de análise que trabalham enquanto editamos o código.

Mas precisamos diferenciar conceitualmente:

  Editor / análise da IDE

de:

  Build real do projeto

Às vezes o editor poderá mostrar algo antes de compilarmos.

Em outros casos, o diagnóstico decisivo aparecerá durante o Build.

Essa distinção será particularmente importante quando começarmos a trabalhar com:

  • headers;

  • bibliotecas;

  • configurações;

  • linker;

  • diferentes targets;

  • Debug;

  • Release.

Por enquanto, guarde:

um sublinhado vermelho no editor e um erro produzido durante o Build estão relacionados, mas não são necessariamente a mesma fonte de diagnóstico.

9. Voltando ao nosso erro

Temos:

  std::cout << "Game Engineering com C++ começou!" << std::endl
return 0;

O compilador precisa determinar onde termina uma instrução e começa outra.

Nós removemos o terminador necessário.

Então o problema não é:

“return está errado."

O problema nasceu na instrução anterior.

Isso nos leva a uma lição importante.

10. A linha indicada nem sempre é a causa real

Imagine:

  std::cout << "Game Engineering com C++ começou!" << std::endl
return 0;

O compilador continua analisando o código depois de std::endl.

Ele encontra:

  return

e percebe que a construção anterior não foi encerrada como esperado.

Dependendo do diagnóstico, podemos receber uma indicação próxima à linha seguinte.

Isso significa que devemos distinguir:

  Lugar onde o compilador percebeu o problema

de:

  Lugar onde nós realmente causamos o problema

Eles podem coincidir.

Mas nem sempre.

Essa ideia será extremamente importante futuramente.

11. Causa versus sintoma

Vamos introduzir dois conceitos simples:

  CAUSA
  ↓
algo incorreto aconteceu
  SINTOMA
  ↓
algo observável apareceu por causa disso

Em nosso exemplo:

  CAUSA
↓
ponto e vírgula removido
  SINTOMA
↓
compilação falhou

E ainda podemos ter:

  CAUSA
↓
uma única construção inválida
  SINTOMAS
↓
vários diagnósticos posteriores

Quando trabalharmos com bugs reais, procurar a causa raiz será muito mais valioso do que atacar sintomas isoladamente.

12. Corrigindo o primeiro problema

Agora sim.

Restaure:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

Faça Build novamente.

Queremos retornar a:

  Build succeeded.

Mas não terminamos.

A correção não está completa apenas porque editamos o código.

Precisamos validar.

13. Correção e validação não são a mesma coisa

Nosso processo foi:

  Problema
   ↓
Investigação
   ↓
Alteração

Ainda falta:

  Validação

Faça:

  Ctrl + Shift + B

Depois:

  Ctrl + F5

Confirme que:

  1. o projeto compila;

  2. o executável inicia;

  3. a mensagem esperada aparece.

Agora podemos dizer que nossa correção foi validada.

Esse hábito será essencial quando estivermos corrigindo bugs maiores.

14. Nossa primeira metodologia de investigação

Acabamos de executar:

  1. Reproduzir
      ↓
2. Ler
      ↓
3. Localizar
      ↓
4. Criar hipótese
      ↓
5. Corrigir
      ↓
6. Compilar novamente
      ↓
7. Executar
      ↓
8. Validar

Essa ainda é uma metodologia simples.

Vamos refiná-la ao longo da série.

Mas ela já é muito superior a:

  deu erro
   ↓
mudar coisas aleatoriamente
   ↓
funcionou

15. Experimento 2 — quebrando o nome do header

Voltamos ao estado válido:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

Agora altere somente:

  #include <iostream>

para:

  #include <iostreax>

Faça Build.

Não existe um header padrão chamado iostreax disponível dessa forma em nosso ambiente.

Observe o diagnóstico.

16. O problema agora é diferente

No primeiro experimento, o compilador conseguiu chegar à instrução dentro de main, mas encontrou uma construção sintaticamente problemática.

Agora temos outra situação.

Estamos solicitando:

  #include <iostreax>

e o arquivo solicitado não pode ser encontrado nos caminhos utilizados para esse tipo de inclusão.

Conceitualmente:

  #include
   ↓
procure iostreax
   ↓
não encontrado
   ↓
processamento não pode continuar normalmente

Isso nos mostra algo importante:

mensagens diferentes representam problemas diferentes.

Não queremos desenvolver o hábito de pesquisar apenas:

“Visual Studio C++ error fix”

Queremos identificar qual categoria de problema temos diante de nós.

17. Não chame tudo de “erro do C++”

Durante a série encontraremos problemas em diferentes partes do sistema.

Por exemplo:

  Source Code
    ↓
Preprocessor
    ↓
Compiler
    ↓
Object Files
    ↓
Linker
    ↓
Executable
    ↓
Runtime

Além disso teremos:

  Game Logic
Rendering
Resources
File System
Operating System
GPU
Audio
Network

Dizer:

“C++ deu erro.”

quase não fornece informação.

Progressivamente queremos dizer coisas como:

“O compilador rejeitou esta construção.”

ou:

“O header não foi encontrado.”

ou futuramente:

“A compilação terminou, mas o linker não encontrou a definição.”

ou:

“O executável foi gerado, mas falhou durante a execução.”

Vocabulário técnico melhora nossa capacidade de investigar.

18. Corrigindo o header

Restaure:

  #include <iostream>

Faça Build.

Depois execute.

Voltamos ao estado conhecido:

  Build succeeded
↓
programa executa
↓
saída correta

Agora podemos avançar para outro tipo de problema.

19. Experimento 3 — removendo o #include

Em vez de escrever um nome incorreto, remova completamente:

  #include <iostream>

Teremos:

  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

Faça Build.

Observe.

20. O código parece quase igual

Visualmente removemos apenas uma linha.

Mas as instruções:

  std::cout

e:

  std::endl

dependem de declarações disponibilizadas através do header apropriado em nosso programa.

Sem:

  #include <iostream>

o compilador não possui o contexto necessário para utilizarmos esses nomes daquela maneira.

O erro nos ajuda a reforçar uma relação aprendida no capítulo anterior:

  <iostream>
    ↓
declarações de I/O
    ↓
std::cout
std::endl

Não estamos apenas decorando que #include <iostream> deve ficar no topo.

Estamos observando por que nosso código atual precisa dele.

21. Dependência já existe em nosso pequeno programa

Ainda não estudamos arquitetura de dependências.

Mas nosso primeiro programa já possui uma dependência.

Ele utiliza funcionalidades da biblioteca padrão.

Podemos representar:

  main.cpp
   ↓
iostream

Isso ainda é extremamente simples.

No futuro poderemos ter algo como:

  Game
 ↓
Engine
 ↓
Rendering
 ↓
SFML

ou outras relações que descobriremos durante o projeto.

A ideia de dependência começa em coisas pequenas.

22. Experimento 4 — quebrando std

Restaure o código válido.

Agora altere:

  std::cout

para:

  stx::cout

Teremos:

  #include <iostream>
  int main()
{
    stx::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

Faça Build.

Observe.

23. O compilador não corrige nossa intenção

Nós sabemos que queríamos:

  std

e digitamos:

  stx

Para um humano, parece óbvio.

Mas o compilador trabalha com aquilo que escrevemos.

Ele não deveria simplesmente substituir qualquer nome parecido pelo que imagina ser nossa intenção.

Imagine se tivéssemos legitimamente:

  namespace stx
{
}

em algum contexto futuro.

Corrigir silenciosamente nomes seria extremamente perigoso.

Programação exige precisão.

24. Experimento 5 — quebrando cout

Agora restaure:

  std::cout

e altere para:

  std::coutx

Faça Build.

Observe novamente.

Temos o namespace correto:

  std

mas estamos procurando um nome que não corresponde ao objeto que queríamos utilizar.

Podemos começar a perceber que diagnósticos carregam pistas sobre qual parte da expressão não pôde ser resolvida.

25. Experimento 6 — removendo uma aspas

Volte ao estado válido:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

Agora remova apenas a segunda aspas:

  std::cout << "Game Engineering com C++ começou! << std::endl;

Faça Build.

Observe cuidadosamente.

Esse tipo de erro pode provocar diagnósticos aparentemente mais confusos do que o simples ponto e vírgula.

Por quê?

Porque o compilador precisa interpretar onde nosso literal textual termina.

Nós abrimos:

  "

mas não fechamos corretamente.

Isso pode afetar a interpretação do restante da linha ou de partes posteriores do código.

26. Um erro pequeno pode afetar muito código

Esse princípio aparecerá várias vezes.

Um problema aqui:

  linha 5

pode gerar sintomas aqui:

  linha 5
linha 6
linha 7
linha 8

Por isso:

quantidade de mensagens não significa necessariamente quantidade de causas.

Se aparecerem dez erros depois de uma alteração, não comece corrigindo dez coisas.

Pergunte:

O que mudou?

Essa pergunta é extremamente poderosa.

27. “O que mudou?”

Imagine que o programa compilava cinco minutos atrás.

Agora não compila.

Uma das primeiras perguntas deveria ser:

O que mudou desde o último estado conhecido como funcionando?

Talvez:

  adicionei uma linha

ou:

  renomeei uma função

ou futuramente:

  adicionei uma biblioteca

ou:

  mudei de Debug para Release

ou:

  troquei x64 por x86

ou:

  atualizei uma dependência

Comparar estados é uma técnica fundamental de troubleshooting.

É também uma das razões pelas quais Git será tão valioso.

28. Git não serve apenas para guardar código

Imagine futuramente:

  Commit A
↓
projeto funcionando
  Commit B
↓
problema apareceu

A pergunta:

“O que mudou?”

pode ser respondida comparando os dois estados.

Git nos permitirá investigar:

  • arquivos modificados;

  • linhas alteradas;

  • commits;

  • histórico;

  • responsáveis;

  • regressões.

Ainda teremos uma fase específica para aprofundar Git.

Mas já conseguimos enxergar por que controle de versão é também uma ferramenta de engenharia e debugging.

29. Experimento 7 — removendo uma chave

Restaure o código.

Agora remova:

  }

Teremos:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;

Faça Build.

Observe.

No capítulo anterior aprendemos que:

  {
}

delimita o corpo da nossa função.

Abrimos:

  {

e não fechamos.

Nossa estrutura ficou incompleta.

30. Delimitadores precisam estar balanceados

Esse conceito aparecerá repetidamente.

Por exemplo:

  ()
{}
[]

Em muitos contextos, estruturas abertas precisam ser fechadas adequadamente.

Quando nosso código crescer:

  if (...)
{
    while (...)
    {
        if (...)
        {
        }
    }
}

perder uma chave poderá se tornar muito mais difícil de enxergar.

Indentação e formatação consistente ajudarão bastante.

31. O Visual Studio pode nos ajudar

Quando trabalhamos com chaves, o editor possui recursos visuais para mostrar correspondência entre delimitadores.

Também possui formatação e indentação automáticas.

Ferramentas são úteis.

Mas queremos compreender o conceito antes de depender completamente delas.

A ferramenta deve aumentar nossa capacidade.

Não substituir nosso entendimento.

32. Experimento 8 — escrevendo Return

Restaure a chave.

Agora altere:

  return 0;

para:

  Return 0;

Faça Build.

Observe.

C++ diferencia letras maiúsculas de minúsculas.

Ou seja:

  return

e:

  Return

não são a mesma coisa.

Isso é chamado de case sensitivity.

33. C++ é case-sensitive

Considere:

  player
Player
PLAYER

Para C++, esses podem representar identificadores diferentes.

Essa característica terá impacto direto em nossas convenções de nomenclatura.

Ao longo da série utilizaremos código em inglês e seguiremos um padrão consistente.

Por exemplo, quando criarmos funções:

  StartGame();
LoadResources();
CalculateDamage();

utilizaremos PascalCase.

Quando tivermos variáveis:

  playerHealth
playerEnergy
currentScene

utilizaremos camelCase.

E quando criarmos constantes:

  MAX_PLAYER_HEALTH
DEFAULT_WINDOW_WIDTH
DEFAULT_WINDOW_HEIGHT

utilizaremos UPPERCASE.

Ainda aprenderemos cada uma dessas construções quando chegar o momento apropriado.

Por enquanto, o importante é perceber que:

  PlayerHealth
playerHealth
PLAYER_HEALTH

não são apenas diferenças visuais.

Em C++, a capitalização faz parte do identificador.

34. Nossa convenção começa antes das classes existirem

Não precisamos criar uma classe apenas para demonstrar PascalCase.

Mas já podemos registrar nossa direção:

  Classes           → PascalCase
Interfaces        → PascalCase
Enums             → PascalCase
Funções/Métodos   → PascalCase
  Variáveis         → camelCaseConstantes        → UPPERCASE

E todos os identificadores técnicos serão escritos em:

  English

Enquanto nossas explicações continuarão em:

  Português do Brasil

A consistência será importante quando o projeto crescer.

35. Experimento 9 — mudando main

Agora faça algo diferente.

Altere:

  int main()

para:

  int Main()

Observe que acabamos de aplicar PascalCase visualmente.

Mas existe um problema conceitual.

main não é uma função qualquer criada por nós.

Ela possui significado especial como ponto de entrada da aplicação hospedada.

Portanto, nossa convenção de funções não substitui regras da linguagem.

Isso é muito importante.

36. Convenção não vence sintaxe ou contrato

Podemos definir:

“Nossas funções utilizam PascalCase.”

Então criaremos futuramente:

  LoadGame();
SaveGame();
UpdatePlayer();
RenderWorld();

Mas isso não significa renomear elementos especiais ou APIs externas para encaixá-los em nossa convenção.

Continuaremos utilizando:

  main()

porque esse nome possui significado definido pela linguagem.

Da mesma forma, futuramente utilizaremos APIs da biblioteca padrão ou da SFML respeitando seus nomes originais.

Nossa convenção governa o código que nós nomeamos.

Não reescreve C++.

37. Erro de sintaxe

Já encontramos vários exemplos.

Podemos começar a utilizar um termo:

erro de sintaxe.

Sintaxe envolve as regras estruturais utilizadas para escrever construções válidas da linguagem.

Por exemplo:

  return 0;

é válido.

Algo como:

  return 0

em determinado contexto pode produzir problema por faltar o terminador esperado.

Ou:

  std::cout << "Hello;

possui um literal malformado.

Uma analogia imperfeita, mas útil, seria pensar na gramática de uma língua.

As palavras podem existir, mas a maneira como são organizadas também precisa obedecer regras.

38. Nem todo problema será de sintaxe

Agora chegamos a uma distinção fundamental.

Considere:

  #include <iostream>
  int main()
{
    std::cout << "Player coins: 500" << std::endl;
    return 0;
}

O código pode compilar perfeitamente.

Mas imagine que, segundo nossa regra de jogo, o jogador deveria possuir:

  50 moedas

e não:

  500 moedas

O compilador não necessariamente possui como saber disso.

Para ele:

  "Player coins: 500"

é um literal válido.

Temos:

  Sintaxe válida
↓
Build succeeded
↓
Programa executa
↓
Resultado errado para nossa regra

Isso é outra categoria de problema.

39. Bug não significa necessariamente Build failed

Essa ideia é essencial.

Podemos ter:

  Build succeeded

e ainda possuir um software incorreto.

Imagine futuramente:

  Jogador possui 100 moedas
Compra item de 30
Saldo apresentado: 130

O programa:

  • compilou;

  • linkou;

  • abriu;

  • executou;

mas a regra está errada.

Temos um bug lógico.

O compilador verifica várias regras da linguagem.

Ele não conhece automaticamente todas as regras do nosso jogo.

40. O compilador não conhece nosso Game Design

Imagine:

  Semente custa: 10
Jogador possui: 50
Compra: 1 semente
Saldo esperado: 40

Se nossa implementação produzir:

  Saldo: 60

o compilador não vai necessariamente dizer:

“Alexander, sua economia está errada.”

Para o compilador, as operações utilizadas podem ser perfeitamente válidas.

Quem conhece a regra é nosso software — porque nós a implementamos — e quem precisa validá-la somos nós através de:

  • raciocínio;

  • testes;

  • debugging;

  • QA;

  • playtesting;

  • telemetria;

dependendo do problema.

É por isso que Game Engineering envolve muito mais do que simplesmente fazer o código compilar.

41. Uma primeira classificação de problemas

Ainda não conhecemos tecnicamente todas essas categorias.

Mas podemos construir um mapa inicial:

  Problemas de desenvolvimento
│
├── Compilação
│
├── Linking
│
├── Runtime
│
└── Lógica

Mais adiante esse mapa crescerá.

Hoje trabalhamos principalmente com problemas percebidos durante compilação.

Ainda não estamos prontos para estudar profundamente linker e runtime.

Eles terão seus próprios capítulos.

42. Compiler Error versus Warning

Você também encontrará:

  Error

e:

  Warning

Eles não são equivalentes.

De maneira inicial:

Error

Problema que impede aquela compilação de ser concluída com sucesso.

Warning

O compilador conseguiu continuar, mas detectou algo potencialmente problemático ou digno de atenção.

Não interprete warning como:

“Pode ignorar.”

Essa é uma péssima prática.

Um warning merece investigação.

43. “Mas o programa funciona mesmo com warning”

Essa frase aparecerá muitas vezes durante a vida de um desenvolvedor.

E precisa ser tratada com cuidado.

Algo funcionar em:

  meu computador
+
meu input
+
minha configuração
+
este momento

não prova que está correto.

Warnings podem revelar:

  • conversões perigosas;

  • perda de informação;

  • código suspeito;

  • uso incorreto;

  • problemas de portabilidade;

  • construções potencialmente inseguras.

Mais adiante estabeleceremos políticas de warnings para nosso projeto.

Por enquanto:

warning é informação técnica que deve ser compreendida, não ruído que deve ser escondido.

44. Não corrija código apenas para silenciar o compilador

Imagine que recebemos um warning.

Uma abordagem ruim seria procurar qualquer alteração que faça a mensagem desaparecer.

Queremos perguntar:

  Por que apareceu?
      ↓
Qual risco está indicando?
      ↓
Meu código está realmente correto?
      ↓
Qual correção expressa melhor a intenção?

Isso evita soluções do tipo:

“coloquei um cast e o warning sumiu.”

Talvez o cast seja correto.

Talvez apenas tenhamos escondido um problema.

Engenharia exige compreender a diferença.

45. Erros em cascata

Vamos voltar a um fenômeno que já observamos.

Um único erro pode gerar muitos diagnósticos.

Considere conceitualmente:

  Erro real na linha 10
        ↓
parser perde contexto
        ↓
linha 11 parece errada
linha 12 parece errada
linha 13 parece errada
linha 14 parece errada

Agora temos:

  5 mensagens

mas talvez apenas:

  1 causa

Isso é conhecido informalmente como uma cascata de erros.

46. Por onde começar quando existem muitos erros?

Uma estratégia inicial:

  1. observe os primeiros diagnósticos;

  2. procure a primeira região onde o código deixa de fazer sentido;

  3. verifique alterações recentes;

  4. corrija uma causa provável;

  5. faça Build novamente;

  6. veja quantos diagnósticos desapareceram.

Não tente necessariamente corrigir do último erro para o primeiro.

O último pode ser apenas consequência do primeiro.

47. Experimento 10 — criando uma cascata

Faça uma cópia do estado válido antes.

Depois transforme:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

em uma construção propositalmente malformada, por exemplo removendo aspas e terminadores de maneira controlada.

Faça Build.

Observe quantos diagnósticos aparecem.

Depois restaure apenas a primeira causa que você introduziu.

Faça Build novamente.

Veja quantas mensagens desaparecem.

Não queremos memorizar os códigos.

Queremos observar o comportamento do compilador diante de código malformado.

48. Um erro por vez durante o aprendizado

Durante nossos experimentos, prefira:

  1 alteração
↓
1 observação

em vez de:

  15 alterações
↓
caos

Essa abordagem também ajuda quando estamos aprendendo uma API.

Futuramente, ao integrar SFML, não faremos simultaneamente:

  configurar include
+
configurar libraries
+
criar Window
+
criar Game Loop
+
carregar Texture
+
criar Sprite
+
adicionar Audio

e só então tentar compilar.

Vamos validar etapas.

Isso reduz drasticamente o espaço de investigação.

49. Reproduzir o problema

Uma habilidade central em debugging é conseguir responder:

Eu consigo reproduzir o problema?

Nosso erro do ponto e vírgula é 100% reproduzível.

Sempre que temos:

  std::cout << "Hello" << std::endl
return 0;

o código está incorreto daquela maneira.

Mas bugs futuros poderão ser mais difíceis:

  acontece somente em Release

ou:

  acontece depois de trocar de mapa

ou:

  acontece quando o inventário está cheio

ou:

  acontece depois de jogar durante 40 minutos

ou:

  acontece somente em determinada máquina

Reprodução é uma parte essencial da investigação.

Estamos treinando isso em escala microscópica.

50. Reduzindo o problema

Outra técnica extremamente poderosa é reduzir.

Imagine futuramente um sistema com milhares de linhas.

Se conseguirmos transformar um bug complexo em:

  int main()
{
    // menor código capaz de reproduzir o problema
}

a investigação fica muito mais fácil.

Esse conceito é frequentemente chamado de criar um minimal reproducible example ou exemplo mínimo reproduzível.

Nosso projeto atual já é praticamente isso.

Cinco linhas são um excelente laboratório.

51. Leia a mensagem antes de pesquisar

Quando aparecer um erro, resista à sequência:

  copiar código do erro
↓
Google
↓
Stack Overflow
↓
copiar primeira resposta

Pesquisa é uma ferramenta excelente.

Documentação é essencial.

Comunidades são extremamente úteis.

IA também poderá ajudar.

Mas primeiro:

leia a mensagem.

Pergunte:

  • o que ela diz?

  • qual arquivo?

  • qual linha?

  • qual símbolo?

  • qual etapa?

  • o que eu alterei?

Depois pesquise com contexto.

52. Pesquisar melhor é consequência de compreender melhor

Compare:

  "C++ error help"

com:

  "MSVC expected semicolon before return"

ou futuramente:

  "MSVC unresolved external symbol x64 Debug"

Quanto melhor conseguimos classificar o problema, melhor conseguimos pesquisar.

Conhecimento técnico melhora até nossa capacidade de fazer perguntas.

53. E quando usarmos IA?

A mesma regra vale.

Uma pergunta ruim:

“Meu C++ não funciona. Arrume.”

Uma pergunta muito melhor:

“Estou compilando um projeto C++20 com MSVC no Visual Studio 2022 em Debug x64. Após esta alteração recebo este diagnóstico. Este é o código mínimo reproduzível. Explique a causa antes de sugerir a correção.”

Percebe a diferença?

IA não substitui capacidade de diagnóstico.

Um desenvolvedor que sabe fornecer contexto consegue extrair muito mais valor das ferramentas.

54. Não esconda o erro ao pedir ajuda

Quando alguém solicitar ajuda, envie a mensagem real.

Evite:

“Apareceu um negócio vermelho.”

Ou:

“Diz alguma coisa sobre símbolo.”

Ou:

“Está dando erro de DLL.”

A mensagem exata contém informação.

Mais adiante aprenderemos a coletar:

  mensagem
stack trace
logs
configuration
environment
steps to reproduce
expected result
actual result

Isso também faz parte da comunicação técnica.

55. Nosso primeiro processo formal de troubleshooting

Podemos agora definir uma versão inicial do processo que utilizaremos durante a série:

  1. IDENTIFICAR
   O que está errado?
  2. REPRODUZIR
   Consigo fazer acontecer novamente?3. OBSERVAR
   Qual é a mensagem ou comportamento?4. LOCALIZAR
   Onde o problema aparece?5. COMPARAR
   O que mudou desde o estado funcionando?6. CRIAR HIPÓTESE
   Qual pode ser a causa?7. TESTAR A HIPÓTESE
   Faça uma alteração controlada.8. CORRIGIR
   Resolva a causa.9. VALIDAR
   Build + execução + comportamento esperado.10. PREVENIR
    Como evitar ou detectar isso futuramente?

Esse processo crescerá.

Mas já podemos começar a utilizá-lo.

56. “Prevenir” parece exagerado para um ;

Para nosso ponto e vírgula, talvez sim.

Mas imagine futuramente:

  Bug
↓
save corrompido
↓
jogador perdeu 60 horas de progresso

Corrigir o bug é necessário.

Mas também perguntaremos:

Como impedir que isso aconteça novamente?

Talvez a resposta envolva:

  • teste automatizado;

  • validação;

  • backup;

  • versionamento do save;

  • checksum;

  • transação;

  • logging;

  • monitoramento.

Um bug pode produzir uma melhoria sistêmica.

Essa mentalidade começa agora.

57. Debugging não é adivinhação

Existe uma diferença enorme entre:

  "acho que é isso"

e:

  "minha hipótese é esta e vou executar um teste que pode confirmá-la ou rejeitá-la"

Debugging profissional é muito mais próximo da segunda abordagem.

Temos:

  Observação
   ↓
Hipótese
   ↓
Experimento
   ↓
Evidência
   ↓
Conclusão

É quase um pequeno método científico aplicado ao software.

58. Exemplo: formulando uma hipótese

Imagine:

  #include <iostream>
  int main()
{
    std::cout << "Hello" << std::endl
    return 0;
}

Observação:

  Build falhou.

Vemos um diagnóstico próximo à transição entre as duas instruções.

Hipótese:

A instrução anterior não foi terminada corretamente.

Experimento:

  std::cout << "Hello" << std::endl;

Novo Build:

  Build succeeded.

Evidência favorável à hipótese.

Isso é muito melhor que adicionar símbolos aleatoriamente.

59. Mas cuidado com falsas conclusões

Imagine que fizemos três alterações simultaneamente:

  A
B
C

e o problema desapareceu.

Qual delas resolveu?

Não sabemos.

Talvez:

  A resolveu
B não fez nada
C criou outro problema ainda invisível

É por isso que alterações pequenas e controladas são tão valiosas durante investigação.

60. O futuro debugger do Visual Studio

Até agora estamos usando “debugging” em sentido amplo: investigar problemas.

O Visual Studio também possui um debugger, uma ferramenta específica que futuramente nos permitirá:

  • definir breakpoints;

  • pausar execução;

  • avançar linha por linha;

  • inspecionar variáveis;

  • observar call stack;

  • analisar memória;

  • acompanhar fluxo;

  • avaliar expressões.

Ainda não temos variáveis interessantes nem fluxo suficiente para explorar toda essa ferramenta.

Quando surgir a necessidade, aprenderemos.

Novamente:

problema primeiro, ferramenta depois.

61. Compiler Error não precisa de breakpoint

Nosso programa nem chega a ser gerado corretamente quando existe determinado erro de compilação.

Portanto, colocar um breakpoint não resolverá um problema que impede a construção do executável.

Precisamos entender em qual etapa estamos.

Essa distinção ficará muito clara quando aprofundarmos:

  Source
 ↓
Preprocessor
 ↓
Compiler
 ↓
Linker
 ↓
Executable
 ↓
Runtime

Cada etapa possui tipos diferentes de problemas e ferramentas.

62. Game Engineering e troubleshooting

Pode parecer que estamos passando muito tempo falando de um ponto e vírgula.

Mas imagine alguns dos problemas que eventualmente poderemos encontrar:

  Texture não carrega
Sprite não aparece
Input não responde
Player atravessa parede
NPC fica preso
Save não abre
Audio toca duas vezes
Frame Time dispara
Memory cresce continuamente
Release funciona diferente de Debug
DLL não é encontrada
Linker não encontra símbolo

A reação não pode ser:

“vou mexer até funcionar.”

Precisamos construir disciplina de investigação antes de os problemas ficarem grandes.

63. Um bug visual também precisa de hipótese

Imagine futuramente:

“O personagem desapareceu.”

Possíveis hipóteses:

  Texture não carregou?
Sprite está fora da câmera?
Posição está errada?
Scale virou zero?
Objeto não foi renderizado?
Scene errada está ativa?
Resource foi destruído?
Alpha está zero?

O sintoma é um:

  personagem não aparece

As causas possíveis são muitas.

Debugging é o processo de reduzir esse espaço até encontrar evidência suficiente.

64. Logs entrarão justamente por causa disso

Hoje nosso projeto é tão pequeno que conseguimos observar praticamente tudo.

No futuro, talvez precisemos saber:

  Qual Scene carregou?
Qual Resource falhou?
Qual arquivo foi procurado?
Qual estado o Player possui?
Quanto tempo uma operação levou?
Qual sistema iniciou primeiro?

É aí que um sistema de Logging começará a fazer sentido.

Mas não criaremos Logger agora.

Hoje:

  main.cpp

continua suficiente.

Quando std::cout deixar de ser suficiente para diagnóstico, teremos um problema real que poderá justificar um sistema melhor.

65. Testes também nascerão dessa necessidade

Imagine um bug futuro:

  Player tem 100 moedas
Compra item de 30
Saldo vira 130

Corrigimos:

  Saldo agora é 70

Mas como garantimos que alguém não quebre essa regra novamente daqui a seis meses?

Uma resposta possível:

  teste automatizado

Por isso nosso Game Automation Framework também não nascerá porque “projetos profissionais têm testes”.

Ele nascerá porque teremos comportamento que queremos validar repetidamente.

66. Erros também ensinam arquitetura

Imagine que um sistema esteja tão acoplado que qualquer alteração quebra cinco módulos.

Isso é informação.

Talvez nossa arquitetura possua um problema.

Imagine que seja impossível testar economia sem abrir a janela gráfica.

Isso é informação.

Talvez responsabilidades estejam acopladas demais.

Imagine que carregar uma textura exija conhecer detalhes de cinco classes.

Isso é informação.

Problemas não servem apenas para encontrar linhas incorretas.

Eles também revelam qualidade estrutural.

67. Nosso código final do capítulo

Depois de todos os experimentos, restaure o estado simples e válido:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

Faça:

  Build

Confirme:

  succeeded

Depois execute.

Confirme:

  Game Engineering com C++ começou!

Terminamos exatamente com um programa simples.

Mas nossa capacidade de lidar com ele mudou.

68. Microdesafio — encontre os erros

Não execute imediatamente.

Observe:

  #include <iostrem>
  int Main()
{
    std::cout << "Game Engineering com C++ começou! << std::endl
    return 0;
}

Quantos problemas você consegue identificar utilizando apenas aquilo que estudamos?

Analise antes de corrigir.

69. Investigando o microdesafio

Podemos identificar pelo menos estes problemas relevantes:

1. Header incorreto

Temos:

  #include <iostrem>

quando pretendíamos utilizar:

  #include <iostream>

2. Ponto de entrada incorreto

Temos:

  int Main()

em vez de:

  int main()

Nossa convenção PascalCase não altera o nome especial main.

3. Literal não fechado

Temos:

  "Game Engineering com C++ começou!

sem a aspas de fechamento adequada.

4. Instrução sem ;

A expressão de saída também não termina corretamente com:

  ;

Agora vem a parte importante.

Não corrija tudo mentalmente e esqueça.

Experimente.

70. Corrija um problema por vez

Comece:

  #include <iostream>
  int Main()
{
    std::cout << "Game Engineering com C++ começou! << std::endl
    return 0;
}

Faça Build.

Observe o que mudou.

Depois:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou! << std::endl
    return 0;
}

Faça Build novamente.

Continue até chegar a:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

Faça Build.

Execute.

Esse processo é mais importante que a resposta pronta.

71. Desafio extra — produza um erro e documente

Escolha um dos experimentos deste capítulo.

Por exemplo:

  remover ;

ou:

  alterar iostream

ou:

  remover }

Agora escreva, para você mesmo:

  Problema:
O que alterei?
  Sintoma:
O que aconteceu?Mensagem:
O que o compilador informou?Hipótese:
Por que acredito que aconteceu?Correção:
O que alterei?Validação:
Como confirmei que voltou a funcionar?

Você acabou de produzir uma versão extremamente simples de um relatório técnico de investigação.

72. Esse modelo vai crescer conosco

Futuramente poderemos ter algo como:

  Título:
Player atravessa collision bounds ao mover na diagonal.
  Environment:
Windows 11
Debug x64
Build 0.8.14Steps to reproduce:
1. ...
2. ...
3. ...Expected:
Player permanece fora do obstáculo.Actual:
Player atravessa o canto.Evidence:
logs...
video...
coordinates...Root cause:
...Fix:
...Regression tests:
...

Hoje documentamos um ;.

A habilidade é a mesma.

A escala é que muda.

73. Precisamos alterar o projeto permanentemente?

Não.

Os erros deste capítulo foram experimentos didáticos.

Nosso estado final continua:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

Portanto, não precisamos criar um commit dizendo:

  fix: add missing semicolon

se nós removemos o ponto e vírgula propositalmente apenas durante o exercício e nunca registramos aquele estado.

Isso seria criar uma história falsa do software.

74. Git registra a evolução real, não nosso teatro didático

Essa distinção será importante durante toda a série.

No Medium podemos mostrar:

  quebrar
↓
investigar
↓
corrigir

para ensinar.

No repositório, o histórico deve representar mudanças relevantes do projeto.

Portanto:

  Experimento temporário
→ não precisa de commit
  Mudança real no software
→ commitCorreção de bug real
→ commitDocumentação relevante
→ commit

O GitHub não precisa reproduzir cada tecla utilizada no artigo.

75. CHANGELOG também não precisa ser alterado

Pelo mesmo motivo, não precisamos adicionar:

  Fixed missing semicolon.

ao CHANGELOG.md.

Não existia um bug real persistente no projeto.

Criamos o erro propositalmente e restauramos o estado.

Nosso CHANGELOG deve continuar descrevendo a evolução real do software.

76. O que evoluiu então?

Nós.

No começo do capítulo:

  Build failed
↓
"deu erro"

Agora queremos começar a pensar:

  Build failed
      ↓
Qual etapa?
      ↓
Qual diagnóstico?
      ↓
Qual arquivo?
      ↓
Qual região?
      ↓
O que mudou?
      ↓
Qual hipótese?
      ↓
Como testar?
      ↓
Qual é a causa?
      ↓
Como corrigir?
      ↓
Como validar?

Esse é um avanço técnico real, mesmo sem adicionarmos uma única classe.

77. Nossa estrutura continua mínima

Ao final:

  07_Game_Engineering_na_Pratica_CPP_SFML/
│
├── CppGameEngine/
│   └── main.cpp
│
├── README.md
├── CHANGELOG.md
├── CONTRIBUTING.md
└── ROADMAP.md

Ainda não temos:

  Core/
Engine/
Game/
Input/
Scene/
Resources/
Physics/
Tests/

E continuamos não precisando deles.

Estamos construindo fundamentos antes de construir abstrações.

78. O que aprendemos neste capítulo?

Começamos a diferenciar:

  erro

de:

  diagnóstico

Aprendemos a observar:

  • Error List;

  • Output;

  • arquivo;

  • linha;

  • mensagens;

  • alterações recentes.

Provocamos problemas envolvendo:

  • ;;

  • <iostream>;

  • ausência do header;

  • std;

  • cout;

  • aspas;

  • {};

  • capitalização;

  • main.

Começamos a compreender:

  • sintaxe;

  • case sensitivity;

  • causa;

  • sintoma;

  • erros em cascata;

  • reprodução;

  • hipótese;

  • validação;

  • warnings;

  • bugs lógicos;

  • troubleshooting.

E estabelecemos nossa primeira metodologia:

  IDENTIFICAR
    ↓
REPRODUZIR
    ↓
OBSERVAR
    ↓
LOCALIZAR
    ↓
COMPARAR
    ↓
CRIAR HIPÓTESE
    ↓
TESTAR
    ↓
CORRIGIR
    ↓
VALIDAR
    ↓
PREVENIR

Vamos utilizar essa sequência inúmeras vezes.

79. Ainda existe uma pergunta importante

Até agora fizemos:

  main.cpp
   ↓
Build
   ↓
CppGameEngine.exe

Falamos várias vezes em:

  compilador

Mas ainda estamos tratando Build quase como uma caixa-preta.

Pressionamos:

  Ctrl + Shift + B

e alguma coisa acontece.

Arquivos aparecem.

Um .exe é produzido.

Depois pressionamos:

  Ctrl + F5

e o programa executa.

Mas:

Build e Run são a mesma coisa?

Não.

E:

O código-fonte e o executável são a mesma coisa?

Também não.

Então está na hora de abrir um pouco mais essa caixa-preta.

Conclusão

Hoje fizemos algo que parece estranho à primeira vista:

quebramos um programa que já funcionava.

E fizemos isso de propósito.

Removemos símbolos.

Alteramos nomes.

Quebramos headers.

Desbalanceamos delimitadores.

Produzimos diagnósticos.

Mas nosso objetivo nunca foi simplesmente gerar erros.

Foi mudar nossa relação com eles.

Um erro não deve produzir automaticamente:

  Pânico
↓
Copiar mensagem
↓
Pesquisar solução
↓
Colar código
↓
Torcer

Queremos construir:

  Erro
 ↓
Observação
 ↓
Classificação
 ↓
Hipótese
 ↓
Experimento
 ↓
Evidência
 ↓
Correção
 ↓
Validação
 ↓
Aprendizado

Esse processo acompanhará toda a nossa evolução.

Quando encontrarmos nosso primeiro erro de linker, utilizaremos essa mentalidade.

Quando uma DLL não carregar, também.

Quando um sprite desaparecer.

Quando uma colisão falhar.

Quando um save corromper.

Quando um teste quebrar.

Quando um frame demorar demais.

Quando um bug aparecer somente em Release.

Os problemas ficarão maiores.

Nossa capacidade de investigá-los também.

E agora que já conseguimos escrever, executar, compreender e quebrar nosso primeiro código, podemos finalmente investigar melhor aquilo que acontece entre o arquivo que escrevemos e o programa que executamos.

Próximo capítulo

Capítulo 04 — Build, Run e Executable não são a mesma coisa

No próximo artigo vamos investigar três conceitos que até agora utilizamos quase como se fossem uma única operação:

  BUILD
RUN
EXECUTABLE

Vamos entender por que:

  Ctrl + S

não é:

  Ctrl + Shift + B

e por que nenhum deles é exatamente:

  Ctrl + F5

Vamos localizar o executável gerado pelo nosso projeto e começar a construir um modelo mais preciso:

  main.cpp
   ↓
processo de Build
   ↓
artefatos gerados
   ↓
CppGameEngine.exe
   ↓
execução
   ↓
processo em memória

Também começaremos a preparar o terreno para uma pergunta muito maior:

O que realmente acontece dentro de um Build C++?

Porque antes de encontrarmos compilador, object files e linker em profundidade, precisamos parar de tratar salvar, construir e executar como se fossem a mesma coisa.

Subscreve "Aeca" para receber atualizações diretamente na tua caixa de entrada
aeca

Subscreve aeca para reagir

Subscrever

Comentários

Ainda não há comentários. Sê o primeiro a comentar!

Subscreve Aeca para receber atualizações diretamente na tua caixa de entrada