C++ Enums (enum class)

Um semáforo só tem três cores. Um açaí só vem em três tamanhos. Um pedido está "recebido", "preparando", "saiu para entrega" ou "entregue", e nada além disso. Quando uma informação só pode ter alguns valores fixos, o C++ oferece um tipo feito sob medida: o enum (de enumeration, "enumeração", uma lista numerada). Nesta aula você vai criar enums com enum class, usá-los com switch e ver por que o compilador consegue avisar quando você esquece uma opção.

Você vai aprender
  • Como criar uma lista de opções: enum class Cor { Vermelho, Amarelo, Verde };.
  • Como guardar e comparar opções: Cor::Verde.
  • Como usar um enum com switch, e o aviso -Wswitch quando falta um caso.
  • Que cada opção é um número por dentro, e como ver esse número com static_cast<int>.
  • O enum antigo, sem class, e os problemas que ele causa.
  • Como mostrar o nome de uma opção na tela (o C++ não faz isso sozinho).

Todos os programas desta aula foram compilados com o g++ 15 na máquina fictícia da Ana, com o comando padrão do guia: g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Você pode repetir cada um no editor do guia.


A ideia: um cardápio fechado

Imagine que a Ana guarda a cor do semáforo num std::string. Nada impede alguém de escrever "verde", "Verde", "VERDE" ou "verdi". São quatro textos diferentes, e o programa só descobre o erro (se descobrir) quando roda. Com um int é pior: 0 é vermelho? E o 7, o que é?

Um enum é um cardápio fechado: você escreve a lista de opções uma vez, e a variável só aceita uma delas. Errou o nome? O compilador avisa, antes de o programa existir.

Guardar a cor como…ExemploProblema
std::string"verde"Aceita qualquer texto, inclusive erros de digitação.
int2Ninguém sabe o que 2 quer dizer; aceita -50.
enum class CorCor::VerdeNenhum: só aceita as opções da lista, e o nome diz o que é.

1. Criar e usar um enum class

#include <iostream>

enum class Cor { Vermelho, Amarelo, Verde };

int main() {
    Cor sinal = Cor::Vermelho;

    if (sinal == Cor::Vermelho) {
        std::cout << "Pare!\n";
    }

    sinal = Cor::Verde;
    if (sinal != Cor::Vermelho) {
        std::cout << "Pode seguir.\n";
    }
}
enum class Cor
Cria um tipo novo chamado Cor. O class aqui não tem nada a ver com as classes do Nível 3: é só o jeito de dizer "enum moderno" (o C++11 reaproveitou a palavra).
{ Vermelho, Amarelo, Verde };
As opções, separadas por vírgula. Cada uma é chamada de enumerador. Como na struct, termina com ; depois da chave.
Cor sinal = Cor::Vermelho;
Cria uma variável do tipo Cor com a opção Vermelho. O Cor:: na frente é obrigatório: diz "o Vermelho que pertence a Cor". O :: se lê "de dentro de".
sinal == Cor::Vermelho
Compara opções com == e !=, como números.
sinal = Cor::Verde;
Troca a opção guardada.
$ g++ -std=c++23 -Wall -Wextra cor.cpp -o cor
$ ./cor
Pare!
Pode seguir.

Os nomes das opções seguem as mesmas regras dos nomes de variáveis. Neste guia, usamos a primeira letra maiúscula (Vermelho). E, como em todos os nomes do guia, sem acento: Medio, não Médio.

O compilador protege a lista

Esqueceu o Cor::? O g++ não acha o nome solto e até sugere o certo:

$ g++ -std=c++23 -Wall -Wextra semprefixo.cpp -o semprefixo
semprefixo.cpp: In function ‘int main()’:
semprefixo.cpp:4:17: error: ‘Verde’ was not declared in this scope; did you mean ‘Cor::Verde’?
    4 |     Cor sinal = Verde;
      |                 ^~~~~
      |                 Cor::Verde
semprefixo.cpp:1:37: note: ‘Cor::Verde’ declared here
    1 | enum class Cor { Vermelho, Amarelo, Verde };
      |                                     ^~~~~
semprefixo.cpp:4:9: warning: unused variable ‘sinal’ [-Wunused-variable]
    4 |     Cor sinal = Verde;
      |         ^~~~~

