Aeca

Capítulo 13 — Estados mais seguros com enum class

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

Capítulo 13 — Estados mais seguros com enum class
aeca

Quando números deixam de representar bem o significado do nosso jogo, podemos criar um tipo próprio para os estados válidos

No capítulo anterior aprendemos a utilizar switch quando uma mesma informação pode corresponder a várias opções discretas conhecidas.

Nosso exemplo final representava as estações do ano desta maneira:

  const int SPRING = 1;
const int SUMMER = 2;
const int AUTUMN = 3;
const int WINTER = 4;

int currentSeason = SUMMER;

Depois:

  switch (currentSeason)
{
    case SPRING:
        std::cout << "Spring" << std::endl;
        break;

    case SUMMER:
        std::cout << "Summer" << std::endl;
        break;

    case AUTUMN:
        std::cout << "Autumn" << std::endl;
        break;

    case WINTER:
        std::cout << "Winter" << std::endl;
        break;

    default:
        std::cout << "Unknown season." << std::endl;
        break;
}

Comparado aos números soltos:

  case 1:
case 2:
case 3:
case 4:

as constantes melhoraram bastante a leitura.

Agora enxergamos:

  SPRING
SUMMER
AUTUMN
WINTER

em vez de:

  1
2
3
4

Mas ainda existe um problema importante.

Nosso estado continua declarado assim:

  int currentSeason = SUMMER;

Para C++, currentSeason ainda é simplesmente:

  int

Isso significa que podemos escrever:

  currentSeason = 999;

E do ponto de vista do tipo:

  int recebe int

não existe nenhuma incompatibilidade.

Só que, no nosso domínio:

  999

não é uma estação.

Nosso código permite representar um estado que não deveria fazer sentido.

Hoje vamos melhorar isso.

Em vez de dizer:

currentSeason é um número inteiro.

queremos dizer:

currentSeason é uma estação.

Para isso conheceremos:

  enum class


1. O problema não está no switch

É importante localizar corretamente o problema.

Nosso switch:

  switch (currentSeason)
{
    case SPRING:
        break;

    case SUMMER:
        break;

    case AUTUMN:
        break;

    case WINTER:
        break;
}

não é o verdadeiro problema.

A limitação está aqui:

  int currentSeason;

Estamos utilizando um tipo extremamente genérico para representar um conceito muito específico.

Um int pode representar valores como:

  -500
0
1
42
999
100000

Mas nosso conceito de estação possui apenas quatro possibilidades:

  Spring
Summer
Autumn
Winter

Existe uma diferença entre:

  o que o tipo permite

e:

  o que o domínio considera válido

Queremos aproximar essas duas coisas.


2. Nomes melhores ajudaram, mas não resolveram tudo

No capítulo anterior fizemos:

  const int SPRING = 1;
const int SUMMER = 2;
const int AUTUMN = 3;
const int WINTER = 4;

Isso melhorou:

  int currentSeason = SUMMER;

em comparação com:

  int currentSeason = 2;

Agora conseguimos ler:

  currentSeason recebe Summer

em vez de tentar lembrar que:

  2 significa Summer

Mas ainda podemos escrever:

  currentSeason = 800;

A variável continua sendo um int.

As constantes melhoraram os nomes dos valores.

Ainda não criaram um tipo específico para o conceito.


3. Queremos representar um conjunto fechado de possibilidades

Nosso conceito possui:

  Season
│
├── Spring
├── Summer
├── Autumn
└── Winter

Não queremos qualquer número.

Queremos uma dessas opções nomeadas.

Esse tipo de problema aparece constantemente em jogos.

Por exemplo:

  Direction
├── North
├── South
├── East
└── West

Ou:

  GameState
├── Menu
├── Playing
├── Paused
└── GameOver

Ou:

  NpcState
├── Idle
├── Walking
├── Working
└── Sleeping

Quando temos um conjunto finito de valores nomeados, uma enumeração pode ser uma ótima representação.


4. Nossa primeira enum class

Podemos declarar:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

Leia isso conceitualmente como:

