CodeGym /Cursos /C++ SELF /Conjunto fechado de valores: enum e enum class

Conjunto fechado de valores: enum e enum class

C++ SELF
Nível 19, Lição 1
Disponível

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:

  • done = true
    e
    inProgress = true
    (a tarefa está feita e em andamento ao mesmo tempo… mecânica quântica, quem sabe),
  • done = false
    e
    inProgress = false
    (algo como “todo”, mas isso já é um acordo “entre linhas”).

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
bool
done = true/false
apenas 2 estados, depois começam os gambiarras
int
status = 2
números mágicos, fácil “acidental 42”
std::string
"in_progress"
erros de digitação criam novos “valores”
enum class
Status::InProgress
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
enum
enum class
Nomes dos valores no scope atual dentro do tipo (
Status::Done
)
Converte implicitamente para
int
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.

1
Tarefa
C++ SELF, nível 19, lição 1
Bloqueado
Saudação do dia
Saudação do dia
1
Tarefa
C++ SELF, nível 19, lição 1
Bloqueado
Comando do semáforo
Comando do semáforo
1
Tarefa
C++ SELF, nível 19, lição 1
Bloqueado
Solicitação de suporte
Solicitação de suporte
1
Tarefa
C++ SELF, nível 19, lição 1
Bloqueado
Estado do player
Estado do player
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION