C++ auto
Até agora, toda variável começou pelo tipo: int idade = 29;. Mas, olhando o 29, o compilador já sabe que é um int. A palavra auto diz a ele: "descubra o tipo você mesmo, pelo valor". Isso economiza digitação e, mais tarde, nomes de tipo enormes. Mas tem um preço: o tipo que o compilador escolhe nem sempre é o que você imagina. Nesta aula você vai ver o que auto deduz em cada caso, aprender dois truques para espiar o tipo escolhido e saber quando usar e quando evitar.
- O que
autofaz e o que ele deduz para5,5.0,'x',truee"oi". - Dois truques reais para descobrir um tipo: o erro proposital e o
typeidcomc++filt. - Por que
auto nome = "Ana";não é umstd::string, e como o sufixos("Ana"s) resolve. - As pegadinhas:
autosem valor,auto preco = 10;eauto x = {5};. - Quando
autoajuda e quando atrapalha.
Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp, com o comando padrão do guia: g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: "o mesmo que ele pediu"
Num restaurante, a Ana pede "um suco de laranja". O Bruno, ao lado, diz só "o mesmo para mim". O garçom não precisa ouvir a palavra "laranja": ele deduz pelo pedido anterior.
auto é o "o mesmo para mim" do C++: a variável recebe o tipo do valor que está do lado direito do =. Importante: a dedução acontece na compilação, uma vez só. Depois disso, a variável tem um tipo fixo, como qualquer outra. auto não cria uma "caixa que aceita qualquer coisa".
C++11 Esse significado de auto chegou no C++11, a grande virada da linguagem (aula Versões do C++).
1. O que o auto deduz
#include <iostream>
int main() {
auto idade = 29;
auto altura = 1.65;
auto inicial = 'A';
auto pago = true;
auto nome = "Ana";
std::cout << idade << " " << altura << " " << inicial << " " << pago << " " << nome << "\n";
}
$ g++ -std=c++23 -Wall -Wextra deduz.cpp -o deduz $ ./deduz 29 1.65 A 1 Ana
A saída é igual à de um programa com os tipos escritos à mão. Mas que tipos o compilador escolheu? O cout não conta. Para descobrir, existem dois truques.
2. Truque 1: o erro proposital (o "espião")
Quando o compilador dá um erro, ele costuma escrever o tipo das coisas na mensagem. Então vamos provocar um erro de propósito, num lugar onde o tipo apareça. A receita tem duas partes:
template <typename T>
struct QualTipo;
int main() {
auto nome = "Ana";
QualTipo<decltype(nome)> espiao;
}
- template <typename T> struct QualTipo;
- Anuncia um "molde" chamado
QualTipoque recebe um tipoT, mas nunca o define (não tem chaves{ }com o conteúdo). Templates são do Nível 3 (Templates de Classe); aqui, basta copiar estas duas linhas. - decltype(nome)
- "O tipo declarado de
nome" (declared type): a pergunta que queremos responder. - QualTipo<decltype(nome)> espiao;
- Tenta criar uma variável de um tipo que não existe. O compilador vai recusar, e na recusa vai escrever o tipo por extenso.
$ g++ -std=c++23 -Wall -Wextra espiao.cpp -o espiao
espiao.cpp: In function ‘int main()’:
espiao.cpp:6:30: error: aggregate ‘QualTipo<const char*> espiao’ has incomplete type and cannot be defined
6 | QualTipo<decltype(nome)> espiao;
| ^~~~~~
O erro (has incomplete type and cannot be defined, "tem tipo incompleto e não pode ser definido") era esperado; o que interessa está entre os sinais < >: QualTipo<const char*>. Então auto nome = "Ana"; criou um const char*, e não um std::string. A seção 4 explica o que isso significa.
Com vários espiões de uma vez:
template <typename T>
struct QualTipo;
int main() {
auto a = 5;
auto b = 5.0;
auto c = 5.0f;
auto d = 5LL;
auto e = 'x';
QualTipo<decltype(a)> espiaoA;
QualTipo<decltype(b)> espiaoB;
QualTipo<decltype(c)> espiaoC;
QualTipo<decltype(d)> espiaoD;
QualTipo<decltype(e)> espiaoE;
}
$ g++ -std=c++23 -Wall -Wextra espiao2.cpp -o espiao2
espiao2.cpp: In function ‘int main()’:
espiao2.cpp:10:27: error: aggregate ‘QualTipo<int> espiaoA’ has incomplete type and cannot be defined
10 | QualTipo<decltype(a)> espiaoA;
| ^~~~~~~
espiao2.cpp:11:27: error: aggregate ‘QualTipo<double> espiaoB’ has incomplete type and cannot be defined
11 | QualTipo<decltype(b)> espiaoB;
| ^~~~~~~
espiao2.cpp:12:27: error: aggregate ‘QualTipo<float> espiaoC’ has incomplete type and cannot be defined
12 | QualTipo<decltype(c)> espiaoC;
| ^~~~~~~
espiao2.cpp:13:27: error: aggregate ‘QualTipo<long long int> espiaoD’ has incomplete type and cannot be defined
13 | QualTipo<decltype(d)> espiaoD;
| ^~~~~~~
espiao2.cpp:14:27: error: aggregate ‘QualTipo<char> espiaoE’ has incomplete type and cannot be defined
14 | QualTipo<decltype(e)> espiaoE;
| ^~~~~~~
| Declaração | Tipo deduzido | Por quê |
|---|---|---|
auto a = 5; | int | Número sem ponto. |
auto b = 5.0; | double | Número com ponto. |
auto c = 5.0f; | float | Sufixo f. |
auto d = 5LL; | long long int (= long long) | Sufixo LL. |
auto e = 'x'; | char | Aspas simples. |
auto f = true; | bool | Palavra-chave true. |
auto g = "oi"; | const char* | Texto entre aspas duplas, no estilo da linguagem C. |
Ou seja: com auto, o tipo vem das regras dos literais, vistas em Tipos de Dados e Inteiros. Saber essas regras é o que torna o auto previsível.
Missão: qual é o tipo de 3 * 2.5f? Inteiro vezes float… O molde QualTipo já está declarado; acrescente, depois da linha do total, a linha do espião (QualTipo<decltype(total)> espiao;), rode e leia o tipo na mensagem de erro. (No editor, o Clang escreve implicit instantiation of undefined template 'QualTipo<...>'; o tipo aparece do mesmo jeito.) A missão conta quando a mensagem mostrar QualTipo<float>. Depois, por curiosidade, troque 2.5f por 2.5 e rode de novo.
template <typename T>
struct QualTipo;
int main() {
auto total = 3 * 2.5f;
}
template <typename T>
struct QualTipo;
int main() {
auto total = 3 * 2.5f;
QualTipo<decltype(total)> espiao;
}
3. Truque 2: typeid e c++filt
O segundo truque descobre o tipo com o programa rodando. O operador typeid (de <typeinfo>) devolve informações sobre o tipo de algo, e o .name() devolve o nome dele:
#include <iostream>
#include <typeinfo>
int main() {
auto a = 5;
auto b = 5.0;
auto c = 'x';
auto d = true;
auto e = "oi";
std::cout << typeid(a).name() << "\n";
std::cout << typeid(b).name() << "\n";
std::cout << typeid(c).name() << "\n";
std::cout << typeid(d).name() << "\n";
std::cout << typeid(e).name() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra typeid.cpp -o typeid $ ./typeid i d c b PKc
Os nomes saíram em código: i = int, d = double, c = char, b = bool e PKc = Pointer to Konst char, ponteiro para char constante. É o nome "embaralhado" (mangled) que o g++ usa por dentro. Para traduzir, o Linux tem o programa c++filt (com -t, de "tipo"), que recebe a saída pelo | (o "cano" do terminal):
$ ./typeid | c++filt -t int double char bool char const*
char const* é o mesmo que const char*, escrito em outra ordem. O typeid é ótimo para matar curiosidade, mas cada compilador escolhe seus próprios nomes (o Clang no editor pode mostrar outros), e ele esconde alguns detalhes, como o const da própria variável. O espião da seção 2 é mais fiel.
4. auto com texto: "oi" × "oi"s
Os dois truques concordaram: auto nome = "Ana"; não cria um std::string. Cria um const char*, o jeito antigo da linguagem C de apontar para um texto. Ele não tem nenhuma das facilidades do std::string:
#include <iostream>
int main() {
auto nome = "Ana";
std::cout << nome.size() << "\n";
}
$ g++ -std=c++23 -Wall -Wextra size.cpp -o size
size.cpp: In function ‘int main()’:
size.cpp:5:23: error: request for member ‘size’ in ‘nome’, which is of non-class type ‘const char*’
5 | std::cout << nome.size() << "\n";
| ^~~~
Request for member ‘size’ in ‘nome’, which is of non-class type ‘const char*’ = "pediu-se o membro size de nome, que é do tipo const char*, que não é uma classe". Traduzindo: um const char* não tem .size(); quem tem é o std::string.
A solução: o sufixo s C++14
Um s logo depois das aspas transforma o texto num std::string de verdade. Ele vem num "pacote de sufixos" da biblioteca, que precisa ser liberado com uma linha using namespace std::literals;:
#include <iostream>
#include <string>
#include <typeinfo>
using namespace std::literals;
int main() {
auto nome = "Ana"s;
std::cout << nome << " tem " << nome.size() << " letras\n";
std::cout << typeid(nome).name() << "\n";
}
- using namespace std::literals;
- Libera os sufixos da biblioteca padrão (
spara textos, e outros para tempo, como5s= 5 segundos, em chrono). Ousing namespaceé explicado em Namespaces. - "Ana"s
- Texto + sufixo
s=std::string. Sem espaço entre as aspas e os.
$ g++ -std=c++23 -Wall -Wextra literal-s.cpp -o literal-s $ ./literal-s Ana tem 3 letras NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE $ ./literal-s | tail -1 | c++filt -t std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >
Agora o .size() funciona (3 letras), e o nome traduzido pelo c++filt é o nome completo do std::string dentro da biblioteca do g++ (basic_string<char> e seus acessórios). O tail -1 pega só a última linha da saída.
std::string
std::string nome = "Ana";(tipo escrito: o mais claro para iniciantes);auto nome = std::string{"Ana"};(o tipo aparece do lado direito);auto nome = "Ana"s;(curto, mas exige ousing namespace std::literals;).
Os três criam exatamente a mesma coisa. Neste guia, o primeiro é o mais usado.
5. As pegadinhas do auto
auto precisa de um valor
$ cat semvalor.cpp
int main() {
auto total;
}
$ g++ -std=c++23 -Wall -Wextra semvalor.cpp -o semvalor
semvalor.cpp: In function ‘int main()’:
semvalor.cpp:2:5: error: declaration of ‘auto total’ has no initializer
2 | auto total;
| ^~~~
"A declaração de auto total não tem inicializador": sem valor, não há do que deduzir. Um efeito colateral bom: com auto, é impossível esquecer de inicializar a variável (o "lixo" da aula Variáveis).
O tipo vem do primeiro valor, e não muda
#include <iostream>
int main() {
auto preco = 10;
preco = preco * 1.15;
std::cout << "Preço novo: " << preco << "\n";
}
$ g++ -std=c++23 -Wall -Wextra preco.cpp -o preco $ ./preco Preço novo: 11 « o aumento de 15% deveria dar 11.5
O 10 fez o preco nascer int. A conta 10 * 1.15 dá 11.5, mas guardar 11.5 num int corta a fração (aula Conversão de Tipos), sem aviso com -Wall -Wextra. Só o -Wconversion avisa:
$ g++ -std=c++23 -Wall -Wextra -Wconversion preco.cpp -o preco
preco.cpp: In function ‘int main()’:
preco.cpp:5:19: warning: conversion from ‘double’ to ‘int’ may change value [-Wfloat-conversion]
5 | preco = preco * 1.15;
| ~~~~~~^~~~~~
Com auto, 10 e 10.0 fazem toda a diferença. A missão final é sobre isso.
auto x{5} × auto x = {5}
Com chaves, uma diferença de um sinal de igual muda o tipo:
#include <initializer_list>
template <typename T>
struct QualTipo;
int main() {
auto a{5};
auto b = {5};
QualTipo<decltype(a)> espiaoA;
QualTipo<decltype(b)> espiaoB;
}
$ g++ -std=c++23 -Wall -Wextra chaves.cpp -o chaves
chaves.cpp: In function ‘int main()’:
chaves.cpp:9:27: error: aggregate ‘QualTipo<int> espiaoA’ has incomplete type and cannot be defined
9 | QualTipo<decltype(a)> espiaoA;
| ^~~~~~~
chaves.cpp:10:27: error: aggregate ‘QualTipo<std::initializer_list<int> > espiaoB’ has incomplete type and cannot be defined
10 | QualTipo<decltype(b)> espiaoB;
| ^~~~~~~
auto a{5}; é um int, como esperado. Já auto b = {5}; é uma std::initializer_list<int>: uma lista de inteiros com um elemento só (as listas aparecem em std::vector). Sem o #include <initializer_list>, o g++ nem aceita essa linha. Regra: com auto, use = com valor simples (auto x = 5;) ou chaves sem = (auto x{5};), nunca = {5}.
6. Quando usar e quando evitar
| Situação | auto? | Exemplo |
|---|---|---|
| O tipo já está escrito do lado direito | Sim: repetir não acrescenta nada | auto nome = std::string{"Ana"}; |
| Nomes de tipo longos (você vai encontrá-los nos iteradores e mapas) | Sim: é para isso que o auto nasceu | auto it = mapa.begin(); (aula Iteradores) |
| Percorrer uma coleção | Sim, muito comum | for (auto nota : notas) (aula for de intervalo) |
| Guardar o resultado de uma função cujo tipo você não sabe de cabeça | Com cuidado: o leitor também não vai saber | auto total = calcula(); |
| Número que vai receber valores com fração depois | Evite, ou use o literal certo | double preco = 10; ou auto preco = 10.0; |
Texto que vai usar .size() e companhia | Só com o sufixo s | std::string nome = "Ana"; |
| Aprendendo a linguagem | Prefira escrever o tipo | Escrever int e double treina o olho para os tipos |
Um meio-termo útil: const auto funciona como você imagina (const auto limite = 100; é um const int), e as aulas de Referências mostram auto&.
Quando dá errado
error: declaration of ‘auto x’ has no initializer
O que significa: auto sem valor: não há como deduzir o tipo.
Como consertar: dê um valor (auto x = 0;) ou escreva o tipo.
error: request for member ‘size’ in ‘x’, which is of non-class type ‘const char*’
O que significa: auto x = "texto"; criou um const char*, que não tem .size(), .length() etc.
Como consertar: std::string x = "texto"; ou auto x = "texto"s; (com using namespace std::literals;).
error: unable to find string literal operator ‘operator""s’ with ‘const char [4]’, ‘long unsigned int’ arguments
O que significa: você usou "Ana"s sem liberar os sufixos.
Como consertar: acrescente using namespace std::literals; (e o #include <string>).
auto perdeu a fração
O que significa: a variável nasceu int porque o primeiro valor era inteiro (auto x = 10;).
Como consertar: use o literal certo (10.0) ou escreva o tipo (double). Na dúvida, rode o espião.
Referência rápida
| Escrita | Tipo |
|---|---|
auto x = 5; / auto x{5}; | int |
auto x = 5.0; | double |
auto x = 5.0f; | float |
auto x = 5LL; | long long |
auto x = 'a'; / auto x = true; | char / bool |
auto x = "oi"; | const char* (não é std::string!) |
auto x = "oi"s; | std::string (com using namespace std::literals;) |
auto x = {5}; | std::initializer_list<int> (evite) |
QualTipo<decltype(x)> espiao; | Truque: o erro mostra o tipo |
typeid(x).name() + c++filt -t | Truque: mostra o tipo ao rodar |
Exercícios
1. Qual tipo?
Que tipo o compilador deduz em auto media = 7.5;?
2. Texto de verdade
Complete para nome ser um std::string:
3. Que tipo tem auto cidade = "Recife";?
4. Depois de auto x = 3;, o que x = 3.9; faz?
5. O que acontece com auto total;?
Missão final: o aumento de preço
Missão: o caderno custava 10 e teve aumento de 15%. O programa mostra Preço novo: 11, mas o certo é Preço novo: 11.5. Mantenha o auto e conserte só o valor inicial, para o compilador deduzir o tipo certo.
#include <iostream>
int main() {
auto preco = 10;
preco = preco * 1.15;
std::cout << "Preço novo: " << preco << "\n";
}
#include <iostream>
int main() {
auto preco = 10.0;
preco = preco * 1.15;
std::cout << "Preço novo: " << preco << "\n";
}
Resumo
autodeduz o tipo pelo valor, na compilação, uma vez só. Depois, o tipo é fixo.- A dedução segue os literais:
5→int,5.0→double,5.0f→float,'x'→char,"oi"→const char*. - Para espiar um tipo:
QualTipo<decltype(x)> espiao;(o erro mostra o tipo) outypeid(x).name()+c++filt -t. auto nome = "Ana";não éstd::string;"Ana"sé (comusing namespace std::literals;).- Pegadinhas:
autoexige valor;auto preco = 10;éint;auto x = {5};é uma lista. - Use
autoquando o tipo é óbvio ou comprido demais; escreva o tipo quando ele ajuda o leitor (e enquanto você está aprendendo).