crie um tipo chamado Season que possui estas opções nomeadas.

Agora temos um novo tipo:

  Season

Ele não é simplesmente:

  int

para o código que utiliza essa enumeração.


5. Nossa convenção de nomes

Seguiremos nossas convenções de código.

O tipo:

  Season

utiliza:

  PascalCase

Os valores:

  Spring
Summer
Autumn
Winter

também utilizarão nomes claros em inglês e PascalCase.

Portanto:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

em vez de:

  enum class season
{
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER
};

Aqui não estamos tratando os enumeradores como nossas constantes nomeadas anteriores.

Estamos definindo valores pertencentes ao tipo Season.


6. Declarando uma variável do novo tipo

Antes:

  int currentSeason = SUMMER;

Agora:

  Season currentSeason = Season::Summer;

Podemos decompor:

  Season currentSeason = Season::Summer;
  │          │                │
  │          │                └── valor da enumeração
  │          │
  │          └── nome da variável
  │
  └── tipo

Isso comunica muito mais.


7. Leia o código como uma frase

  Season currentSeason = Season::Summer;

podemos interpretar como:

currentSeason é uma Season e atualmente possui o valor Summer.

Compare com:

  int currentSeason = 2;

Qual deles comunica melhor o domínio?

A diferença é grande.


8. O :: apareceu novamente

Já conhecemos:

  std::cout

e:

  std::string

Agora temos:

  Season::Summer

O operador:

  ::

é o operador de resolução de escopo.

Neste contexto:

  Season::Summer

indica que:

  Summer

é um valor pertencente ao escopo do tipo:

  Season

Da mesma maneira:

  Season::Winter

deixa explícito que estamos falando do Winter de Season.


9. Por que isso é melhor que nomes soltos?

Imagine que futuramente também tenhamos:

  enum class EventType
{
    Spring,
    Summer
};

Agora podemos ter:

  Season::Spring

e:

  EventType::Spring

Os nomes ficam qualificados por seus tipos.

Isso reduz colisões e deixa o código mais explícito.

Essa é uma das vantagens de enum class.


10. O problema do valor 999

Voltemos à nossa limitação.

Antes:

  int currentSeason = SUMMER;

currentSeason = 999;

Isso era perfeitamente compatível com o tipo.

Agora:

  Season currentSeason = Season::Summer;

currentSeason = 999;

não é uma atribuição válida.

Temos:

  Season

de um lado e:

  int

do outro.

O compilador agora consegue nos ajudar a proteger a intenção do código.


11. Faça o erro propositalmente

Experimente:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

int main()
{
    Season currentSeason = Season::Summer;

    currentSeason = 999;

    return 0;
}

Faça Build.

Observe o diagnóstico.

Não precisamos decorar a mensagem exata.

Pergunte:

  Qual é o tipo de currentSeason?

Qual é o tipo de 999?

Existe conversão implícita permitida aqui?

O que nossa declaração está tentando proteger?

Depois restaure:

  currentSeason = Season::Winter;


12. Tornando estados inválidos mais difíceis de representar

Esse é um princípio importante de Engenharia de Software.

Antes:

  int currentSeason;

permitia naturalmente:

  currentSeason = -10;
currentSeason = 123;
currentSeason = 999;

Agora:

  Season currentSeason;

faz com que o uso normal da variável trabalhe com valores como:

  Season::Spring
Season::Summer
Season::Autumn
Season::Winter

O sistema de tipos começou a representar melhor nossa regra.


13. Mas enum class não torna qualquer estado inválido literalmente impossível

Precisamos ser tecnicamente precisos.

É possível utilizar conversões explícitas para criar um valor de enumeração correspondente a um valor subjacente que não tenha um enumerador nomeado, por exemplo:

  Season currentSeason = static_cast<Season>(999);

Ainda não estudamos:

  static_cast

e não precisamos dele agora.

O ponto importante é:

enum class impede que inteiros arbitrários sejam atribuídos implicitamente como fazíamos antes.

