Aeca

Capítulo 21 — Lifetime: existir não é a mesma coisa que estar acessível

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

Capítulo 21 — Lifetime: existir não é a mesma coisa que estar acessível
aeca

Escopo nos ensinou onde um nome pode ser utilizado. Agora precisamos entender durante quanto tempo o objeto associado a esse nome realmente existe.

No capítulo anterior estudamos escopo.

Aprendemos que uma variável como:

  int playerHealth = 100;

não fica automaticamente disponível em todo o programa.

Seu nome pertence a determinado contexto.

Vimos exemplos como:

  if (playerCoins < itemPrice)
{
    int missingCoins = itemPrice - playerCoins;

    std::cout << missingCoins << std::endl;
}

Dentro do bloco:

  missingCoins

pode ser utilizado.

Fora dele, não.

Também vimos que funções possuem seus próprios nomes locais:

  int CalculateHealthAfterDamage(int playerHealth, int damage)
{
    int newHealth = playerHealth - damage;

    return newHealth;
}

O newHealth dessa função não pode ser utilizado diretamente dentro de main().

Naquele capítulo nossa pergunta principal foi:

Onde esse nome pode ser encontrado e utilizado?

Agora precisamos fazer outra pergunta:

Durante quanto tempo o objeto associado a esse nome realmente existe durante a execução do programa?

Essa pergunta nos leva a um conceito central de C++:

lifetime, ou tempo de vida.


1. Escopo e lifetime não são a mesma coisa

Vamos começar pela distinção mais importante do capítulo.

De maneira simplificada:

  SCOPE
↓
onde um nome pode ser utilizado

Enquanto:

  LIFETIME
↓
durante qual período da execução
o objeto existe

Esses conceitos frequentemente caminham juntos em exemplos simples.

Mas eles não são sinônimos.

Essa diferença será fundamental quando chegarmos a:

  referências
ponteiros
objetos
construtores
destrutores
RAII
memória dinâmica
recursos

Por enquanto começaremos com variáveis locais simples.


2. Começando pelo exemplo mais simples

Considere:

  #include <iostream>

int main()
{
    int playerCoins = 10;

    std::cout << playerCoins << std::endl;

    return 0;
}

Temos:

  int playerCoins = 10;

Durante a execução de main(), chega um momento em que esse objeto é criado e inicializado.

Podemos representar:

  entramos em main()
↓
playerCoins é inicializado
↓
playerCoins existe
↓
utilizamos playerCoins
↓
saímos de main()
↓
playerCoins deixa de existir

Para esse exemplo simples, escopo e lifetime parecem quase a mesma coisa.

Mas precisamos observar melhor.


3. O objeto não existia antes da declaração

Considere:

  int main()
{
    std::cout << "Before" << std::endl;

    int playerCoins = 10;

    std::cout << playerCoins << std::endl;

    return 0;
}

Conceitualmente:

  entra em main()
↓
"Before"
↓
playerCoins é criado e inicializado
↓
playerCoins existe
↓
playerCoins é utilizado

Antes da inicialização:

  playerCoins ainda não possui seu lifetime iniciado

Para nossos tipos simples, podemos pensar didaticamente:

a vida útil começa quando o objeto é criado e sua inicialização é concluída.

Mais tarde veremos detalhes mais precisos para tipos complexos.


4. Saindo do bloco

Agora:

  #include <iostream>

int main()
{
    std::cout << "Before block" << std::endl;

    {
        int reward = 10;

        std::cout << "Reward: " << reward << std::endl;
    }

    std::cout << "After block" << std::endl;

    return 0;
}

Temos:

  Before block
Reward: 10
After block

Conceitualmente:

  entra no bloco
↓
reward é criado
↓
reward existe
↓
reward é utilizado
↓
sai do bloco
↓
lifetime de reward termina

Para essa variável local simples, sair do bloco encerra sua vida útil.


5. O nome deixa de estar disponível e o objeto deixa de existir

No exemplo:

  {
    int reward = 10;
}

depois das chaves não podemos fazer:

  std::cout << reward << std::endl;

No capítulo anterior explicamos isso pelo escopo.

Agora adicionamos outra informação.

Para essa variável local comum, ao sair do bloco:

  o nome deixa de estar no escopo
+
o objeto também encerra seu lifetime

Nos exemplos simples que estamos utilizando, os dois acontecimentos coincidem.

Mas não devemos concluir que isso sempre acontecerá em qualquer situação de C++.


6. Por que insistir nessa diferença?

Porque existem situações em que um objeto pode continuar existindo mesmo quando determinada forma de acessá-lo ficou escondida.

Considere o shadowing que vimos:

  #include <iostream>

int main()
{
    int value = 10;

    {
        int value = 20;

        std::cout << value << std::endl;
    }

    std::cout << value << std::endl;

    return 0;
}

Resultado:

  20
10

Durante o bloco interno, a variável externa com valor 10 não deixou de existir.

Ela apenas ficou ocultada pelo nome interno naquela resolução simples de value.

Esse é um excelente exemplo de que:

estar inacessível por determinado nome naquele ponto não significa necessariamente ter deixado de existir.