‘Verde’ was not declared in this scope; did you mean ‘Cor::Verde’? = "Verde não foi declarado aqui; você quis dizer Cor::Verde?". O aviso do fim só diz que a variável sinal não foi usada no programa (ele é curtinho de propósito).

E tentar guardar um texto numa Cor também não passa:

$ g++ -std=c++23 -Wall -Wextra texto.cpp -o texto
texto.cpp: In function ‘int main()’:
texto.cpp:6:17: error: cannot convert ‘const char*’ to ‘Cor’ in initialization
    6 |     Cor sinal = "Verde";
      |                 ^~~~~~~
      |                 |
      |                 const char*
texto.cpp:6:9: warning: unused variable ‘sinal’ [-Wunused-variable]
    6 |     Cor sinal = "Verde";
      |         ^~~~~

cannot convert ‘const char*’ to ‘Cor’ = "não dá para converter um texto em Cor". const char* é o tipo de um texto entre aspas (veja Strings). Uma Cor só aceita Cor::Vermelho, Cor::Amarelo ou Cor::Verde.


2. Enum e switch: a dupla perfeita

Para fazer uma coisa diferente para cada opção, o switch é o caminho natural: um case por opção.

#include <iostream>

enum class Cor { Vermelho, Amarelo, Verde };

void orientar(Cor sinal) {
    switch (sinal) {
        case Cor::Vermelho:
            std::cout << "Pare!\n";
            break;
        case Cor::Amarelo:
            std::cout << "Atenção, vai fechar.\n";
            break;
        case Cor::Verde:
            std::cout << "Pode seguir.\n";
            break;
    }
}

int main() {
    orientar(Cor::Vermelho);
    orientar(Cor::Amarelo);
    orientar(Cor::Verde);
}
$ g++ -std=c++23 -Wall -Wextra semaforo.cpp -o semaforo
$ ./semaforo
Pare!
Atenção, vai fechar.
Pode seguir.

O aviso -Wswitch: faltou uma opção

Aqui está o grande presente dos enums. Como o compilador conhece a lista inteira, ele percebe quando um switch esquece uma opção. Veja o mesmo programa sem o caso Amarelo:

#include <iostream>

enum class Cor { Vermelho, Amarelo, Verde };

void orientar(Cor sinal) {
    switch (sinal) {
        case Cor::Vermelho:
            std::cout << "Pare!\n";
            break;
        case Cor::Verde:
            std::cout << "Pode seguir.\n";
            break;
    }
}