Portanto, ela torna usos inválidos muito mais difíceis de acontecer acidentalmente.

Não devemos vender a ideia incorreta de que um enum class torna matematicamente impossível qualquer valor não enumerado em todas as circunstâncias.


14. enum class e switch trabalham muito bem juntos

Nosso switch anterior utilizava:

  int currentSeason

Agora podemos escrever:

  switch (currentSeason)
{
    case Season::Spring:
        std::cout << "Spring" << std::endl;
        break;

    case Season::Summer:
        std::cout << "Summer" << std::endl;
        break;

    case Season::Autumn:
        std::cout << "Autumn" << std::endl;
        break;

    case Season::Winter:
        std::cout << "Winter" << std::endl;
        break;
}

Agora cada case comunica exatamente qual estado representa.


15. Nossa implementação completa inicial

  #include <iostream>

enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

int main()
{
    Season currentSeason = Season::Summer;

    switch (currentSeason)
    {
        case Season::Spring:
            std::cout << "Spring" << std::endl;
            break;

        case Season::Summer:
            std::cout << "Summer" << std::endl;
            break;

        case Season::Autumn:
            std::cout << "Autumn" << std::endl;
            break;

        case Season::Winter:
            std::cout << "Winter" << std::endl;
            break;
    }

    return 0;
}

Resultado:

  Summer

Nosso código ficou mais próximo da linguagem do problema.


16. Antes e depois

Antes

  const int SPRING = 1;
const int SUMMER = 2;
const int AUTUMN = 3;
const int WINTER = 4;

int currentSeason = SUMMER;

Depois

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

Season currentSeason = Season::Summer;

Compare mentalmente:

  int

com:

  Season

O segundo tipo comunica uma informação que o primeiro não conseguia comunicar.


17. Não precisamos escolher números manualmente

Observe nossa enumeração:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

Não escrevemos:

  1
2
3
4

O compilador associa valores subjacentes aos enumeradores.

Para nosso código atual, não precisamos nos preocupar com quais números específicos estão por trás deles.

Nossa intenção está nos nomes.


18. Podemos definir valores explícitos?

Sim.

Seria possível escrever:

  enum class Season
{
    Spring = 1,
    Summer = 2,
    Autumn = 3,
    Winter = 4
};

Mas precisamos perguntar:

nosso programa realmente depende desses números?

Neste momento, não.

Então preferiremos:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

Menos detalhes desnecessários.


19. Quando valores explícitos podem fazer sentido?

No futuro poderemos encontrar situações como:

  protocolo externo
formato persistido
compatibilidade de arquivo
integração com API
formato binário
serialização versionada

em que valores numéricos explícitos tenham importância.

Aí definiremos esses valores conscientemente.

Não faremos isso antecipadamente.


20. enum class cria um tipo forte e separado

Imagine:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

enum class Direction
{
    North,
    South,
    East,
    West
};

Agora temos dois tipos diferentes:

  Season
Direction

Então isto não representa uma atribuição válida:

  Season currentSeason = Season::Summer;
Direction playerDirection = Direction::North;

currentSeason = playerDirection;

Mesmo que internamente enumerações possuam representações integrais, os tipos são distintos no código.

Isso é exatamente o tipo de proteção que queremos.


21. O compilador começa a reconhecer erros conceituais

Imagine uma função futura que receba:

  Season

e alguém tente passar:

  Direction

São conceitos diferentes.

Um sistema de tipos mais expressivo ajuda o compilador a detectar certas classes de erro.

Estamos começando a perceber uma ideia poderosa:

quanto melhor representamos nossos conceitos nos tipos, mais ajuda o compilador pode nos oferecer.


22. enum class não substitui bool

Suponha:

  bool isAlive = true;

Não precisamos criar:

  enum class AliveState
{
    Alive,
    Dead
};

apenas porque aprendemos enumerações.

Se uma condição realmente possui natureza booleana:

  true
false

bool pode continuar sendo a melhor representação.

De novo:

  nova ferramenta
≠
usar em todo lugar


23. Quando enum class começa a fazer sentido?

Pergunte:

  Existe um conjunto conhecido e limitado de estados?

Os estados possuem nomes significativos?

Um bool seria insuficiente?

Usar números ou strings esconderia o domínio?

Queremos impedir misturas acidentais entre conceitos diferentes?

Se as respostas apontarem nessa direção, enum class pode ser uma excelente opção.


24. std::string também não seria necessariamente melhor

Poderíamos escrever:

  std::string currentSeason = "Summer";

Parece legível.

Mas também podemos escrever:

  currentSeason = "Summmer";

com três letras m.

Ou:

  currentSeason = "banana";

A linguagem não sabe que essas strings são inválidas para o conceito de estação.

Com:

  Season currentSeason = Season::Summer;

trabalhamos com um conjunto específico de valores nomeados pelo tipo.


25. Comparando as três representações

Número genérico

  int currentSeason = 2;

Problemas:

  2 significa o quê?
Qualquer int pode ser atribuído.

Texto

  std::string currentSeason = "Summer";

Melhor para leitura, mas:

  qualquer texto pode ser atribuído
erros de digitação são possíveis
comparações dependem de strings

enum class

  Season currentSeason = Season::Summer;

Comunica:

  tipo específico
+
valores nomeados
+
menos conversões implícitas
+
melhor intenção

Para nosso problema atual, essa representação é muito mais adequada.


26. O switch agora se torna mais expressivo

Compare:

  switch (currentSeason)
{
    case 1:
        break;

    case 2:
        break;
}

com:

  switch (currentSeason)
{
    case Season::Spring:
        break;

    case Season::Summer:
        break;
}

A segunda versão praticamente documenta a própria regra.

Não precisamos procurar:

  qual número significa Summer?

O significado está diretamente no código.


27. Precisamos de default?

No capítulo anterior utilizamos:

  default:

para tratar números inesperados.

Agora nosso switch trabalha com:

  Season

e listamos:

  Spring
Summer
Autumn
Winter

Uma possibilidade seria:

  switch (currentSeason)
{
    case Season::Spring:
        break;

    case Season::Summer:
        break;

    case Season::Autumn:
        break;

    case Season::Winter:
        break;

    default:
        break;
}

Mas existe um detalhe interessante.


28. Em enums, default nem sempre é automaticamente a melhor escolha

Imagine que futuramente adicionemos:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter,
    StormSeason
};

Mas esqueçamos de alterar o switch.

Dependendo do compilador e das configurações de warning, um switch sobre uma enumeração sem default pode permitir que o compilador nos avise sobre um enumerador não tratado.

Se sempre tivermos:

  default:

esse tipo de diagnóstico pode ficar menos útil em determinadas configurações.

Portanto, para enumerações fechadas controladas por nós, não adotaremos a regra:

sempre coloque default.

A decisão dependerá do contexto.


29. Para nosso exercício, vamos tratar todos os valores nomeados

Podemos manter:

  switch (currentSeason)
{
    case Season::Spring:
        std::cout << "Spring" << std::endl;
        break;

    case Season::Summer:
        std::cout << "Summer" << std::endl;
        break;

    case Season::Autumn:
        std::cout << "Autumn" << std::endl;
        break;

    case Season::Winter:
        std::cout << "Winter" << std::endl;
        break;
}

Temos quatro enumeradores.

Temos quatro case.

A intenção é cobrir explicitamente todos os estados conhecidos.


30. Teste todos os estados

Comece:

  Season currentSeason = Season::Spring;

Esperado:

  Spring

Depois:

  Season currentSeason = Season::Summer;

Esperado:

  Summer

Depois:

  Season currentSeason = Season::Autumn;

Esperado:

  Autumn

Finalmente:

  Season currentSeason = Season::Winter;

Esperado:

  Winter

Uma enumeração com quatro estados nos dá quatro cenários naturais de validação.


31. Um exemplo um pouco mais contextual

Podemos aproveitar nosso futuro farming game sem construir o sistema de estações de verdade.

  #include <iostream>

enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