7. Visualizando o shadowing com lifetime

Podemos imaginar:

  value externo nasce
value externo = 10
│
│      entra no bloco
│      ↓
│      value interno nasce
│      value interno = 20
│      ↓
│      "value" encontra o interno
│      ↓
│      value interno morre
│
"value" volta a encontrar o externo
│
value externo continua existindo
│
fim de main
↓
value externo morre

O objeto externo permaneceu vivo durante todo o bloco interno.

Apenas não era o objeto encontrado quando escrevíamos:

  value

naquele contexto.

Aqui a diferença entre visibilidade do nome e lifetime do objeto fica muito mais clara.


8. Lifetime de variáveis locais

Nos exemplos que utilizamos até agora:

  int playerHealth = 100;

  float playerSpeed = 4.5f;

  bool isRunning = true;

são variáveis locais simples.

Quando são criadas dentro de um bloco, normalmente podemos pensar:

  entrada na região apropriada
↓
execução chega à declaração
↓
objeto é inicializado
↓
lifetime começa
↓
objeto é utilizado
↓
saímos do bloco
↓
lifetime termina

Essa é a primeira regra mental que precisamos dominar.


9. Variável dentro de if

Considere:

  if (playerHealth <= 0)
{
    int penalty = 10;

    playerCoins -= penalty;
}

O que acontece se a condição for falsa?

O bloco nem é executado.

Então:

  int penalty = 10;

não é alcançado.

Nesse caminho de execução, aquele objeto local não chega a ser criado.

Temos:

  condição false
↓
bloco não executa
↓
penalty não nasce


10. E quando a condição é verdadeira?

Se:

  playerHealth <= 0

for verdadeiro:

  entra no bloco
↓
penalty nasce
↓
penalty = 10
↓
é utilizado
↓
sai do bloco
↓
penalty morre

Lifetime depende do caminho real que a execução percorre.

Isso é importante.

O código fonte define possibilidades.

A execução concreta determina quais objetos realmente são criados naquele momento.


11. Variáveis dentro de loops

Agora considere:

  for (int frame = 1; frame <= 3; frame++)
{
    int reward = 5;

    std::cout << reward << std::endl;
}

Temos três iterações.

Uma pergunta interessante:

existe um único reward durante todo o loop?

Para a variável local declarada dentro do corpo:

  int reward = 5;

podemos pensar que a cada iteração a execução entra novamente naquele bloco e cria um novo objeto local para aquela iteração.


12. Visualizando as iterações

Temos:

  ITERAÇÃO 1
↓
reward nasce
reward = 5
↓
usa reward
↓
reward morre

ITERAÇÃO 2
↓
novo reward nasce
reward = 5
↓
usa reward
↓
reward morre

ITERAÇÃO 3
↓
novo reward nasce
reward = 5
↓
usa reward
↓
reward morre

Isso será extremamente importante quando começarmos a trabalhar com objetos que executam comportamento durante construção e destruição.

Por enquanto estamos usando int, então o processo não é visível no console.


13. Podemos tornar a criação conceitualmente visível

Vamos adicionar mensagens:

  #include <iostream>

int main()
{
    for (int frame = 1; frame <= 3; frame++)
    {
        std::cout << "Iteration " << frame << " started." << std::endl;

        int reward = frame * 10;

        std::cout << "Reward: " << reward << std::endl;

        std::cout << "Iteration " << frame << " finished." << std::endl;
    }

    return 0;
}

Resultado:

  Iteration 1 started.
Reward: 10
Iteration 1 finished.
Iteration 2 started.
Reward: 20
Iteration 2 finished.
Iteration 3 started.
Reward: 30
Iteration 3 finished.

Conceitualmente, cada reward pertence àquela iteração.


14. O contador do for possui outro lifetime

Observe:

  for (int frame = 1; frame <= 3; frame++)
{
    int reward = frame * 10;
}

O:

  frame

não é criado novamente da mesma maneira no início de cada execução do corpo.

Ele controla a estrutura inteira do for.

Já:

  reward

é declarado dentro do corpo.

Temos lifetimes diferentes dentro da mesma estrutura.

Conceitualmente:

  frame nasce
│
├── iteração 1
│   └── reward nasce e morre
│
├── iteração 2
│   └── reward nasce e morre
│
├── iteração 3
│   └── reward nasce e morre
│
frame morre ao terminar o for


15. Parâmetros também possuem lifetime

Considere:

  void ShowPlayerHealth(int playerHealth)
{
    std::cout << playerHealth << std::endl;
}

Quando fazemos:

  ShowPlayerHealth(100);

o parâmetro:

  playerHealth

passa a existir para aquela chamada da função.

Podemos pensar:

  chamada começa
↓
parâmetro é inicializado
↓
corpo executa
↓
função termina
↓
parâmetro encerra seu lifetime


16. Cada chamada possui sua própria execução

Imagine:

  ShowPlayerHealth(100);
ShowPlayerHealth(50);
ShowPlayerHealth(25);

Não existe um único parâmetro playerHealth sendo permanentemente alterado entre todas essas chamadas.

Cada chamada estabelece seu próprio contexto de execução.

