1. Introdução
O conjunto de dados de ontem era muito conveniente:
struct Task {
int id; // id da tarefa
std::string title; // Título da tarefa
bool done = false; // Status: feito/não feito
};
O problema começa no dia em que você quer adicionar um terceiro status: “em andamento”.
E aí o bool começa a falhar, porque ele só sabe “sim/não”, e nós queremos “três significados diferentes”.
O “fix rápido” mais comum de um iniciante é adicionar uma segunda flag:
struct Task {
int id;
std::string title;
bool done = false;
bool inProgress = false; // ambos podem ser true? ambos false?
};
O código compila. A lógica — nem tanto. Agora são possíveis estados:
edone = true
(a tarefa está feita e em andamento ao mesmo tempo… mecânica quântica, quem sabe),inProgress = true
edone = false
(algo como “todo”, mas isso já é um acordo “entre linhas”).inProgress = false
Quanto mais statuses — mais flags, e mais “combinações ilegais”.
“Vamos fazer um int status” — números mágicos entram em cena
A próxima ideia: “ah, eu só vou codificar”.
int status = 0; // 0=todo, 1=in_progress, 2=done (na cabeça)
status = 42; // o compilador não reclama, mas a lógica já chora
O problema aqui não é o tipo int em si. O problema é que o tipo não reflete o significado. Se você daqui a uma semana ver if (status == 2), isso não é código — é um enigma.
“Vamos fazer std::string status” — um erro vira novo status
Passar para strings não ajudou muito:
std::string status = "done";
status = "inprogres"; // erro de digitação -> “novo status” que ninguém combinou
Strings frequentemente resultam em um zoológico de variantes: "Done", "done", "DONE", "in progress", "in_progress"…
O que realmente precisamos
Precisamos de um tipo que diga: o valor só pode ser um entre um conjunto pré‑conhecido de opções. Isso são enums: enum e enum class. enum é a forma mais antiga, que já não é recomendada. Portanto vamos estudar diretamente o moderno enum class.
Uma pequena “tabela de resumo”:
| Como armazenar o status | Exemplo | Problema |
|---|---|---|
|
|
apenas 2 estados, depois começam os gambiarras |
|
|
números mágicos, fácil “acidental 42” |
|
|
erros de digitação criam novos “valores” |
|
|
type-safe e auto-documentado |
2. enum class: sintaxe básica e trabalhar com valores
Declaração
Com enum class você pode criar um novo tipo listando todos os seus possíveis valores:
enum class Status {
Todo,
InProgress,
Done
}; // sim, o ponto e vírgula é necessário
Aqui:
- Status — nome do tipo
- Todo, InProgress, Done — os elementos do enum (valores possíveis)
- ; após } é obrigatório (como em struct)
Criar, atribuir, comparar
O enum é um tipo “normal”: pode ser guardado em variável, atribuído, comparado.
enum class Status { Todo, InProgress, Done };
void Demo() {
Status s = Status::Todo; // criado
s = Status::InProgress; // atribuído
if (s == Status::InProgress) { // comparado
// fazemos algo
}
}
Um hábito a lembrar: em enum class os valores são escritos como Status::Todo, e não apenas Todo. Isso não é “ruído extra”, é controle para evitar conflitos de nomes.
Detalhe importante: enum class não se converte implicitamente para int ou bool
O enum antigo (enum) podia se converter para int e até ser usado em checagens como “verdade/falso”.
enum Status { Todo, InProgress, Done };
void Bad() {
Status s = Done;
int x = s;
if (s) { }
}
Muito conveniente e muito… problemático. No novo enum class isso foi proibido. Ou seja, o compilador não vai permitir que você trate o status como número ou como booleano acidentalmente.
3. Adicionando enum class ao nosso modelo Task
Reescrevemos Task: em vez de bool done vamos usar um status apropriado.
#include <string>
enum class Status { Todo, InProgress, Done };
struct Task {
int id;
std::string title;
Status status = Status::Todo; // status padrão
};
Agora criar tarefas fica bem mais “expressivo”:
Task a{1, "Buy milk"}; // status = Todo (padrão)
Task b{2, "Pay rent", Status::InProgress}; // especificado explicitamente
E um detalhe agradável: é impossível atribuir algo errado por acidente.
Task t{1, "Fix bug"};
// t.status = 2; // erro de compilação: exatamente o que queremos
4. Status e prioridades: mais um enum class
Já que aprendemos a expressar um “conjunto fechado de valores”, vamos adicionar outro conjunto: prioridade.
enum class Priority {
Low,
Normal,
High
};
E expandir o modelo Task. Boa notícia: podemos adicionar o campo de forma que o código antigo quase não quebre — basta dar um valor padrão.
#include <string>
enum class Status { Todo, InProgress, Done };
enum class Priority { Low, Normal, High };
struct Task {
int id;
std::string title;
Status status = Status::Todo;
Priority priority = Priority::Normal;
};
Agora podemos escrever assim:
Task a{1, "Buy milk"}; // Todo + Normal
Task b{2, "Pay rent", Status::InProgress}; // InProgress + Normal
Task c{3, "Fix prod", Status::Todo, Priority::High}; // Todo + High
E é aí que a type-safety fica quase “tangível”:
Task t{1, "Write report"};
t.status = Priority::High; // erro: tipos diferentes!
t.priority = Status::Done; // também erro
Se ambos os campos fossem int, esses erros seriam reais e desagradáveis.
5. O que é melhor: enum vs enum class
Em C++ existem dois estilos de enumeração:
- enum — unscoped (estilo antigo)
- enum class — scoped (estilo moderno, normalmente preferido)
Problema do enum comum: nomes “vazam” para fora
enum Color { Red, Green, Blue };
enum TrafficLight { Red, Yellow, Green }; // conflito de nomes Red/Green
Em um projeto grande isso vira uma fonte constante de “por que Red já está em uso?”.
Problema do enum comum: é fácil virar int
enum Color { Red, Green, Blue };
void Surprise() {
int x = Red; // compila
if (Green == 1) { // também compila (e é duvidoso)
// ...
}
}
Ou seja, enum pode “vazar” para números, e você volta a lidar com valores mágicos.
O que enum class oferece
Vantagens do enum class:
- mantém os nomes dentro do tipo (exemplo: Status::Done)
- não permite conversão implícita para número/booleano
Uma pequena folha de consulta:
| Propriedade | |
|
|---|---|---|
| Nomes dos valores | no scope atual | dentro do tipo () |
Converte implicitamente para |
frequentemente sim | não |
| Menos conflitos de nomes | não | sim |
| “Padrão” no C++ moderno | mais ou menos não | sim |
Regra prática do curso: se não tiver certeza — use enum class.
6. Valores numéricos dos elementos do enum
Valores “por padrão”
Se você não especificar números, os elementos normalmente começam em 0, 1, 2… (em ordem).
enum class Status { Todo, InProgress, Done };
// normalmente Todo=0, InProgress=1, Done=2
Mas ponto importante: isso é um detalhe de representação, não de significado. O sentido é escrito assim:
if (t.status == Status::Done) {
// tarefa pronta
}
e não assim:
if (code == 2) { ... } // isto nos leva de volta aos números mágicos
Códigos explícitos: quando realmente necessário
Às vezes você precisa de números estáveis (por exemplo, formato de arquivo/protocolo/integração com uma API externa). Então atribua valores explicitamente:
enum class Status {
Todo = 10,
InProgress = 20,
Done = 30
};
Se você atribui um valor a um elemento, o próximo sem valor continua a contagem:
enum class Status {
Todo = 10,
InProgress, // 11
Done // 12
};
Isso é conveniente, mas exige disciplina: se você introduz números — documente por que.
7. Underlying type: qual tipo é realmente armazenado no enum
O enum é armazenado na memória como um inteiro. O tipo desse inteiro chama-se underlying type (tipo base do enum). Na maioria dos casos é algo como int, mas às vezes é desejável especificar o tipo explicitamente.
Motivações comuns:
- compatibilidade com formatos externos (por exemplo, “no arquivo o status é 1 byte”)
- economia de memória em grandes arrays/tabelas
- desejo de limitar claramente o intervalo
Sintaxe: : std::uint8_t
Assim você substitui o int interno por std::uint8_t:
#include <cstdint>
enum class Status : std::uint8_t {
Todo,
InProgress,
Done
};
Existem dois pontos práticos aqui.
Primeiro, todos os valores precisam caber no tipo escolhido. Segundo, mesmo que Status passe a ter 1 byte, sizeof(Task) pode não diminuir por causa do alinhamento (padding). Isso não é bug, é geometria de memória.
Se quiser experimentar (e ver que o compilador é um organismo vivo, não uma calculadora), você pode imprimir tamanhos:
#include <iostream>
int main() {
std::cout << sizeof(Status) << '\n';
std::cout << sizeof(Task) << '\n';
}
Os resultados podem variar entre compiladores/plataformas — e isso é normal.
8. Como obter o valor numérico do enum
std::to_underlying — a forma “oficial” correta
No C++23 existe a função std::to_underlying, que retorna o valor underlying do enum.
#include <iostream>
#include <utility> // std::to_underlying
enum class Status { Todo, InProgress, Done }; // 0, 1, 2
int main() {
Status s = Status::Done;
std::cout << std::to_underlying(s) << '\n'; // 2
}
Isso é útil para depuração ou para “transportar” para um protocolo/arquivo (se for o acordo estabelecido).
Arm armadilha: std::uint8_t em cout pode ser impresso como caractere
Se o underlying type é std::uint8_t, na hora de imprimir você pode ver um “caractere”, e não o número. Porque uint8_t costuma se comportar como unsigned char.
#include <cstdint>
#include <iostream>
#include <utility>
enum class Status : std::uint8_t { Todo, InProgress, Done };
int main() {
auto code = std::to_underlying(Status::Done);
std::cout << static_cast<int>(code) << '\n'; // imprimir como número
}
Regra simples: se o underlying é “byte”, converta para int antes de imprimir.
Se std::to_underlying não estiver disponível: static_cast
Às vezes o ambiente está preso a um padrão antigo (ou simplesmente não usa C++23). Então o cast explícito salva:
enum class Status { Todo, InProgress, Done };
int code = static_cast<int>(Status::Done); // 2
O sentido é o mesmo: você diz explicitamente ao compilador “sim, eu quero o número”.
9. Zona perigosa: conversão de número para enum
Na verdade static_cast<Status>(42) é uma mina em potencial. Esse código compila:
enum class Status { Todo, InProgress, Done };
Status s = static_cast<Status>(42); // formalmente ok, logicamente — lixo
Você acabou de criar um valor de Status que não tem nome no enum. Isso quebra a ideia do “conjunto fechado de valores”.
enum class protege de erros acidentais, mas se você “arrombar a porta” via static_cast, o compilador não é obrigado a te salvar.
Sutileza com Status s{};
Value-initialization ({}) dá o underlying‑valor zero.
enum class Status { Todo, InProgress, Done };
Status ok{}; // underlying=0 -> isto é Todo
Mas se você definiu códigos que não começam em zero:
enum class Status { Todo = 10, InProgress = 20, Done = 30 };
Status bad{}; // underlying=0 -> isto NÃO é Todo, é um valor “sem nome”
Conclusão prática: se você quer um status padrão, declare‑o explicitamente. No modelo fazemos isso:
Status status = Status::Todo;
Se for necessário obter um enum a partir de um número — valide
Ainda não estudamos std::optional e “resultados de parsing corretos” (isso vem depois), mas o princípio pode ser mostrado de forma simples: verificar o número antes de atribuir.
enum class Status { Todo, InProgress, Done };
bool TryParseStatus(int code, Status& out) {
if (code == 0) { out = Status::Todo; return true; }
if (code == 1) { out = Status::InProgress; return true; }
if (code == 2) { out = Status::Done; return true; }
return false; // código desconhecido
}
Sim, aqui voltam a aparecer números — mas agora eles vivem em um só lugar e são usados apenas como “formato de entrada”, não como base da lógica por todo o programa.
10. using enum: escrever menos Status::...
O C++20 introduziu using enum, que permite “trazer” os elementos do enum para o escopo atual.
enum class Status { Todo, InProgress, Done };
void Demo() {
using enum Status; // Todo/InProgress/Done disponíveis sem Status::
Status s = Todo;
s = InProgress;
}
É conveniente, mas a boa prática é: use using enum localmente, dentro de funções. Se fizer no escopo global, é fácil ter conflitos e perguntar “quem é esse Todo e de onde ele veio?”.
11. Erros típicos ao trabalhar com enum / enum class
Erro #1: esquecer o ; depois da declaração do enum.
Os sintomas geralmente aparecem como “expected ‘;’ …” ou erros estranhos nas linhas seguintes. Cura simples: enum class {...}; — sempre com ponto e vírgula.
Erro #2: escrever Todo em vez de Status::Todo.
Em enum class os valores ficam dentro do tipo. Isso é feito de propósito para evitar conflitos e deixar o código claro. Se quiser algo mais curto, use using enum Status; localmente dentro da função.
Erro #3: tentar usar enum class como int ou bool.
enum class não se converte automaticamente para números ou valores lógicos. Isso não é capricho da linguagem, é proteção contra checagens sem sentido como if (status) ou atribuições como status = 2;.
Erro #4: construir a lógica de negócio sobre números 0/1/2.
Se você vê if (code == 2), significa que o enum “não fez seu trabalho” e você voltou aos números mágicos. O código correto deve comparar com Status::Done, não com 2.
Erro #5: fazer static_cast<Status>(número) sem validação.
Assim você pode obter um status “sem nome”, que não é igual a Todo, nem a InProgress, nem a Done. A partir daí o programa pode se comportar de forma estranha, porque você mesmo contornou a proteção do tipo.
GO TO FULL VERSION