int main()
{
    Season currentSeason = Season::Autumn;

    std::cout << "=== CURRENT SEASON ===" << std::endl;

    switch (currentSeason)
    {
        case Season::Spring:
            std::cout << "Spring" << std::endl;
            std::cout << "Flowers begin to bloom." << std::endl;
            break;

        case Season::Summer:
            std::cout << "Summer" << std::endl;
            std::cout << "Days are longer and warmer." << std::endl;
            break;

        case Season::Autumn:
            std::cout << "Autumn" << std::endl;
            std::cout << "Leaves begin to fall." << std::endl;
            break;

        case Season::Winter:
            std::cout << "Winter" << std::endl;
            std::cout << "Cold weather reaches the farm." << std::endl;
            break;
    }

    return 0;
}

Resultado:

  === CURRENT SEASON ===
Autumn
Leaves begin to fall.


32. Isso ainda não é um sistema de estações

Precisamos manter nossa disciplina arquitetural.

Ainda não temos:

  SeasonSystem
Calendar
DayCycle
Weather
CropGrowth
WorldSimulation

Temos apenas:

  um tipo
+
uma variável
+
um switch

O exemplo contextualiza o conceito.

Não representa a arquitetura final do jogo.


33. Outro exemplo: direção

Podemos criar:

  enum class Direction
{
    North,
    South,
    East,
    West
};

Depois:

  Direction playerDirection = Direction::East;

Isso comunica muito melhor que:

  int playerDirection = 3;

Ou:

  std::string playerDirection = "East";

Novamente temos um conjunto fechado de estados nomeados.


34. Outro exemplo: estado do jogo

Futuramente poderemos chegar a algo como:

  enum class GameState
{
    MainMenu,
    Playing,
    Paused,
    GameOver
};

Isso pode ser muito útil.

Mas não criaremos agora:

  GameStateManager
SceneManager
StateMachine

Só porque já conseguimos imaginar esses nomes.

O tipo nasce quando o problema realmente existir.


35. Cuidado com enums gigantes

Também não queremos fazer:

  enum class Everything
{
    Player,
    Farm,
    Sword,
    Spring,
    Pause,
    Enemy,
    Coin,
    Tree
};

Uma enumeração deve representar um conceito coerente.

Por exemplo:

  Season

contém estações.

  Direction

contém direções.

  GameState

contém estados do jogo.

Boa modelagem continua importando.


36. enum class não elimina a necessidade de regras

Considere:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

Agora nosso tipo representa bem as estações disponíveis.

Mas ele não sabe automaticamente:

  qual estação vem depois de Spring?

quantos dias possui Summer?

quais crops crescem em Autumn?

quanto tempo dura Winter?

A enumeração representa os estados possíveis.

As regras envolvendo esses estados virão depois.

Não coloque comportamento inexistente dentro do conceito apenas porque agora temos nomes melhores.


37. E o tamanho em memória?

Talvez você encontre afirmações como:

  enum class ocupa 4 bytes

Isso pode acontecer em ambientes comuns, mas não devemos transformar isso em uma regra universal.

Enumerações possuem um underlying type, ou tipo subjacente.

Quando não especificamos um tipo subjacente para uma enum class, C++ utiliza int como tipo subjacente padrão.

Ainda assim, nossa decisão de utilizar enumeração aqui não está baseada em economia de memória.

Está baseada em:

  modelagem
clareza
type safety
intenção


38. Podemos controlar o tipo subjacente

C++ permite algo como:

  enum class Season : unsigned char
{
    Spring,
    Summer,
    Autumn,
    Winter
};

Mas não temos nenhuma necessidade real disso neste momento.

Escolher um tipo subjacente menor apenas porque sabemos que quatro estados cabem nele seria otimização e complexidade sem benefício demonstrado.

Então manteremos:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

Simples e claro.


39. Não precisamos pensar em memória antes do problema existir

Nosso programa possui uma variável:

  Season currentSeason;

Não temos milhões de entidades contendo essa informação.

Não medimos nenhum problema.