Conceitualmente:

  CALL 1
playerHealth = 100
↓
termina

CALL 2
playerHealth = 50
↓
termina

CALL 3
playerHealth = 25
↓
termina

Isso ajuda a compreender por que funções podem ser reutilizadas com valores diferentes.


17. Variáveis locais da função também vivem durante a chamada

Considere:

  int CalculateHealthAfterDamage(int playerHealth, int damage)
{
    int newHealth = playerHealth - damage;

    return newHealth;
}

Durante uma chamada:

  CalculateHealthAfterDamage(100, 20);

podemos visualizar:

  chamada começa
↓
playerHealth nasce com 100
damage nasce com 20
↓
newHealth nasce com 80
↓
return newHealth
↓
newHealth encerra seu lifetime
playerHealth encerra seu lifetime
damage encerra seu lifetime
↓
chamada termina

Mas aí aparece uma pergunta importante.

Se:

  newHealth

morre quando a função termina...

como conseguimos receber o valor 80 fora dela?


18. Retornar uma variável local por valor é válido

Temos:

  int CalculateHealthAfterDamage(int playerHealth, int damage)
{
    int newHealth = playerHealth - damage;

    return newHealth;
}

E:

  int playerHealth = CalculateHealthAfterDamage(100, 20);

Isso é válido.

Por quê?

Porque estamos retornando o valor.

Não estamos pedindo ao chamador para continuar utilizando o objeto local newHealth depois que ele deixou de existir.

Conceitualmente:

  newHealth = 80
↓
return devolve o valor 80
↓
resultado da chamada recebe esse valor
↓
objeto local newHealth encerra seu lifetime


19. O nome local não sobrevive, mas o valor retornado pode continuar

Essa distinção é extremamente importante.

Dentro da função:

  int newHealth = 80;

Depois:

  return newHealth;

Fora da função:

  int playerHealth = CalculateHealthAfterDamage(...);

Não estamos transportando para fora o nome:

  newHealth

Estamos produzindo um resultado.

O chamador possui seu próprio objeto:

  playerHealth

inicializado com o valor devolvido.


20. Não precisamos imaginar uma cópia física obrigatória em todos os casos

Didaticamente podemos pensar:

  valor sai da função
↓
é entregue ao chamador

Em C++ moderno, o compilador pode realizar otimizações que evitam cópias intermediárias em diferentes situações.

Não precisamos entrar agora em:

  copy elision
move semantics
RVO
NRVO

Esses assuntos aparecerão muito mais tarde.

A regra útil para agora é:

retornar um valor local por valor é uma operação normal e segura.


21. Isso será diferente de referências e ponteiros

Futuramente poderemos encontrar construções que tentam fornecer acesso ao objeto local em si, em vez de simplesmente devolver seu valor.

Aí lifetime se tornará crítico.

Por exemplo, existirá uma grande diferença entre:

  devolver o valor

e:

  devolver alguma forma de acesso ao objeto que está prestes a morrer

Ainda não estudamos referências ou ponteiros.

Não vamos antecipar a sintaxe.

Mas já podemos entender o risco conceitual.


22. Uma analogia simples

Imagine uma função como uma pequena sala temporária.

Dentro dela existe:

  newHealth

escrito em um quadro.

Antes de sair da sala, alguém anota o número:

  80

e entrega essa informação para você.

Depois a sala é fechada.

O quadro desaparece.

Mas o número que você recebeu continua com você.

Isso representa razoavelmente bem o retorno por valor no nosso estágio atual.


23. Escopo e lifetime em uma função

Considere:

  int CalculateReward(int baseCoins, int bonusCoins)
{
    int reward = baseCoins + bonusCoins;

    return reward;
}

Temos:

  baseCoins
bonusCoins
reward

Scope

Esses nomes são utilizados dentro do contexto apropriado da função.

Lifetime

Para cada chamada, os objetos correspondentes existem durante seus respectivos períodos dentro daquela execução.

Quando a chamada termina, os locais automáticos deixam de existir.

O valor retornado pode ser usado pelo chamador.


24. Variáveis externas vivem mais que variáveis internas

Considere:

  int main()
{
    int playerCoins = 0;

    if (true)
    {
        int reward = 10;

        playerCoins += reward;
    }

    std::cout << playerCoins << std::endl;

    return 0;
}

Temos:

  playerCoins nasce
│
│  reward nasce
│  playerCoins += reward
│  reward morre
│
playerCoins continua existindo
│
playerCoins é mostrado
│
playerCoins morre

O objeto externo possui lifetime maior.


25. Isso explica por que podemos modificar playerCoins usando um valor temporário

Dentro:

  {
    int reward = 10;

    playerCoins += reward;
}

Depois que:

  reward

deixa de existir, o efeito da operação:

  playerCoins += reward;

já aconteceu.

playerCoins possui seu próprio lifetime e continua com:

  10

Não existe dependência contínua entre os dois objetos.


26. O resultado de uma expressão não precisa continuar "ligado" às entradas

Considere:

  int reward = 10;
playerCoins += reward;

Se depois reward desaparece:

  playerCoins

