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.
- 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-Wswitchquando falta um caso. - Que cada opção é um número por dentro, e como ver esse número com
static_cast<int>. - O
enumantigo, semclass, 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… | Exemplo | Problema |
|---|---|---|
std::string | "verde" | Aceita qualquer texto, inclusive erros de digitação. |
int | 2 | Ninguém sabe o que 2 quer dizer; aceita -50. |
enum class Cor | Cor::Verde | Nenhum: 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. Oclassaqui 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
Corcom a opçãoVermelho. OCor::na frente é obrigatório: diz "oVermelhoque pertence aCor". 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.
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.
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ção | Cor::Verde | Verde |
| Dois enums com o mesmo nome de opção | Pode | Erro: os nomes batem |
Vira int sozinho | Não; use static_cast<int> | Sim, sem avisar |
| Comparar com outro enum | Erro | Só um aviso |
| Desde | C++11 | C (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
casedevolve o texto direto, então não precisa debreak: oreturnjá sai da função. - return "desconhecida";
- Depois do
switch. Parece inútil (todas as opções têmcase), mas umaCorcriada comstatic_castde 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).
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
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.
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.
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.
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.
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
| Escrita | Significado |
|---|---|
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::Verde | Compara 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:
2. Escolher uma opção
Complete para a variável t guardar o tamanho grande:
3. O número da opção
Complete para mostrar o número da opção guardada em t:
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 umcasepor opção e evitedefault: o-Wswitchavisa 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
enumantigo (semclass) espalha os nomes pelo programa e vira número sozinho. Use sempreenum class. - O C++ não mostra o nome de uma opção: escreva uma função
nome()comswitch.