Portanto:

  não existe problema de memória demonstrado
↓
não existe otimização necessária

Quando desempenho e layout de dados importarem, mediremos.


40. Microdesafio — direção

Crie:

  enum class Direction
{
    North,
    South,
    East,
    West
};

Depois:

  Direction playerDirection = Direction::North;

Utilize:

  switch (playerDirection)

para imprimir:

  Moving North.
Moving South.
Moving East.
Moving West.

Teste todos os valores.


41. Uma possível solução

  #include <iostream>

enum class Direction
{
    North,
    South,
    East,
    West
};

int main()
{
    Direction playerDirection = Direction::East;

    switch (playerDirection)
    {
        case Direction::North:
            std::cout << "Moving North." << std::endl;
            break;

        case Direction::South:
            std::cout << "Moving South." << std::endl;
            break;

        case Direction::East:
            std::cout << "Moving East." << std::endl;
            break;

        case Direction::West:
            std::cout << "Moving West." << std::endl;
            break;
    }

    return 0;
}

Resultado:

  Moving East.


42. Microdesafio — misturando tipos errados

Crie também:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

enum class Direction
{
    North,
    South,
    East,
    West
};

Depois:

  Season currentSeason = Season::Summer;
Direction playerDirection = Direction::North;

Agora tente:

  currentSeason = playerDirection;

Faça Build.

Observe como o compilador rejeita a mistura.

Isso demonstra concretamente que:

  Season

e:

  Direction

são tipos diferentes.


43. Microdesafio — tente usar um inteiro

Com:

  Season currentSeason = Season::Spring;

tente:

  currentSeason = 3;

Compile.

Depois corrija:

  currentSeason = Season::Autumn;

Compare a intenção das duas formas.


44. Não faça casts apenas para derrotar o sistema de tipos

Se o compilador rejeitar:

  currentSeason = 3;

não pense imediatamente:

vou descobrir como forçar a conversão.

A pergunta primeiro deve ser:

por que estou tentando colocar um inteiro dentro de Season?

Talvez o problema esteja no modelo ou na origem dos dados.

Conversões explícitas têm usos legítimos.

Mas não devem ser utilizadas apenas para silenciar o compilador.


45. Código final do capítulo

Nossa evolução principal ficará assim:

  #include <iostream>

enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

int main()
{
    Season currentSeason = Season::Summer;

    std::cout << "=== CURRENT SEASON ===" << std::endl;

    switch (currentSeason)
    {
        case Season::Spring:
            std::cout << "Spring" << std::endl;
            std::cout << "Flowers begin to bloom." << std::endl;
            break;

        case Season::Summer:
            std::cout << "Summer" << std::endl;
            std::cout << "Days are longer and warmer." << std::endl;
            break;

        case Season::Autumn:
            std::cout << "Autumn" << std::endl;
            std::cout << "Leaves begin to fall." << std::endl;
            break;

        case Season::Winter:
            std::cout << "Winter" << std::endl;
            std::cout << "Cold weather reaches the farm." << std::endl;
            break;
    }

    return 0;
}

Resultado:

  === CURRENT SEASON ===
Summer
Days are longer and warmer.


46. Comparando a evolução completa

Começamos com:

  int currentSeason = 2;

Depois:

  const int SUMMER = 2;

int currentSeason = SUMMER;

Agora:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

Season currentSeason = Season::Summer;

Nossa evolução foi:

  NÚMERO
↓
NÚMERO COM NOME
↓
TIPO COM ESTADOS NOMEADOS

Essa é uma evolução real de modelagem.


47. O código ficou maior ou menor?

Se contarmos caracteres, talvez a enumeração tenha adicionado algumas linhas.

Mas Engenharia de Software não mede qualidade apenas pela menor quantidade de texto.

Agora o compilador sabe diferenciar:

  Season

de:

  Direction

E o leitor vê:

  Season::Summer

em vez de:

  2

Nosso código ficou mais expressivo.


48. Quando não usar enum class

Não crie uma enumeração apenas porque uma variável possui alguns valores atuais.

Por exemplo:

  int playerHealth = 100;

Não faria sentido criar:

  enum class Health
{
    Zero,
    One,
    Two,
    Three
};

Health é naturalmente um valor numérico no nosso modelo atual.

Da mesma maneira:

  float playerSpeed = 4.5f;

não precisa virar uma enumeração.

Use enum class quando o problema realmente envolver um conjunto discreto e nomeado de possibilidades.


49. Nossa arquitetura ainda continua mínima

Apesar de termos criado um novo tipo, nossa estrutura continua:

  CppGameEngine/
└── main.cpp

A declaração:

  enum class Season

pode continuar dentro de nosso arquivo atual para este estágio didático.

Não precisamos criar:

  Enums/
Domain/
GameStates/
Season/

por causa de uma enumeração.

Quando o projeto crescer e os tipos começarem a ser reutilizados em diferentes arquivos, reorganizaremos a estrutura conscientemente.


50. Validando a implementação

Faça:

  Ctrl + S

Depois:

  Ctrl + Shift + B

Confirme:

  Build succeeded

Execute:

  Ctrl + F5

Teste:

  Season currentSeason = Season::Spring;

Depois:

  Season currentSeason = Season::Summer;

Depois:

  Season currentSeason = Season::Autumn;

Finalmente:

  Season currentSeason = Season::Winter;

Todos os estados devem produzir o comportamento correspondente.


51. Testes negativos

Também queremos confirmar aquilo que não deve funcionar.

Tente:

  currentSeason = 999;

Esperado:

  erro de compilação

Tente criar:

  Direction playerDirection = Direction::North;

e depois:

  currentSeason = playerDirection;

Esperado:

  erro de compilação

Esses testes negativos demonstram uma vantagem importante do novo modelo.

O compilador está impedindo determinadas combinações incorretas.


52. Checklist de compreensão

Antes de avançar, tente responder:

  • Qual problema existia em int currentSeason?

  • Por que constantes nomeadas melhoraram, mas não resolveram completamente o problema?

  • O que é uma enumeração?

  • O que enum class Season cria?

  • Por que escrevemos Season::Summer?

  • O que significa :: nesse contexto?

  • Por que Season e Direction são tipos distintos?

  • Por que currentSeason = 999; não funciona mais implicitamente?

  • enum class torna literalmente impossível qualquer valor não enumerado?

  • Por que não utilizamos números explícitos para cada estação?

  • Por que não especificamos um tipo subjacente menor?

  • Quando enum class é uma representação adequada?

  • Quando bool, int, float ou outro tipo simples continua sendo melhor?

  • Por que switch combina tão bem com enumerações?

Se você consegue responder essas perguntas sem apenas repetir a sintaxe, o objetivo do capítulo foi atingido.


53. Git

Depois de validar:

  git status

Adicione:

  git add CppGameEngine/main.cpp

Ajuste o caminho caso sua estrutura local seja diferente.

Commit sugerido:

  git commit -m "refactor: replace season integers with enum class"

Observe que usamos:

  refactor

em vez de:

  feat

porque a principal mudança deste capítulo é melhorar a representação do conceito já existente sem introduzir uma funcionalidade de negócio completamente nova.

Nosso comportamento de estação continua essencialmente o mesmo.

A modelagem ficou melhor.


54. Atualizando o CHANGELOG

Em [Unreleased]:

  ### Changed

- Replaced integer-based season values with a strongly typed `Season` enum class.
- Updated season selection to use named `Season` values.
- Updated season `switch` cases to use scoped enumerators.

### Refactored

- Removed integer constants previously used to represent seasons.
- Improved type safety and semantic clarity for season state.

Depois:

  git add CHANGELOG.md
git commit -m "docs: update changelog with season enum refactor"

O ROADMAP técnico continua sem necessidade de alteração neste ponto.


55. O que aprendemos?

No capítulo anterior tínhamos:

  const int SPRING = 1;
const int SUMMER = 2;
const int AUTUMN = 3;
const int WINTER = 4;