não volta automaticamente ao valor anterior.

Já calculamos e armazenamos um novo estado.

Isso parece óbvio agora, mas será importante quando começarmos a diferenciar valores de referências a objetos.


27. Lifetime e nosso mini Game Loop

Voltemos ao código:

  while (isRunning)
{
    int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);

    playerCoins += coinsReward;
}

O:

  playerCoins

foi criado antes do loop.

Então permanece existindo durante as diversas iterações.

Já:

  coinsReward

é criado dentro do corpo do loop.

Temos conceitualmente:

  playerCoins nasce
│
├── frame 1
│   └── coinsReward nasce e morre
│
├── frame 2
│   └── coinsReward nasce e morre
│
├── frame 3
│   └── coinsReward nasce e morre
│
...
│
loop termina
│
playerCoins continua
│
fim de main
↓
playerCoins morre

Isso é uma diferença real de lifetime.


28. Estado persistente versus estado temporário

Dentro de nosso mini Game Loop já podemos identificar duas categorias didáticas.

Estado que precisa atravessar várias iterações

  int playerHealth = 100;
int playerCoins = 0;
int frame = 0;

Essas variáveis precisam continuar existindo entre uma passagem e outra pelo loop.

Estado temporário de uma iteração

  int coinsReward = CalculateCoinsReward(...);

Esse valor só precisa existir durante aquela atualização.

A posição da declaração ajuda a representar essa intenção.


29. Isso começa a influenciar design

Se declarássemos:

  int coinsReward = 0;

while (isRunning)
{
    coinsReward = CalculateCoinsReward(...);

    playerCoins += coinsReward;
}

funcionaria.

Mas agora coinsReward vive em uma região maior do que o necessário.

Se ele só representa um resultado temporário da iteração, faz mais sentido:

  while (isRunning)
{
    int coinsReward = CalculateCoinsReward(...);

    playerCoins += coinsReward;
}

Escopo e lifetime menores podem comunicar melhor a responsabilidade.


30. Não mantenha objetos vivos sem necessidade

Esse princípio ficará muito mais importante quando nossos objetos deixarem de ser apenas:

  int
float
bool

Imagine futuramente objetos que possuam:

  texturas
arquivos
buffers
áudio
conexões
memória

Manter algo vivo significa potencialmente manter recursos associados a ele vivos também.

Ainda não chegamos lá.

Mas a base conceitual começa aqui:

um objeto deveria viver pelo período necessário para cumprir sua responsabilidade.


31. Lifetime será central para RAII

Mais tarde estudaremos uma das ideias mais importantes de C++:

  RAII

Resource Acquisition Is Initialization.

Sem antecipar o capítulo, a ideia dependerá profundamente de sabermos:

  quando um objeto nasce?
quando ele morre?
o que acontece nesses momentos?

Construtores e destrutores se conectarão diretamente ao lifetime.

Por isso estamos estudando esse conceito antes de memória avançada.


32. Objetos simples escondem parte do processo

Quando temos:

  int value = 10;

não vemos nenhuma mensagem aparecer quando o objeto começa ou termina sua vida.

Então lifetime pode parecer abstrato.

Mais tarde, uma classe poderá executar código em:

  construção
destruição

e o momento ficará observável.

Por enquanto precisamos construir o modelo mental sem depender disso.


33. Um bloco artificial para controlar lifetime

C++ permite criar um bloco simplesmente com:

  {
}

Por exemplo:

  #include <iostream>

int main()
{
    std::cout << "Before" << std::endl;

    {
        int temporaryValue = 10;

        std::cout << temporaryValue << std::endl;
    }

    std::cout << "After" << std::endl;

    return 0;
}

Neste caso as chaves não pertencem a:

  if
while
for

Ainda assim criam uma região de bloco.

Isso pode ser usado para limitar escopo e lifetime.


34. Não vamos criar blocos desnecessários

Só porque aprendemos que:

  {
}

pode limitar lifetime, não significa que devemos escrever:

  {
    int a = 10;
}

{
    int b = 20;
}

sem motivo.

Assim como em toda a série:

  nova ferramenta
≠
usar em todo lugar

Utilize um bloco adicional quando houver uma razão real de organização ou controle de lifetime.


35. O caso da variável declarada em if

Observe esta variação:

  if (playerHealth <= 0)
{
    int penalty = 10;

    playerCoins -= penalty;
}

Talvez você pense:

se penalty só existe durante o bloco, por que não escrever diretamente 10?

Poderíamos:

  playerCoins -= 10;

Mas:

  int penalty = 10;

pode dar significado ao valor.

A decisão não deve ser baseada apenas no lifetime.

Também pensamos em:

  clareza
intenção
legibilidade


36. Lifetime de um parâmetro por valor

Vamos observar novamente:

  void ShowPlayerHealth(int playerHealth)
{
    std::cout << playerHealth << std::endl;
}

Com:

  int playerHealth = 100;

ShowPlayerHealth(playerHealth);

Temos dois objetos diferentes para nosso modelo didático:

  playerHealth de main
↓
continua existindo antes, durante e depois da chamada

Enquanto:

  parâmetro playerHealth