int main() {
    orientar(Cor::Amarelo);
    std::cout << "Fim.\n";
}
$ g++ -std=c++23 -Wall -Wextra faltacaso.cpp -o faltacaso
faltacaso.cpp: In function ‘void orientar(Cor)’:
faltacaso.cpp:6:12: warning: enumeration value ‘Amarelo’ not handled in switch [-Wswitch]
    6 |     switch (sinal) {
      |            ^
$ ./faltacaso
Fim.                « o amarelo não mostrou nada

enumeration value ‘Amarelo’ not handled in switch = "a opção Amarelo não é tratada no switch". É um aviso, não um erro: o programa compila e roda, mas o amarelo passa em silêncio. O [-Wswitch] é o nome do aviso, ligado pelo -Wall (veja Avisos).

Isso vale ouro quando o programa cresce. Se um dia a Ana acrescentar Cor::Piscando ao enum, o g++ aponta todos os switch do programa que ainda não sabem lidar com a opção nova.

O default desliga esse aviso

Se o switch tiver um default:, o compilador considera que todas as opções estão tratadas e não avisa mais nada, nem quando você acrescentar uma opção nova ao enum. Num switch sobre enum, prefira escrever um case para cada opção e não usar default.

Missão: rode o programa e leia o aviso: o açaí grande está saindo por R$ 0! Acrescente o case que falta, para o grande custar 18.0. A missão só passa quando o aviso sumir.

#include <iostream>

enum class Tamanho { Pequeno, Medio, Grande };

double preco(Tamanho t) {
    switch (t) {
        case Tamanho::Pequeno:
            return 10.0;
        case Tamanho::Medio:
            return 14.0;
    }
    return 0.0;
}

int main() {
    std::cout << "Açaí pequeno: R$ " << preco(Tamanho::Pequeno) << '\n';
    std::cout << "Açaí grande: R$ " << preco(Tamanho::Grande) << '\n';
}
#include <iostream>

enum class Tamanho { Pequeno, Medio, Grande };

double preco(Tamanho t) {
    switch (t) {
        case Tamanho::Pequeno:
            return 10.0;
        case Tamanho::Medio:
            return 14.0;
        case Tamanho::Grande:
            return 18.0;
    }
    return 0.0;
}

int main() {
    std::cout << "Açaí pequeno: R$ " << preco(Tamanho::Pequeno) << '\n';
    std::cout << "Açaí grande: R$ " << preco(Tamanho::Grande) << '\n';
}

Atalho: using enum C++20

Repetir Cor:: em todo case cansa. Desde o C++20, using enum Cor; deixa os nomes das opções disponíveis sem o prefixo, só dentro do bloco onde ele aparece:

#include <iostream>

enum class Cor { Vermelho, Amarelo, Verde };

void orientar(Cor sinal) {
    switch (sinal) {
        using enum Cor;
        case Vermelho: std::cout << "Pare!\n"; break;
        case Amarelo:  std::cout << "Atenção, vai fechar.\n"; break;
        case Verde:    std::cout << "Pode seguir.\n"; break;
    }
}

int main() {
    orientar(Cor::Amarelo);
}
$ g++ -std=c++23 -Wall -Wextra usingenum.cpp -o usingenum
$ ./usingenum
Atenção, vai fechar.

Fora do switch, o prefixo continua obrigatório (por isso orientar(Cor::Amarelo) no main). Antes do C++20, escreva sempre Cor::Vermelho.


3. Por dentro, cada opção é um número

O computador não guarda a palavra "Verde": guarda um número inteiro. A primeira opção vale 0, a segunda 1, e assim por diante. O enum class esconde isso de propósito, mas você pode pedir o número com static_cast<int> (veja Conversão de Tipos). Também dá para escolher os números:

#include <iostream>

enum class Cor { Vermelho, Amarelo, Verde };
enum class Nivel { Baixo = 1, Medio = 5, Alto = 10 };

int main() {
    std::cout << static_cast<int>(Cor::Vermelho) << ' '
              << static_cast<int>(Cor::Amarelo) << ' '
              << static_cast<int>(Cor::Verde) << '\n';

    std::cout << static_cast<int>(Nivel::Medio) << '\n';

    int lido = 10;
    Nivel n = static_cast<Nivel>(lido);
    if (n == Nivel::Alto) {
        std::cout << "Nível alto!\n";
    }
}
$ g++ -std=c++23 -Wall -Wextra numero.cpp -o numero
$ ./numero
0 1 2
5
Nível alto!
static_cast<int>(Cor::Verde)
"Converta para int": dá o número da opção, 2.
enum class Nivel { Baixo = 1, Medio = 5, Alto = 10 };
Escolhe o número de cada opção. Útil quando o número tem significado fora do programa (um código de um arquivo, uma pontuação).
static_cast<Nivel>(lido)
O caminho inverso: de número para opção. Serve, por exemplo, para transformar um número lido do teclado numa opção.

Sem o static_cast, o enum class não vira número sozinho:

$ g++ -std=c++23 -Wall -Wextra semcast.cpp -o semcast
semcast.cpp: In function ‘int main()’:
semcast.cpp:6:18: error: cannot convert ‘Cor’ to ‘int’ in initialization
    6 |     int x = Cor::Verde;
      |             ~~~~~^~~~~
      |                  |
      |                  Cor

Isso é proposital: uma cor não é um número, e o compilador não deixa você misturar os dois sem querer. Quando você precisa do número, o static_cast deixa claro que foi de propósito.

O static_cast de número para enum não confere nada

static_cast<Nivel>(7) compila sem aviso, mas 7 não é nenhuma opção da lista: a variável fica com um valor que nenhum case trata. Se o número vem de fora (teclado, arquivo), confira antes se ele é válido (veja Validar a Entrada).

Experimente: troque os números do Nivel (por exemplo, Medio = 50) e veja o que muda na saída. Depois mude lido para 7 e repare que "Nível alto!" some, sem nenhum erro.

#include <iostream>

enum class Cor { Vermelho, Amarelo, Verde };
enum class Nivel { Baixo = 1, Medio = 5, Alto = 10 };

int main() {
    std::cout << static_cast<int>(Cor::Vermelho) << ' '
              << static_cast<int>(Cor::Amarelo) << ' '
              << static_cast<int>(Cor::Verde) << '\n';

    std::cout << static_cast<int>(Nivel::Medio) << '\n';

    int lido = 10;
    Nivel n = static_cast<Nivel>(lido);
    if (n == Nivel::Alto) {
        std::cout << "Nível alto!\n";
    }
}

4. O enum antigo (sem class)

Antes do C++11, só existia o enum "simples", herdado da linguagem C. Você vai encontrá-lo em muito código e em bibliotecas antigas:

#include <iostream>

enum Cor { Vermelho, Amarelo, Verde };

int main() {
    Cor sinal = Verde;
    int x = sinal;
    std::cout << x << '\n';
    std::cout << sinal + 10 << '\n';
}
$ g++ -std=c++23 -Wall -Wextra antigo.cpp -o antigo
$ ./antigo
2
12

Parece mais prático: não precisa do Cor::, e vira número sozinho. Esses são justamente os dois problemas.

Problema 1: os nomes "vazam"

No enum antigo, Vermelho, Amarelo e Verde ficam soltos no programa, como se fossem variáveis comuns. Se outro enum também tiver um Verde (a fruta verde, por exemplo), os nomes batem:

enum Cor { Vermelho, Amarelo, Verde };
enum Fruta { Laranja, Limao, Verde };

int main() {
    Cor c = Verde;
}
$ g++ -std=c++23 -Wall -Wextra vazamento.cpp -o vazamento
vazamento.cpp:2:30: error: ‘Verde’ conflicts with a previous declaration
    2 | enum Fruta { Laranja, Limao, Verde };
      |                              ^~~~~
vazamento.cpp:1:31: note: previous declaration ‘Cor Verde’
    1 | enum Cor { Vermelho, Amarelo, Verde };
      |                               ^~~~~
vazamento.cpp: In function ‘int main()’:
vazamento.cpp:5:9: warning: unused variable ‘c’ [-Wunused-variable]
    5 |     Cor c = Verde;
      |         ^

‘Verde’ conflicts with a previous declaration = "Verde entra em conflito com uma declaração anterior": o nome já existia, vindo do enum Cor. No editor do guia, o Clang diz redefinition of enumerator 'Verde' ("redefinição do enumerador"). Com enum class, cada lista tem os seus próprios nomes, e Cor::Verde e Fruta::Verde convivem em paz: o mesmo programa com enum class compila (só com o aviso de variáveis não usadas).

Problema 2: vira número sem você pedir

O enum antigo se converte em int sozinho. Por isso sinal + 10 deu 12 lá em cima, o que não faz sentido para uma cor. E dá para comparar coisas que não têm nada a ver:

#include <iostream>

enum Cor { Vermelho, Amarelo, Verde };
enum Tamanho { Pequeno, Medio, Grande };

int main() {
    Cor c = Vermelho;
    Tamanho t = Pequeno;
    if (c == t) {
        std::cout << "Vermelho é igual a Pequeno?!\n";
    }
}
$ g++ -std=c++23 -Wall -Wextra mistura.cpp -o mistura
mistura.cpp: In function ‘int main()’:
mistura.cpp:9:11: warning: comparison between ‘enum Cor’ and ‘enum Tamanho’ [-Wenum-compare]
    9 |     if (c == t) {
      |         ~~^~~~
$ ./mistura
Vermelho é igual a Pequeno?!

Os dois valem 0 por dentro, então a comparação deu verdadeira. O g++ avisa (comparison between ‘enum Cor’ and ‘enum Tamanho’), mas compila. Com enum class, essa comparação é erro:

$ g++ -std=c++23 -Wall -Wextra mistura2.cpp -o mistura2
mistura2.cpp: In function ‘int main()’:
mistura2.cpp:7:14: error: no match for ‘operator==’ (operand types are ‘Cor’ and ‘Tamanho’)
    7 |     return c == t;
      |            ~ ^~ ~
      |            |    |
      |            Cor  Tamanho
«… seguido de notas que explicam as duas comparações que existem: Cor com Cor e Tamanho com Tamanho …»
enum class Cor (moderno)enum Cor (antigo)
Como escrever a opçãoCor::VerdeVerde
Dois enums com o mesmo nome de opçãoPodeErro: os nomes batem
Vira int sozinhoNão; use static_cast<int>Sim, sem avisar
Comparar com outro enumErroSó um aviso
DesdeC++11C (sempre existiu)

Qual usar: sempre enum class no código novo. Conheça o antigo para ler código dos outros.


5. Mostrar o nome de uma opção

Uma surpresa para quem vem de outras linguagens: o C++ não sabe transformar Cor::Verde no texto "Verde". O nome existe só no código-fonte; o programa pronto guarda o número. E o cout nem aceita um enum class:

$ g++ -std=c++23 -Wall -Wextra imprimir.cpp -o imprimir
imprimir.cpp: In function ‘int main()’:
imprimir.cpp:7:15: error: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘Cor’)
    7 |     std::cout << sinal << '\n';
      |     ~~~~~~~~~ ^~ ~~~~~
      |          |       |
      |          |       Cor
      |          std::ostream {aka std::basic_ostream<char>}
imprimir.cpp:7:15: note: there are 52 candidates
«… mais de 360 linhas com os candidatos que não serviram …»

A solução clássica é escrever uma função com switch que devolve o texto de cada opção:

#include <iostream>
#include <string>

enum class Cor { Vermelho, Amarelo, Verde };

std::string nome(Cor c) {
    switch (c) {
        case Cor::Vermelho: return "vermelho";
        case Cor::Amarelo:  return "amarelo";
        case Cor::Verde:    return "verde";
    }
    return "desconhecida";
}

int main() {
    Cor sinal = Cor::Amarelo;
    std::cout << "O sinal está " << nome(sinal) << ".\n";

    for (Cor c : {Cor::Vermelho, Cor::Amarelo, Cor::Verde}) {
        std::cout << static_cast<int>(c) << " = " << nome(c) << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra nome.cpp -o nome
$ ./nome
O sinal está amarelo.
0 = vermelho
1 = amarelo
2 = verde
case Cor::Vermelho: return "vermelho";
Cada case devolve o texto direto, então não precisa de break: o return já sai da função.
return "desconhecida";
Depois do switch. Parece inútil (todas as opções têm case), mas uma Cor criada com static_cast de um número estranho não cai em nenhum. Sem essa linha, o g++ avisa, como você vai ver abaixo.
for (Cor c : {Cor::Vermelho, …})
Percorre uma lista escrita ali mesmo, entre chaves. O C++ não tem um jeito pronto de percorrer todas as opções de um enum.

Sem o return do fim, o g++ reclama:

$ g++ -std=c++23 -Wall -Wextra nome2.cpp -o nome2
nome2.cpp: In function ‘std::string nome(Cor)’:
nome2.cpp:12:1: warning: control reaches end of non-void function [-Wreturn-type]
   12 | }
      | ^

control reaches end of non-void function = "a execução pode chegar ao fim de uma função que deveria devolver um valor" sem devolver nada (veja Retorno).

E no futuro?

O C++26 traz a reflexão, um recurso que permite ao programa consultar os nomes do próprio código, inclusive os das opções de um enum. Ainda não está disponível no g++ 15. Até lá, a função com switch é o jeito padrão, e o -Wswitch avisa se você esquecer uma opção nela.


Quando dá errado

"‘Verde’ was not declared in this scope; did you mean ‘Cor::Verde’?"

O que aconteceu: faltou o prefixo Cor::. No enum class, ele é obrigatório.

Como resolver: escreva Cor::Verde, ou use using enum Cor; (C++20) dentro do bloco.

"enumeration value ‘…’ not handled in switch"

O que aconteceu: o switch não tem case para uma das opções. Quando a variável tiver esse valor, nenhum código roda.

Como resolver: acrescente o case que falta. Não "resolva" com default:: o aviso some, mas você perde a proteção para opções futuras.

"cannot convert ‘Cor’ to ‘int’"

O que aconteceu: você usou uma opção de enum class onde se espera um número (numa conta, num int, num índice de vetor).

Como resolver: se o número é mesmo o que você quer, static_cast<int>(Cor::Verde). Se não, guarde numa variável do tipo Cor.

"‘Verde’ conflicts with a previous declaration"

O que aconteceu: dois enums antigos (sem class) usam o mesmo nome de opção, ou uma opção tem o nome de uma variável ou função.

Como resolver: troque enum por enum class e escreva as opções com o prefixo.

"no match for ‘operator<<’ … and ‘Cor’"

O que aconteceu: o cout não sabe mostrar um enum class.

Como resolver: mostre o texto com uma função nome(Cor) (seção 5) ou o número com static_cast<int>.


Referência rápida

EscritaSignificado
enum class Cor { Vermelho, Verde };Cria o tipo Cor com duas opções (valem 0 e 1).
enum class Nivel { Baixo = 1, Alto = 10 };Escolhe o número de cada opção.
Cor c = Cor::Verde;Variável com uma opção.
c == Cor::VerdeCompara opções.
switch (c) { case Cor::Verde: … }Um caminho por opção; -Wswitch avisa se faltar alguma.
using enum Cor;Usa as opções sem o prefixo, dentro do bloco C++20.
static_cast<int>(c)O número da opção.
static_cast<Cor>(1)A opção de número 1 (sem conferir se existe).
enum Cor { Vermelho, Verde };O enum antigo: nomes soltos, vira número sozinho. Evite.

Exercícios

1. Declarar um enum

Complete para criar o tipo Tamanho moderno, com as opções Pequeno, Medio e Grande:

Tamanho { Pequeno, Medio, Grande };

2. Escolher uma opção

Complete para a variável t guardar o tamanho grande:

Tamanho t = ;

3. O número da opção

Complete para mostrar o número da opção guardada em t:

std::cout << << '\n';

4. Em enum class Dia { Seg, Ter, Qua };, quanto vale static_cast<int>(Dia::Qua)?

5. Por que é melhor não pôr default: num switch sobre um enum?

6. Qual destas é uma vantagem do enum class sobre o enum antigo?

Missão final: o status do pedido

Missão: a lanchonete da Ana acompanha cada pedido com o enum Status. Escreva a função std::string nome(Status s), com um switch, que devolve "recebido", "preparando", "saiu para entrega" e "entregue". No laço do main, mostre cada opção como 0 = recebido, 1 = preparando e assim por diante, sem nenhum aviso.

#include <iostream>
#include <string>

enum class Status { Recebido, Preparando, Saiu, Entregue };

// escreva aqui a função nome(Status)

int main() {
    for (Status s : {Status::Recebido, Status::Preparando, Status::Saiu, Status::Entregue}) {
        // mostre o número e o nome, como em: 0 = recebido
    }
}
#include <iostream>
#include <string>

enum class Status { Recebido, Preparando, Saiu, Entregue };

std::string nome(Status s) {
    switch (s) {
        case Status::Recebido:   return "recebido";
        case Status::Preparando: return "preparando";
        case Status::Saiu:       return "saiu para entrega";
        case Status::Entregue:   return "entregue";
    }
    return "desconhecido";
}

int main() {
    for (Status s : {Status::Recebido, Status::Preparando, Status::Saiu, Status::Entregue}) {
        std::cout << static_cast<int>(s) << " = " << nome(s) << '\n';
    }
}

Resumo

  • Um enum é um tipo com uma lista fechada de opções: enum class Cor { Vermelho, Amarelo, Verde };.
  • Escreva as opções com o prefixo: Cor::Verde. Compare com == e !=.
  • Com switch, escreva um case por opção e evite default: o -Wswitch avisa quando faltar alguma.
  • Por dentro, cada opção é um número (0, 1, 2…). static_cast<int> mostra o número; static_cast<Cor> faz o caminho inverso, sem conferir.
  • O enum antigo (sem class) espalha os nomes pelo programa e vira número sozinho. Use sempre enum class.
  • O C++ não mostra o nome de uma opção: escreva uma função nome() com switch.