int currentSeason = SUMMER;

Hoje evoluímos para:

  enum class Season
{
    Spring,
    Summer,
    Autumn,
    Winter
};

Season currentSeason = Season::Summer;

Aprendemos que:

  • uma enumeração representa um conjunto de valores nomeados;

  • enum class cria um tipo específico e com escopo;

  • Season::Summer deixa explícito a qual tipo aquele valor pertence;

  • Season e Direction não são intercambiáveis;

  • inteiros arbitrários não são atribuídos implicitamente a uma enum class;

  • o sistema de tipos pode impedir determinadas classes de erro;

  • enum class combina muito bem com switch;

  • valores explícitos para enumeradores só devem ser definidos quando houver motivo;

  • otimizar o tipo subjacente sem necessidade seria prematuro;

  • default em um switch sobre enumeração merece uma decisão consciente;

  • enumerações representam estados possíveis, não todas as regras relacionadas a esses estados;

  • tipos mais expressivos aproximam o código do domínio.


Conclusão

Nosso programa começou representando uma estação assim:

  int currentSeason = 2;

Isso funcionava.

Mas exigia conhecimento externo:

  2 significa Summer.

Depois melhoramos:

  const int SUMMER = 2;

int currentSeason = SUMMER;

Agora o código carregava um nome.

Mas o tipo ainda dizia:

  qualquer inteiro

Finalmente chegamos a:

  Season currentSeason = Season::Summer;

Agora nome e tipo trabalham juntos.

Podemos visualizar nossa evolução:

  2
↓
SUMMER
↓
Season::Summer

Cada etapa reduziu uma parte da ambiguidade.

Essa é uma ideia que aparecerá muitas vezes durante nossa jornada:

um bom tipo não serve apenas para armazenar dados; ele também comunica quais dados fazem sentido naquele contexto.

E isso é muito maior que enumerações.

Conforme avançarmos em C++, veremos o sistema de tipos sendo utilizado para representar cada vez melhor as regras e conceitos do nosso jogo.

Mas agora existe outro problema.

Nosso programa já sabe tomar decisões.

Consegue escolher entre múltiplas opções.

Possui estados nomeados.

Só que ainda executa suas instruções essencialmente uma vez.

Imagine que quiséssemos algo como:

  mostrar Day 1
mostrar Day 2
mostrar Day 3
mostrar Day 4
mostrar Day 5

Poderíamos escrever:

  std::cout << "Day 1" << std::endl;
std::cout << "Day 2" << std::endl;
std::cout << "Day 3" << std::endl;
std::cout << "Day 4" << std::endl;
std::cout << "Day 5" << std::endl;

Mas e se quisermos repetir uma ação:

  10 vezes?
100 vezes?
enquanto uma condição permanecer verdadeira?

Copiar e colar instruções claramente não será uma boa solução.

Precisamos ensinar nosso programa a repetir comportamento.


Próximo capítulo

Capítulo 14 — Loops com while

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

No próximo artigo começaremos com um problema extremamente simples:

  Day 1
Day 2
Day 3
Day 4
Day 5

e perceberemos que repetir manualmente:

  std::cout
std::cout
std::cout
std::cout
std::cout

não escala.

Queremos chegar a uma ideia como:

  ENQUANTO uma condição for verdadeira
↓
repita uma ação

Será o momento de conhecer:

  while

Vamos entender:

  • por que loops existem;

  • condição de repetição;

  • contador;

  • atualização do estado;

  • quando o loop termina;

  • o que acontece quando a condição já começa falsa;

  • por que esquecer de atualizar a condição pode gerar um loop infinito;

  • como investigar um programa que parece ter travado;

  • como decisões e repetições começam a trabalhar juntas;

  • e como esse conceito começará a preparar o terreno para algo central em jogos: repetir atualizações continuamente.

Ainda não construiremos nosso Game Loop real.

Primeiro precisamos entender o que significa repetir uma instrução de forma controlada.

Nosso programa já sabe escolher.

Agora ele aprenderá a repetir.

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