↓
existe durante aquela chamada

Quando a função termina, o parâmetro termina seu lifetime.

O original em main() continua vivo.


37. Isso reforça a passagem por valor

Podemos representar:

  MAIN

playerHealth = 100
│
│ continua vivo
│
├──────────────┐
│              │
│     FUNCTION CALL
│     parâmetro nasce com 100
│     ↓
│     função executa
│     ↓
│     parâmetro morre
│
playerHealth original continua 100
│
fim de main
↓
original morre

Isso explica visualmente o comportamento observado no Capítulo 18.


38. E quando retornamos um valor?

Agora:

  int CalculateHealthAfterDamage(int playerHealth, int damage)
{
    int newHealth = playerHealth - damage;

    return newHealth;
}

e:

  playerHealth = CalculateHealthAfterDamage(playerHealth, 20);

Temos:

  original = 100
│
├── chamada começa
│   ├── parâmetro playerHealth nasce com 100
│   ├── damage nasce com 20
│   ├── newHealth nasce com 80
│   ├── valor 80 é retornado
│   └── locais encerram lifetime
│
resultado 80 é atribuído ao playerHealth original
│
original agora = 80

O fluxo começa a ficar muito mais claro.


39. Erro proposital — usar variável após o bloco

Teste:

  #include <iostream>

int main()
{
    {
        int reward = 10;

        std::cout << reward << std::endl;
    }

    std::cout << reward << std::endl;

    return 0;
}

Faça Build.

O compilador rejeita o segundo uso por causa do escopo.

Ao mesmo tempo, para nossa variável local simples, também sabemos que o objeto já terminou seu lifetime ao sair do bloco.

Temos dois problemas conceitualmente relacionados no mesmo ponto.


40. Erro proposital — variável de uma iteração

Teste:

  #include <iostream>

int main()
{
    for (int frame = 1; frame <= 3; frame++)
    {
        int reward = frame * 10;

        std::cout << reward << std::endl;
    }

    std::cout << reward << std::endl;

    return 0;
}

Novamente, o nome não está disponível depois do loop.

Além disso, não existe um único reward local do corpo que permaneceu vivo depois de todas as iterações.

Cada iteração criou seu objeto temporário.


41. Erro conceitual — querer que uma variável temporária preserve estado entre frames

Imagine:

  while (isRunning)
{
    int playerCoins = 0;

    playerCoins++;

    std::cout << playerCoins << std::endl;
}

Talvez você espere:

  1
2
3
4
...

Mas a variável é criada dentro do corpo a cada iteração e começa novamente com:

  0

Então veremos repetidamente:

  1
1
1
1
...

Encontramos um bug relacionado ao posicionamento e ao lifetime do estado.


42. Estado que precisa persistir deve nascer fora da iteração

Se queremos acumular:

  int playerCoins = 0;

while (isRunning)
{
    playerCoins++;

    std::cout << playerCoins << std::endl;

    // condição de saída...
}

Agora:

  playerCoins

vive durante todas as iterações.

A cada passagem:

  0 → 1 → 2 → 3 → ...

Esse é um exemplo extremamente importante para Game Engineering.


43. O estado do jogo precisa sobreviver entre frames

Imagine futuramente:

  Player Position
Health
Inventory
Current Scene
Game Time

Se esses estados fossem recriados do zero a cada iteração do Game Loop, perderíamos a evolução anterior.

O próprio conceito de jogo depende de determinadas informações continuarem existindo entre frames.

Isso conecta diretamente lifetime a estado.


44. Nem todo estado precisa sobreviver

Ao mesmo tempo, um cálculo temporário como:

  int coinsReward = CalculateCoinsReward(...);

não precisa sobreviver.

Temos então uma primeira distinção arquitetural:

  PERSISTENT GAME STATE
↓
precisa atravessar atualizações

versus:

  TEMPORARY CALCULATION STATE
↓
só precisa existir durante uma operação

Essa distinção crescerá muito ao longo do projeto.


45. Código final do capítulo

Vamos tornar essa diferença observável:

  #include <iostream>

int CalculateHealthAfterDamage(int playerHealth, int damage);
int CalculateCoinsReward(int baseCoins, int bonusCoins);
void ShowPlayerState(int frame, int playerHealth, int playerCoins);

int main()
{
    const int TOTAL_FRAMES = 3;
    const int DAMAGE_PER_FRAME = 20;
    const int BASE_COINS_REWARD = 1;
    const int BONUS_COINS_REWARD = 2;

    int playerHealth = 100;
    int playerCoins = 0;

    std::cout << "=== LIFETIME SIMULATION START ===" << std::endl;

    for (int frame = 1; frame <= TOTAL_FRAMES; frame++)
    {
        int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);

        playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE_PER_FRAME);
        playerCoins += coinsReward;

        ShowPlayerState(frame, playerHealth, playerCoins);
    }

    std::cout << "=== LIFETIME SIMULATION FINISHED ===" << std::endl;
    std::cout << "Final Health: " << playerHealth << std::endl;
    std::cout << "Final Coins: " << playerCoins << std::endl;

    return 0;
}

int CalculateHealthAfterDamage(int playerHealth, int damage)
{
    int newHealth = playerHealth - damage;

    if (newHealth < 0)
    {
        newHealth = 0;
    }

    return newHealth;
}

int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
    int reward = baseCoins + bonusCoins;

    return reward;
}

void ShowPlayerState(int frame, int playerHealth, int playerCoins)
{
    std::cout << "Frame: " << frame << std::endl;
    std::cout << "Health: " << playerHealth << std::endl;
    std::cout << "Coins: " << playerCoins << std::endl;
    std::cout << std::endl;
}

Resultado esperado:

  === LIFETIME SIMULATION START ===
Frame: 1
Health: 80
Coins: 3

Frame: 2
Health: 60
Coins: 6

Frame: 3
Health: 40
Coins: 9

=== LIFETIME SIMULATION FINISHED ===
Final Health: 40
Final Coins: 9


46. Mapeando os lifetimes do código final

Antes do loop:

  int playerHealth = 100;
int playerCoins = 0;

Esses objetos precisam persistir durante toda a simulação.

Conceitualmente:

  playerHealth nasce
playerCoins nasce
│
├── frame 1
├── frame 2
├── frame 3
│
simulação termina
│
ainda podem ser utilizados no resumo
│
fim de main
↓
ambos encerram lifetime


47. Lifetime do frame

Temos:

  for (int frame = 1; frame <= TOTAL_FRAMES; frame++)

O contador pertence à execução daquela estrutura de repetição.

Depois que o for termina, não precisamos dele.

Por isso seu lifetime fica naturalmente associado ao loop.


48. Lifetime de coinsReward

Dentro:

  {
    int coinsReward = CalculateCoinsReward(...);
}

temos:

  Frame 1
↓
coinsReward nasce com 3
↓
é usado
↓
morre

Frame 2
↓
outro coinsReward nasce com 3
↓
é usado
↓
morre

Frame 3
↓
outro coinsReward nasce com 3
↓
é usado
↓
morre

Não precisamos preservar coinsReward entre frames.


49. Lifetime dentro de CalculateCoinsReward

A cada chamada:

  CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD)

a função cria seu próprio contexto.

Temos:

  baseCoins
bonusCoins
reward

durante aquela execução.

Depois:

  return reward;

o valor é devolvido.

Os locais da função encerram seus lifetimes.

O chamador continua com o resultado.


50. Isso começa a criar um modelo mental de execução

Agora conseguimos olhar para:

  int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);

e imaginar:

  chamada
↓
parâmetros nascem
↓
reward nasce
↓
resultado é calculado
↓
valor retorna
↓
locais da função morrem
↓
coinsReward no chamador é inicializado
↓
chamador continua

Esse modelo será extremamente útil nos próximos níveis de C++.


51. Teste: colocando estado persistente no lugar errado

Faça uma experiência temporária.

Mova:

  int playerCoins = 0;

para dentro do loop:

  for (int frame = 1; frame <= TOTAL_FRAMES; frame++)
{
    int playerCoins = 0;

    playerCoins += CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);

    std::cout << playerCoins << std::endl;
}

Esperado:

  3
3
3

Em vez de:

  3
6
9

Explique por quê.

A variável está sendo recriada em cada iteração.


52. Teste: variável temporária fora do loop

Agora faça o inverso.

Coloque:

  int coinsReward = 0;

antes do loop.

Dentro:

  coinsReward = CalculateCoinsReward(...);

O comportamento pode continuar correto.

Mas pergunte:

coinsReward precisa realmente continuar existindo depois de cada iteração?

Se não, mantê-lo fora aumenta seu lifetime sem necessidade.

A questão não é apenas:

  funciona?

É também:

  esse lifetime representa corretamente a responsabilidade?


53. Testando funções repetidas

Faça:

  int rewardA = CalculateCoinsReward(1, 2);
int rewardB = CalculateCoinsReward(10, 5);

Temos duas chamadas diferentes.

Durante cada chamada existem seus próprios:

  baseCoins
bonusCoins
reward

Resultado:

  rewardA = 3
rewardB = 15

As variáveis locais de uma chamada não permanecem sendo reutilizadas pela próxima.


54. Boas práticas que já podemos aplicar

Neste estágio podemos adotar alguns princípios simples:

  • declare objetos o mais perto possível de onde sua responsabilidade começa;

  • não mantenha temporários vivos por regiões maiores sem necessidade;

  • estados que precisam atravessar iterações devem existir fora do corpo que é recriado a cada repetição;

  • parâmetros por valor vivem durante a chamada correspondente;

  • variáveis locais de função pertencem àquela execução;

  • retornar um valor local por valor é normal;

  • não confunda invisibilidade de um nome com inexistência de um objeto;

  • não crie acesso de longa duração a objetos locais sem compreender profundamente lifetime.

Esse último ponto será especialmente importante quando chegarmos a referências e ponteiros.


55. O que ainda não estamos estudando

Lifetime em C++ é um assunto muito maior.

Ainda veremos:

  automatic storage duration
static storage duration
dynamic storage duration
temporaries
references
pointers
constructors
destructors
RAII
smart pointers
ownership
move semantics
containers
resource lifetime

