3. Structs e enum class¶
Antes de tudo: o que é uma struct¶
Até aqui, uma "coisa" com vários atributos vira várias variáveis soltas, uma para cada atributo. Um aluno com nome e nota:
Isso funciona para duas variáveis, mas não escala: uma função que recebe "um
aluno" precisa receber nome e nota como dois parâmetros separados, nada no
código deixa explícito que os dois valores andam juntos, e passar um aluno errado
para o parâmetro errado (imprime(nota1, nome1)) é um erro fácil de cometer e
que o compilador não pega.
Uma struct resolve isso: ela declara um tipo novo, que empacota vários campos com um nome só.
Repare no ; depois do } — é obrigatório em C++, e esquecê-lo é o erro mais
comum de quem começa a usar struct (veja em Erros comuns, mais abaixo). Dentro
das chaves ficam os campos, cada um com seu tipo, exatamente como uma
declaração de variável comum.
Declarar a struct não cria nenhum aluno — é só a "planta" do tipo. Para ter um
aluno de verdade, você cria uma variável do tipo Aluno, inicializando os
campos entre chaves, na ordem em que foram declarados:
Aluno a{"Ana", 8.5}; // nome = "Ana", nota = 8.5
Aluno b{"Bruno", 6.0};
Aluno vazio{}; // string vazia, nota 0.0 — campos zerados
Aluno passa a se comportar como int ou std::string: um tipo que você pode
usar para declarar quantas variáveis quiser, cada uma com sua própria cópia dos
campos. Alterar b não afeta a.
Para ler ou alterar um campo, use . (ponto) entre a variável e o nome do campo:
std::cout << a.nome << " tirou " << a.nota << "\n"; // Ana tirou 8.5
a.nota = a.nota + 1.0; // agora a.nota vale 9.5
E uma struct pode ser usada como parâmetro e como retorno de função, igual
qualquer outro tipo. As mesmas regras de passagem do capítulo
2
valem aqui — e é exatamente para struct (e para tipos como std::string) que
aquela tabela foi pensada:
bool tem_bolsa(const Aluno& a) { return a.nota >= 9.0; } // le, sem copiar
void aprova(Aluno& a) { a.nota = 10.0; } // altera o do chamador
Aluno cria_aluno(const std::string& nome, double nota) { // struct como retorno
Aluno novo{nome, nota};
return novo;
}
Guarde essas quatro ideias — tipo novo, {} para inicializar, . para
acessar campo, struct como parâmetro/retorno — porque o resto deste
capítulo (e boa parte do curso a partir daqui) é construído em cima delas.
struct com métodos: a ponte para orientação a objetos¶
Do jeito que foi vista até agora, uma struct é só um pacote de dados — nenhuma
função "mora dentro" dela; para calcular algo sobre um Aluno, você escreve uma
função separada que recebe o Aluno como parâmetro:
struct Ponto {
double x;
double y;
};
double distancia_origem(const Ponto& p) {
return sqrt(p.x * p.x + p.y * p.y);
}
(É assim, aliás, que uma struct de C sempre funciona — em C não existe outra
opção.)
Em C++, uma struct pode ter funções (chamadas métodos) dentro dela, com acesso direto aos campos, sem precisar receber a struct como parâmetro:
#include <cmath>
struct Ponto {
double x;
double y;
double distancia_origem() const {
return std::sqrt(x * x + y * y);
}
};
Ponto p{3.0, 4.0};
double d = p.distancia_origem(); // 5.0
Note o const depois dos parênteses: ele diz "este método não altera os campos de p". É a mesma ideia de const do capítulo 1, aplicada a métodos — o compilador barra qualquer tentativa de atribuir a x ou y dentro de um método marcado const. Isso vira essencial na Unidade 5, quando struct evolui para class com encapsulamento — por enquanto, guarde a ideia: struct com método já é 80% de uma classe.
enum class: mais seguro que enum de C¶
Antes da sintaxe, o problema. Muita coisa que um programa representa só pode
assumir um punhado de valores: um turno é manhã, tarde ou noite; um semáforo
está verde, amarelo ou vermelho; um pedido está pendente, pago ou cancelado. A
saída improvisada é guardar isso num int (0 é manhã, 1 é tarde...) ou numa
std::string ("manha", "tarde"). As duas funcionam e as duas mentem:
int turno = 7; // 7 nao e turno nenhum, e ninguem reclama
std::string t = "Tarde"; // "Tarde", "tarde" ou "TARDE"? escolha errado e o if falha
O int aceita valores que não existem no seu domínio; a string aceita qualquer
texto e ainda erra por maiúscula. Nos dois casos o compilador não tem como
ajudar, porque para ele são só um número e um texto quaisquer.
Um enum resolve isso criando um tipo cujos únicos valores possíveis são os que você listou. Escrever algo fora da lista deixa de compilar — o erro sai na compilação, não na frente do usuário.
Em C, porém, um enum é basicamente um int disfarçado — dois enums diferentes podem "vazar" e se misturar sem erro:
enum Cor { VERMELHO, VERDE, AZUL };
enum Status { OK, ERRO };
enum Cor c = OK; // compila em C, e um bug logico
Pior: como os nomes ficam todos soltos no escopo de fora, dois enums não podem
repetir um nome. Declarar enum Turno { Manha, Tarde } e enum Periodo { Manha,
Madrugada } no mesmo programa dá erro de compilação:
enum class (C++11+) cria um tipo de verdade, isolado:
enum class Cor { Vermelho, Verde, Azul };
enum class Status { Ok, Erro };
Cor c = Cor::Vermelho; // precisa qualificar com Cor::
// Cor c2 = Status::Ok; // erro de compilacao: tipos incompativeis
Os nomes agora moram dentro do tipo — é isso que o Cor:: na frente
significa, e é por isso que Turno::Manha e Periodo::Manha podem coexistir
sem colidir.
Comparação funciona normalmente:
Decidindo por caso: switch sobre um enum¶
Quando a ação muda conforme o valor, o switch é a forma natural — e é aqui que
o enum paga o investimento:
std::string nome_cor(Cor c) {
switch (c) {
case Cor::Vermelho: return "vermelho";
case Cor::Verde: return "verde";
case Cor::Azul: return "azul";
}
return "desconhecida";
}
Dois detalhes que todo mundo tropeça na primeira vez:
O case precisa do qualificador. É case Cor::Verde:, não case Verde: —
pela mesma razão de sempre, o nome mora dentro do tipo.
O return depois do switch parece inútil, e não é. Você cobriu as três
cores, mas o compilador não considera isso uma garantia (nada impede que um
static_cast mal-intencionado produza um Cor fora da lista). Sem aquela última
linha, o g++ avisa:
Em troca, o compilador oferece algo que nem int nem std::string dariam: se
você esquecer um caso, ele avisa, dizendo qual.
Isso é o argumento mais prático a favor de enum: no dia em que você acrescentar
Cor::Amarelo à lista, o compilador aponta cada switch do programa que
precisa ser atualizado. Com int ou std::string, você descobriria testando —
ou não descobriria.
Devolvendo um enum¶
Um enum não serve só de parâmetro: é um tipo como outro qualquer, e uma função
pode devolvê-lo. Esse é o jeito de fazer uma classificação ficar explícita no
tipo de retorno, em vez de devolver um int que quem chama precisa decifrar:
enum class Faixa { Baixa, Normal, Alta };
Faixa classifica(double temperatura) {
if (temperatura < 18.0) return Faixa::Baixa;
if (temperatura > 26.0) return Faixa::Alta;
return Faixa::Normal;
}
Quem chama classifica(30.0) recebe um Faixa, não um número solto — e só pode
compará-lo com os valores de Faixa.
Convertendo entre enum e int¶
O outro lado dessa segurança é que um enum class não vira int sozinho —
nem para imprimir. Quando você realmente precisar do número por trás do valor,
peça a conversão explicitamente com static_cast:
std::cout << static_cast<int>(Cor::Azul) << "\n"; // 2
// std::cout << Cor::Azul << "\n"; // erro de compilacao
static_cast<T>(x) é o (T) x de C, com um nome que dá para procurar e que
salta aos olhos de quem lê. A diferença é justamente essa: em C a conversão
acontecia em silêncio, e aqui ela fica escrita no código, onde pode ser
discutida.
Por padrão, os valores começam em 0 e seguem na ordem em que você declarou
— por isso Cor::Azul é 2. Mas você pode escolher os números, o que é útil
quando eles significam algo fora do programa:
enum class Codigo { Ok = 200, NaoEncontrado = 404, ErroInterno = 500 };
std::cout << static_cast<int>(Codigo::NaoEncontrado) << "\n"; // 404
O caminho de volta, de int para o enum, também existe — e exige cuidado,
porque nada é validado:
Cor c = static_cast<Cor>(1); // Cor::Verde
Cor ruim = static_cast<Cor>(7); // compila! e nao existe cor 7
O static_cast obedece sem perguntar, e ruim passa a carregar um valor que não
corresponde a cor nenhuma — o switch da seção anterior cairia direto no
return "desconhecida". Quando o número vem de fora do programa (de um arquivo,
da entrada do usuário), conferir o intervalo antes de converter é
responsabilidade de quem converte. Guarde essa questão: mais adiante neste
capítulo aparece a forma honesta de uma função dizer "esse número não era uma cor
nenhuma".
Prefira enum class a enum sempre que estiver escrevendo código novo em C++.
std::pair: agrupando dois valores sem criar uma struct¶
Às vezes você só precisa devolver dois valores de uma função, sem que valha a pena nomear uma struct nova:
#include <utility>
std::pair<int, int> divide_com_resto(int a, int b) {
return {a / b, a % b};
}
auto resultado = divide_com_resto(17, 5);
std::cout << resultado.first << " " << resultado.second << "\n"; // 3 2
std::pair<A, B> acessa os campos por .first e .second. Você vai reencontrar pair na Unidade 2, porque é exatamente o que um std::map usa internamente para guardar chave e valor.
Quando a função pode não ter resposta¶
Nem toda pergunta tem resposta. "Em que posição está a letra z nesta palavra?"
não tem resposta se a letra não estiver lá; "qual é o maior valor deste vector?"
não tem resposta se o vector estiver vazio. Em C, isso se resolve de duas formas,
e as duas continuam valendo em C++.
A primeira é devolver um valor que não pode ser confundido com uma resposta de verdade:
int busca_posicao(const std::string& texto, char alvo) {
for (std::size_t i = 0; i < texto.size(); i++) {
if (texto[i] == alvo) {
return static_cast<int>(i);
}
}
return -1; // "nao achei"
}
Aqui o -1 funciona, e funciona por um motivo específico que vale entender: nenhuma
posição válida é negativa, então -1 nunca se confunde com um índice real. É
sorte do problema, não uma regra geral.
Porque basta a função devolver algo em que todo valor seja legítimo para a
convenção ruir. Uma divisão protegida é o caso limite — -1 é um quociente
perfeitamente possível:
double divide(double a, double b) {
if (b == 0.0) return -1.0; // "deu errado"... ou o resultado de -10.0 / 10.0
return a / b;
}
Quem chama não tem como distinguir os dois casos. Aí entra a segunda forma, que é
a que este curso adota daqui para frente: a função devolve bool dizendo se
deu certo, e entrega o resultado por um parâmetro de referência — o mesmo
mecanismo da Aula 02:
bool divide_seguro(double a, double b, double& resultado) {
if (b == 0.0) {
return false; // nao mexe em "resultado"
}
resultado = a / b;
return true;
}
E quem chama fica obrigado a olhar a resposta antes de usar o valor:
double r = 0.0;
if (divide_seguro(10.0, 2.0, r)) {
std::cout << "deu " << r << "\n"; // deu 5
} else {
std::cout << "divisao por zero\n";
}
Regra prática para escolher entre as duas: se existe um valor que a função
jamais devolveria como resposta legítima, use-o (-1 para índice, 0 para uma
contagem que nunca é zero); se todos os valores são possíveis, devolva bool e
o resultado por referência.
C++ tem uma terceira forma, e ela não é assunto deste curso
A biblioteca padrão oferece std::optional<T>, um tipo que representa "ou
tem um T, ou não tem nada" sem precisar de valor mágico nem de parâmetro de
saída. É mais elegante que as duas formas acima, e é o que se usa em C++
moderno — mas envolve um conceito novo (um tipo que embrulha outro tipo) que
não compensa carregar agora, no primeiro contato com struct. Ele está descrito
no apêndice de próximos passos, e é um bom
primeiro assunto para depois do curso.
Juntando tudo: um exemplo completo¶
#include <iostream>
#include <string>
enum class Categoria { Eletronico, Alimento, Vestuario };
struct Produto {
std::string nome;
double preco;
Categoria categoria;
bool esta_em_promocao() const {
return preco < 10.0;
}
};
bool busca_produto(const std::string& nome, Produto& achado) {
if (nome == "caneta") {
achado = Produto{"caneta", 2.50, Categoria::Vestuario};
return true;
}
return false;
}
int main() {
Produto resultado{"", 0.0, Categoria::Eletronico};
if (busca_produto("caneta", resultado)) {
std::cout << resultado.nome << " custa " << resultado.preco << "\n";
if (resultado.esta_em_promocao()) {
std::cout << "esta em promocao!\n";
}
}
}
Repare que o parâmetro de saída é uma struct inteira, e isso não muda nada: referência funciona com qualquer tipo, inclusive os que você mesmo criou. E repare no if: o valor de resultado só é lido depois de a função confirmar que há o que ler.
Erros comuns¶
"error: expected ‘;’ after struct definition" — falta o ; depois do } que fecha a struct. Todo struct { ... } termina em ;, igual qualquer outra declaração — é fácil esquecer porque { ... } de função ou de if não pede isso.
Trocar a ordem dos valores na inicialização — {} segue a ordem dos campos na struct, não os nomes. Com struct Ponto { double x; double y; };, escrever Ponto p{4.0, 3.0} querendo dizer "x=3, y=4" compila sem erro nenhum — os dois campos são double, então qualquer ordem é aceita, e o valor errado só aparece quando o programa roda. Confira a ordem dos campos na declaração da struct sempre que inicializar.
"error: base operand of ‘->’ has non-pointer type ‘Aluno’" — -> é para ponteiro (Aluno* p; p->nome). Uma variável comum do tipo Aluno usa ., não ->.
A função devolveu false e o programa usou o valor mesmo assim — não é erro de compilação nenhum: o parâmetro de saída fica com o que já estava nele, que pode ser lixo. Por isso a chamada vai dentro de um if, e o valor só é lido no ramo verdadeiro.
"error: ‘VERMELHO’ was not declared in this scope" ao usar enum class — com enum class, os valores não "vazam" para o escopo externo como no enum de C. Use Cor::Vermelho, não Vermelho sozinho.
Esquecer const no método e o compilador reclamar em outro lugar — se você tentar chamar um método não-const numa struct que chegou como const Produto&, o compilador barra. A regra prática: todo método que só lê os campos deveria ser const.
Resumo¶
structdeclara um tipo novo que empacota vários campos com um nome só;Nome{...}inicializa os campos na ordem em que foram declarados, e.lê ou altera cada um.structem C++ pode ter métodos — funções que enxergam os campos diretamente, sem receber a struct como parâmetro.- Método
constpromete não alterar os campos; o compilador garante isso. enum classcria um tipo isolado e seguro, ao contrário doenum"int disfarçado" de C: os nomes moram dentro do tipo e não colidem.switchsobre um enum é a forma natural de decidir por caso, e o compilador avisa quando você esquece um dos valores — o queintestd::stringnunca fariam.- Um enum serve de parâmetro e de retorno; a conversão para
inte de volta exigestatic_cast, e a volta não valida nada. std::pairagrupa dois valores sem precisar nomear uma struct.- Quando a função pode não ter resposta: use um valor impossível (
-1para índice) se ele existir; senão devolvaboole o resultado por parâmetro de referência. - A chamada de uma função assim vai dentro de um
if— o parâmetro de saída só tem valor confiável quando a função devolveutrue.