C++ using e typedef
Alguns tipos do C++ têm nomes enormes: std::map<std::string, std::string>, std::vector<Aluno>. Outros têm nomes curtos demais para dizer o que guardam: um double pode ser uma nota, um preço, uma altura. Com using você dá um apelido a um tipo, e o código fica mais curto e mais fácil de ler. Nesta aula você vai criar apelidos com using, reconhecer a forma antiga typedef e entender o que um apelido não faz.
- Como criar um apelido:
using Nota = double;. - Que o apelido é só outro nome:
Notaedoublecontinuam sendo o mesmo tipo. - A forma antiga,
typedef double Nota;, e por que a nova é mais fácil de ler. - Como encurtar tipos compridos:
using Agenda = std::map<std::string, std::string>;. - Que existem apelidos "com buraco" (alias de template), só para reconhecê-los.
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 apelido, não uma pessoa nova
A Ana se chama Ana Beatriz, mas a família a chama de "Bia". Quando a mãe diz "a Bia chegou", ninguém pensa que entrou uma pessoa nova na casa: é a mesma Ana Beatriz, com outro nome.
Um apelido de tipo é isso. using Nota = double; diz ao compilador: "quando eu escrever Nota, entenda double". O tipo é exatamente o mesmo. O ganho é para quem lê o código: Nota media(Nota a, Nota b) conta mais que double media(double a, double b).
Compare com as aulas anteriores: struct e enum class criam tipos novos, diferentes de todos os outros. using e typedef só dão outro nome a um tipo que já existe.
1. using: o jeito moderno
#include <iostream>
using Nota = double;
Nota media(Nota a, Nota b) {
return (a + b) / 2;
}
int main() {
Nota prova = 8.0;
Nota trabalho = 6.5;
std::cout << "Média: " << media(prova, trabalho) << '\n';
double comum = prova;
Nota outra = 10;
std::cout << comum << ' ' << outra << '\n';
}
- using
- Palavra-chave (em inglês, "usando"). Aqui ela começa um apelido. A mesma palavra tem outros usos em C++, como
using enum(veja Enums) eusing namespace(veja Namespaces); o que decide é o que vem depois. - Nota
- O apelido novo. Como nos tipos próprios, começa com maiúscula.
- = double;
- O tipo verdadeiro. Leia a linha inteira como "
Notaé igual adouble". - Nota media(Nota a, Nota b)
- A partir daí,
Notaserve em qualquer lugar onde serviria umdouble: variáveis, parâmetros, retorno. - double comum = prova;
- Como é o mesmo tipo,
Notaedoublese misturam à vontade, sem conversão.
$ g++ -std=c++23 -Wall -Wextra nota.cpp -o nota $ ./nota Média: 7.25 8 10
Outra vantagem: se um dia a Ana decidir que as notas devem ser float, ela muda uma linha (using Nota = float;) em vez de caçar todos os double do programa, que também podem ser preços, alturas…
Missão: crie o apelido Preco para double e use-o no retorno e no primeiro parâmetro da função total (e, se quiser, na variável caderno). A saída não muda: Total: R$ 37.5.
#include <iostream>
// crie aqui o apelido Preco para double
double total(double unitario, int quantidade) {
return unitario * quantidade;
}
int main() {
double caderno = 12.5;
std::cout << "Total: R$ " << total(caderno, 3) << '\n';
}
#include <iostream>
using Preco = double;
Preco total(Preco unitario, int quantidade) {
return unitario * quantidade;
}
int main() {
Preco caderno = 12.5;
std::cout << "Total: R$ " << total(caderno, 3) << '\n';
}
2. O apelido não é um tipo novo
Isso tem uma consequência que surpreende: para o compilador, Nota e double são indistinguíveis. Ele não protege você de misturar uma nota com um preço, como o enum class faria. E duas funções que só diferem por um apelido são, na verdade, a mesma função:
#include <iostream>
using Nota = double;
void mostrar(double x) {
std::cout << "double: " << x << '\n';
}
void mostrar(Nota n) {
std::cout << "nota: " << n << '\n';
}
int main() {
mostrar(8.5);
}
$ g++ -std=c++23 -Wall -Wextra mesmotipo.cpp -o mesmotipo
mesmotipo.cpp:9:6: error: redefinition of ‘void mostrar(Nota)’
9 | void mostrar(Nota n) {
| ^~~~~~~
mesmotipo.cpp:5:6: note: ‘void mostrar(double)’ previously defined here
5 | void mostrar(double x) {
| ^~~~~~~
redefinition of ‘void mostrar(Nota)’ = "redefinição de mostrar(Nota)". A nota de baixo explica: ela já tinha sido definida como mostrar(double), que é a mesma coisa. Na sobrecarga de funções, os parâmetros precisam ter tipos de verdade diferentes; um apelido não conta. Se você precisa que Nota e Preco sejam tipos diferentes, crie uma struct para cada um.
3. typedef: o jeito antigo
Antes do C++11, os apelidos eram feitos com typedef (de type definition, "definição de tipo"), que vem da linguagem C. Ele faz exatamente a mesma coisa, mas na ordem inversa: primeiro o tipo verdadeiro, depois o apelido, como se você declarasse uma variável.
#include <iostream>
typedef double Nota;
typedef unsigned int Contador;
int main() {
Nota n = 7.5;
Contador alunos = 30;
std::cout << n << ' ' << alunos << '\n';
}
$ g++ -std=c++23 -Wall -Wextra typedef.cpp -o typedef $ ./typedef 7.5 30
using (C++11) | typedef (antigo) |
|---|---|
using Nota = double; | typedef double Nota; |
using Contador = unsigned int; | typedef unsigned int Contador; |
using Turma = std::vector<Aluno>; | typedef std::vector<Aluno> Turma; |
Com using, o nome novo fica sempre à esquerda do =, como numa variável (int x = 5;). Com typedef, você precisa achar o apelido no fim da linha. A diferença fica gritante com tipos complicados, como um ponteiro para função:
#include <iostream>
typedef void (*AcaoAntiga)(int);
using Acao = void (*)(int);
void dizer(int n) {
std::cout << "Número " << n << '\n';
}
int main() {
AcaoAntiga a = dizer;
Acao b = dizer;
a(1);
b(2);
}
$ g++ -std=c++23 -Wall -Wextra ponteirofuncao.cpp -o ponteirofuncao $ ./ponteirofuncao Número 1 Número 2
Não se preocupe com o que é um ponteiro para função; repare só onde está o nome. No typedef, o apelido AcaoAntiga fica escondido no meio, entre parênteses. No using, Acao está na frente, e o tipo inteiro do outro lado do =.
Qual usar: using no código novo. Conheça o typedef porque ele está em todo lugar, inclusive dentro da própria biblioteca padrão. Dois exemplos reais, dos cabeçalhos do g++ 15 na máquina da Ana:
$ grep -n 'size_t;' /usr/include/x86_64-linux-gnu/c++/15/bits/c++config.h | head -1 338: typedef __SIZE_TYPE__ size_t; $ grep -n 'typedef basic_string<char> *string;' /usr/include/c++/15/bits/stringfwd.h 79: typedef basic_string<char> string;
Isso mesmo: o std::string que você usa desde o Nível 1 é um apelido para std::basic_string<char>, e o std::size_t (o tipo de v.size()) é um apelido para um inteiro sem sinal. É por isso que algumas mensagens de erro falam em basic_string<char>: o compilador às vezes mostra o nome verdadeiro, não o apelido.
4. Apelidos para tipos compridos
O uso mais comum de using no dia a dia é encurtar tipos que se repetem pelo programa. Um vetor de alunos, por exemplo:
#include <iostream>
#include <string>
#include <vector>
struct Aluno {
std::string nome;
double nota = 0.0;
};
using Turma = std::vector<Aluno>;
double media(const Turma& turma) {
double soma = 0.0;
for (const auto& a : turma) {
soma += a.nota;
}
return soma / turma.size();
}
int main() {
Turma manha = {{"Ana", 8.5}, {"Bruno", 6.0}};
Turma tarde = {{"Carla", 9.5}, {"Davi", 7.0}, {"Eva", 8.0}};
std::cout << "Manhã: " << media(manha) << '\n';
std::cout << "Tarde: " << media(tarde) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra turma.cpp -o turma $ ./turma Manhã: 7.25 Tarde: 8.16667
O ganho cresce com o tamanho do tipo. A agenda de telefones da Ana usa um std::map: uma tabela que liga uma chave (o nome) a um valor (o telefone). Você vai estudá-lo em std::map; aqui, basta saber que std::map<std::string, std::string> é "uma tabela de texto para texto". Sem apelido, esse nome aparece em todo lugar:
#include <iostream>
#include <map>
#include <string>
void mostrar(const std::map<std::string, std::string>& agenda) {
for (const auto& contato : agenda) {
std::cout << contato.first << ": " << contato.second << '\n';
}
}
int main() {
std::map<std::string, std::string> amigos;
amigos["Bruno"] = "(11) 98765-4321";
mostrar(amigos);
}
Com apelido:
#include <iostream>
#include <map>
#include <string>
using Agenda = std::map<std::string, std::string>;
void mostrar(const Agenda& agenda) {
for (const auto& contato : agenda) {
std::cout << contato.first << ": " << contato.second << '\n';
}
}
int main() {
Agenda amigos;
amigos["Bruno"] = "(11) 98765-4321";
amigos["Carla"] = "(21) 91234-5678";
amigos["Ana"] = "(31) 99999-0000";
mostrar(amigos);
}
$ g++ -std=c++23 -Wall -Wextra agenda.cpp -o agenda $ ./agenda Ana: (31) 99999-0000 Bruno: (11) 98765-4321 Carla: (21) 91234-5678
- using Agenda = std::map<std::string, std::string>;
- Um nome curto, que diz para que o tipo serve, no lugar de um nome longo, que diz como ele é feito.
- amigos["Bruno"] = "(11) 98765-4321";
- Guarda o telefone do Bruno na tabela.
- contato.first / contato.second
- Cada item da tabela tem a chave (
first, "primeiro") e o valor (second, "segundo"). Omapmostra os nomes em ordem alfabética.
Lá em cima no arquivo, depois dos #include e das structs que eles usam, como no turma.cpp: o apelido Turma só pode vir depois da struct Aluno. Também é possível criar um apelido dentro de uma função; aí ele só vale ali dentro (veja Escopo).
Apelido bom diz algo que o tipo original não diz (Nota, Agenda, Turma). Apelidos como using Inteiro = int; ou using Texto = std::string; só obrigam quem lê a procurar o que eles significam. E nunca esconda que algo é um ponteiro ou uma referência atrás de um apelido.
5. Apelidos com "buraco": alias de template
Só para você reconhecer quando encontrar: um apelido pode deixar uma parte do tipo em aberto, para ser preenchida depois, entre < >. Isso se chama alias de template, e só funciona com using (o typedef não consegue).
#include <iostream>
#include <map>
#include <string>
#include <vector>
template <typename T>
using Lista = std::vector<T>;
template <typename V>
using Tabela = std::map<std::string, V>;
int main() {
Lista<int> idades = {20, 19, 22};
Lista<std::string> nomes = {"Ana", "Bruno"};
Tabela<double> notas = {{"Ana", 8.5}};
std::cout << idades.size() << ' ' << nomes[1] << ' ' << notas["Ana"] << '\n';
}
$ g++ -std=c++23 -Wall -Wextra template.cpp -o template $ ./template 3 Bruno 8.5
Lista<int> vira std::vector<int>; Tabela<double> vira std::map<std::string, double>. O T e o V são os "buracos". Como isso funciona por dentro é assunto de Templates de Classe, no Nível 3.
Quando dá errado
#include <iostream>
Nota dobro(Nota n) {
return n * 2;
}
using Nota = double;
int main() {
std::cout << dobro(4.5) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra antes.cpp -o antes
antes.cpp:3:1: error: ‘Nota’ does not name a type
3 | Nota dobro(Nota n) {
| ^~~~
antes.cpp: In function ‘int main()’:
antes.cpp:10:18: error: ‘dobro’ was not declared in this scope
10 | std::cout << dobro(4.5) << '\n';
| ^~~~~
O que aconteceu: o compilador lê o arquivo de cima para baixo. Na linha 3, ele ainda não conhecia Nota: does not name a type = "não é nome de nenhum tipo". Como a função dobro não pôde ser criada, o segundo erro é consequência do primeiro (por isso, conserte sempre o primeiro erro; veja Lendo Erros do Compilador). No editor do guia, o Clang diz unknown type name 'Nota' ("nome de tipo desconhecido").
Como resolver: coloque o using antes de qualquer uso, lá no começo do arquivo.
typedef: ordem invertida
$ g++ -std=c++23 -Wall -Wextra typedefinvertido.cpp -o typedefinvertido
typedefinvertido.cpp:1:9: error: ‘Nota’ does not name a type
1 | typedef Nota double;
| ^~~~
typedefinvertido.cpp: In function ‘int main()’:
typedefinvertido.cpp:4:5: error: ‘Nota’ was not declared in this scope
4 | Nota n = 7.5;
| ^~~~
O que aconteceu: no typedef, o tipo verdadeiro vem primeiro. typedef Nota double; está ao contrário: o compilador tentou ler Nota como o tipo verdadeiro e não achou.
Como resolver: typedef double Nota;, ou melhor, using Nota = double;, que não deixa dúvida sobre a ordem.
O que aconteceu: você escreveu duas funções com o mesmo nome cujos parâmetros só diferem por um apelido (mostrar(double) e mostrar(Nota)). Para o compilador, são a mesma função (seção 2).
Como resolver: dê nomes diferentes às funções, ou use tipos realmente diferentes (uma struct).
Missão (provoque o erro): este programa compila. Mova a linha using Nota = double; para depois da função dobro e rode. Leia a mensagem do compilador e compare com a do g++ acima.
#include <iostream>
using Nota = double;
Nota dobro(Nota n) {
return n * 2;
}
int main() {
std::cout << dobro(4.5) << '\n';
}
#include <iostream>
Nota dobro(Nota n) {
return n * 2;
}
using Nota = double;
int main() {
std::cout << dobro(4.5) << '\n';
}
Referência rápida
| Escrita | Significado |
|---|---|
using Nota = double; | Cria o apelido Nota para double (C++11). |
typedef double Nota; | O mesmo, na forma antiga: tipo verdadeiro primeiro, apelido depois. |
using Agenda = std::map<std::string, std::string>; | Encurta um tipo comprido. |
template <typename T> using Lista = std::vector<T>; | Alias de template: Lista<int> = std::vector<int>. Só com using. |
using enum Cor; | Outra coisa: libera as opções de um enum sem prefixo (veja Enums). |
using namespace std; | Outra coisa: libera os nomes de um namespace (veja Namespaces). |
Exercícios
1. Criar um apelido
Complete para criar o apelido Idade para int, do jeito moderno:
2. Do typedef para o using
Reescreva typedef std::vector<double> Notas; com using:
3. Encurtar a agenda
Com using Agenda = std::map<std::string, std::string>;, complete o parâmetro da função que só lê a agenda, sem copiar:
4. Depois de using Nota = double;, o que acontece com double x = 5.0; Nota n = x;?
5. Qual destas linhas está errada?
Missão final: a turma com apelido
Missão: crie o apelido Turma para std::vector<Aluno> (depois da struct!) e use-o no parâmetro da função media (const Turma&) e na variável do main. A saída continua Média da turma: 7.75.
#include <iostream>
#include <string>
#include <vector>
struct Aluno {
std::string nome;
double nota = 0.0;
};
// crie aqui o apelido Turma
double media(const std::vector<Aluno>& turma) {
double soma = 0.0;
for (const auto& a : turma) {
soma += a.nota;
}
return soma / turma.size();
}
int main() {
std::vector<Aluno> turma = {{"Ana", 8.5}, {"Bruno", 6.0}, {"Carla", 9.5}, {"Davi", 7.0}};
std::cout << "Média da turma: " << media(turma) << '\n';
}
#include <iostream>
#include <string>
#include <vector>
struct Aluno {
std::string nome;
double nota = 0.0;
};
using Turma = std::vector<Aluno>;
double media(const Turma& turma) {
double soma = 0.0;
for (const auto& a : turma) {
soma += a.nota;
}
return soma / turma.size();
}
int main() {
Turma turma = {{"Ana", 8.5}, {"Bruno", 6.0}, {"Carla", 9.5}, {"Davi", 7.0}};
std::cout << "Média da turma: " << media(turma) << '\n';
}
Resumo
using Nota = double;cria um apelido: um nome a mais para um tipo que já existe.- O apelido não cria um tipo novo:
Notaedoublese misturam e não servem para sobrecarga. Para um tipo novo, usestructouenum class. typedef double Nota;é a forma antiga, com o apelido no fim. Prefirausing.- Use apelidos para tipos compridos (
using Agenda = std::map<std::string, std::string>;) e para dar significado (Nota,Turma). - O apelido precisa vir antes do primeiro uso, senão:
does not name a type. std::stringestd::size_tsão apelidos da própria biblioteca padrão.