Não vamos colocar tudo no Capítulo 21.

Nosso objetivo atual é construir a base correta.


56. Uma precisão importante: lifetime não é simplesmente "estar alocado"

Às vezes uma explicação simplificada diz:

lifetime é o tempo em que a memória existe.

Isso pode ser enganoso.

Memória de armazenamento e lifetime de um objeto são conceitos relacionados, mas não idênticos.

Para o nível atual, prefira:

Lifetime é o período durante a execução em que um objeto existe como aquele objeto.

Mais tarde estudaremos armazenamento e memória com muito mais precisão.


57. Outra precisão: fim de escopo e fim de lifetime nem sempre coincidem

Nos exemplos de variáveis locais automáticas simples:

  {
    int value = 10;
}

o fim do bloco também encerra o lifetime daquele objeto.

Mas não transforme isso em:

  scope acabou
=
qualquer objeto necessariamente morreu

como regra universal da linguagem.

Já vimos um exemplo simples de separação conceitual com shadowing:

  int value = 10;

{
    int value = 20;
}

O objeto externo continuou vivo mesmo enquanto seu nome simples estava ocultado.

Mais tarde veremos casos ainda mais interessantes.


58. Lifetime é especialmente importante porque C++ permite controle fino

Em algumas linguagens muitos detalhes de gerenciamento são mais escondidos.

C++ nos oferece controle muito maior sobre:

  objetos
memória
recursos
destruição
ownership

Esse poder exige que entendamos bem:

  quem existe?
onde?
até quando?

É uma das razões pelas quais lifetime será um tema recorrente durante toda a série.


59. Relação com Game Engineering

Imagine futuramente:

  Texture
SoundBuffer
Window
Scene
Player
Enemy
Particle
Projectile
UI Element
File
Network Connection

Todos esses objetos terão:

  momento de criação
↓
período de uso
↓
momento de destruição

Se destruirmos cedo demais:

  algo ainda pode precisar deles

Se mantivermos vivos por tempo demais:

  podemos conservar recursos desnecessariamente

Se alguém tentar acessar um objeto depois do fim de seu lifetime:

  podemos entrar em bugs muito perigosos

Hoje estamos apenas construindo o vocabulário para compreender isso.


60. Nosso Game Loop já depende de lifetime correto

Veja:

  int playerHealth = 100;

while (isRunning)
{
    playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
}

playerHealth precisa sobreviver às várias iterações.

Se fosse recriado dentro:

  while (isRunning)
{
    int playerHealth = 100;

    playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
}

cada frame começaria novamente com:

  100

Nosso gameplay quebraria.

Lifetime não é apenas teoria de linguagem.

Ele já afeta o comportamento do jogo.


61. Checklist de compreensão

Antes de avançar, tente responder sem consultar o artigo:

  • O que é lifetime?

  • Qual diferença existe entre lifetime e escopo?

  • Quando uma variável local simples começa a existir?

  • O que acontece quando saímos de seu bloco?

  • Uma variável declarada dentro de um if existe quando o bloco não é executado?

  • O que acontece com uma variável declarada dentro do corpo de um loop a cada iteração?

  • O contador do for e uma variável dentro do corpo possuem exatamente o mesmo lifetime?

  • Quanto tempo vive um parâmetro por valor?

  • Quanto tempo vive uma variável local de função?

  • Como podemos retornar o valor de uma variável local se ela morre ao final da função?

  • O objeto local retornado continua existindo fora da função?

  • Por que um estado como playerCoins precisa existir fora do corpo do Game Loop?

  • Por que um valor temporário como coinsReward pode existir apenas durante uma iteração?

  • Como shadowing mostra que estar inacessível por um nome não significa necessariamente deixar de existir?

  • Por que lifetime será importante para referências e ponteiros?

Se consegue responder essas perguntas com suas próprias palavras, construímos a base necessária.


62. Estado atual da nossa jornada

Terminamos uma sequência muito importante de fundamentos:

  Capítulo 17
Funções
↓
comportamentos nomeados

Capítulo 18
Parâmetros
↓
dados entram

Capítulo 19
Valores de retorno
↓
resultados saem

Capítulo 20
Escopo
↓
onde nomes podem ser utilizados

Capítulo 21
Lifetime
↓
por quanto tempo objetos existem

Isso nos dá uma base muito mais sólida para continuar evoluindo em C++.


63. Estrutura do projeto

Ainda permanecemos:

  CppGameEngine/
└── main.cpp

Isso continua proposital.

Ainda não precisamos criar arquitetura artificial.

Mas agora nosso programa possui:

  funções
estado persistente
temporários
parâmetros
retornos
escopos
lifetimes diferentes

Nosso modelo mental está ficando preparado para estruturas de dados mais interessantes.


64. Git

Depois de validar os exercícios:

  git status

Adicione:

  git add CppGameEngine/main.cpp

Ajuste o caminho se sua estrutura local for diferente.

Commit sugerido:

  git commit -m "refactor: clarify object lifetime in simulation"

A ideia principal deste commit é tornar evidente a diferença entre:

  estado persistente

