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.

Você vai aprender
  • Como criar um apelido: using Nota = double;.
  • Que o apelido é só outro nome: Nota e double continuam 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) e using 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 a double".
Nota media(Nota a, Nota b)
A partir daí, Nota serve em qualquer lugar onde serviria um double: variáveis, parâmetros, retorno.
double comum = prova;
Como é o mesmo tipo, Nota e double se 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"). O map mostra os nomes em ordem alfabética.
Onde colocar os apelidos

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).

Não exagere

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

"‘Nota’ does not name a type": usou o apelido antes de criá-lo
#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.

"does not name a type" no próprio 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.

"redefinition of" com um apelido

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

EscritaSignificado
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:

void mostrar() { … }

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: Nota e double se misturam e não servem para sobrecarga. Para um tipo novo, use struct ou enum class.
  • typedef double Nota; é a forma antiga, com o apelido no fim. Prefira using.
  • 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::string e std::size_t são apelidos da própria biblioteca padrão.