e:

  temporários de cada iteração


65. Atualizando o CHANGELOG

Em [Unreleased]:

  ### Refactored

- Kept persistent player state outside repeated simulation scopes.
- Limited temporary reward values to individual loop iterations.
- Clarified the lifetime of local calculation state.

### Added

- Added examples of local object lifetime inside blocks.
- Added loop iteration lifetime demonstrations.
- Added parameter lifetime examples.
- Added return-by-value lifetime explanations.
- Added scope versus lifetime comparison exercises.

Depois:

  git add CHANGELOG.md
git commit -m "docs: update changelog with lifetime concepts"

O ROADMAP técnico ainda não precisa ser alterado neste ponto.


66. O que aprendemos?

No capítulo anterior perguntamos:

  Onde esse nome pode ser utilizado?

Hoje perguntamos:

  Durante quanto tempo esse objeto existe?

Aprendemos que:

  • scope e lifetime são conceitos relacionados, mas diferentes;

  • scope trata da região em que um nome pode ser utilizado;

  • lifetime trata do período durante a execução em que um objeto existe;

  • objetos locais simples normalmente iniciam seu lifetime quando são criados e inicializados;

  • sair do bloco encerra o lifetime de variáveis locais automáticas comuns daquele bloco;

  • um bloco que não é executado não cria seus objetos locais;

  • objetos locais no corpo de um loop podem nascer e morrer a cada iteração;

  • o contador do for pode possuir lifetime diferente dos temporários de seu corpo;

  • parâmetros por valor pertencem à execução daquela chamada;

  • variáveis locais de função encerram seu lifetime quando a execução apropriada termina;

  • retornar o valor de uma variável local por valor é seguro;

  • o nome local não sobrevive, mas o resultado retornado pode continuar no chamador;

  • shadowing mostra que um objeto pode continuar vivo mesmo quando outro nome esconde seu acesso simples;

  • estado que precisa persistir entre frames precisa possuir lifetime compatível com essa necessidade;

  • temporários não precisam permanecer vivos além de sua responsabilidade;

  • lifetime será essencial para referências, ponteiros, objetos, RAII e gerenciamento de recursos.


Conclusão

Começamos nossa jornada com uma linha aparentemente simples:

  int playerHealth = 100;

Durante muitos capítulos nossa preocupação principal era o valor:

  100

Depois começamos a perguntar:

  Qual é o tipo?

Pode mudar?

Onde pode ser acessado?

Como vai para uma função?

Como volta?

Agora adicionamos uma pergunta ainda mais importante:

Quando esse objeto começa a existir e quando sua existência termina?

Isso muda nossa forma de enxergar código.

Considere:

  while (isRunning)
{
    int coinsReward = CalculateCoinsReward(...);

    playerCoins += coinsReward;
}

Agora enxergamos duas histórias diferentes.

Uma:

  playerCoins
↓
vive durante várias iterações
↓
acumula estado

Outra:

  coinsReward
↓
nasce em uma iteração
↓
é usado
↓
deixa de existir

Essa diferença é uma das bases de qualquer aplicação de jogo.

O mundo do jogo precisa preservar determinados estados:

  Health
Coins
Position
Inventory
Current State

ao longo do tempo.

Enquanto milhares de cálculos temporários podem surgir e desaparecer continuamente.

Compreender isso agora evitará muita confusão quando chegarmos à parte de C++ em que lifetime deixa de ser apenas um conceito didático e passa a determinar se o programa é seguro ou possui bugs difíceis de reproduzir.

Com esse capítulo encerramos nossa primeira grande fase de fundamentos.

Agora estamos prontos para começar a lidar com dados de maneira mais estruturada.

Até agora trabalhamos com textos apenas de forma pontual:

  std::cout << "Player defeated." << std::endl;

Mas um jogo precisa armazenar texto.

Por exemplo:

  Player Name
NPC Name
Item Name
Dialogue
Location
Quest Description
Messages

Precisamos representar esses textos como dados do programa.

E esse será nosso próximo passo.


Próximo capítulo

Capítulo 22 — Strings e texto

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

No próximo artigo iniciaremos a Fase 2 — Dados e Estruturas Fundamentais.

Partiremos de textos que até agora aparecem diretamente no código:

  std::cout << "Player defeated." << std::endl;

e evoluiremos para informações armazenadas:

  std::string playerName = "Alex";

Mas não vamos apenas apresentar std::string como uma linha para copiar.

Vamos entender:

  • por que texto precisa de uma representação própria;

  • diferença inicial entre um caractere e uma sequência de caracteres;

  • por que "Alex" e 'A' não representam a mesma coisa;

  • como incluir <string>;

  • como declarar e inicializar std::string;

  • atribuição de novos textos;

  • concatenação;

  • tamanho de uma string;

  • strings vazias;

  • comparação básica;

  • como texto começará a representar nomes e mensagens do nosso futuro jogo;

  • e por que precisamos compreender strings antes de avançar para coleções como arrays, std::array e std::vector.

Até agora nossos principais estados foram numéricos ou booleanos.

No próximo capítulo nosso programa começará a armazenar e transformar texto como dado real.

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