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.

Você vai aprender
  • O que auto faz e o que ele deduz para 5, 5.0, 'x', true e "oi".
  • Dois truques reais para descobrir um tipo: o erro proposital e o typeid com c++filt.
  • Por que auto nome = "Ana"; não é um std::string, e como o sufixo s ("Ana"s) resolve.
  • As pegadinhas: auto sem valor, auto preco = 10; e auto x = {5};.
  • Quando auto ajuda 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 QualTipo que recebe um tipo T, 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çãoTipo deduzidoPor quê
auto a = 5;intNúmero sem ponto.
auto b = 5.0;doubleNúmero com ponto.
auto c = 5.0f;floatSufixo f.
auto d = 5LL;long long int (= long long)Sufixo LL.
auto e = 'x';charAspas simples.
auto f = true;boolPalavra-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 (s para textos, e outros para tempo, como 5s = 5 segundos, em chrono). O using namespace é explicado em Namespaces.
"Ana"s
Texto + sufixo s = std::string. Sem espaço entre as aspas e o s.
$ 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.

Três jeitos de ter um 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 o using 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çãoauto?Exemplo
O tipo já está escrito do lado direitoSim: repetir não acrescenta nadaauto nome = std::string{"Ana"};
Nomes de tipo longos (você vai encontrá-los nos iteradores e mapas)Sim: é para isso que o auto nasceuauto it = mapa.begin(); (aula Iteradores)
Percorrer uma coleçãoSim, muito comumfor (auto nota : notas) (aula for de intervalo)
Guardar o resultado de uma função cujo tipo você não sabe de cabeçaCom cuidado: o leitor também não vai saberauto total = calcula();
Número que vai receber valores com fração depoisEvite, ou use o literal certodouble preco = 10; ou auto preco = 10.0;
Texto que vai usar .size() e companhiaSó com o sufixo sstd::string nome = "Ana";
Aprendendo a linguagemPrefira escrever o tipoEscrever 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>).

Uma conta com 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

EscritaTipo
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 -tTruque: 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:

auto nome = "Ana";

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

  • auto deduz 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) ou typeid(x).name() + c++filt -t.
  • auto nome = "Ana"; não é std::string; "Ana"s é (com using namespace std::literals;).
  • Pegadinhas: auto exige valor; auto preco = 10; é int; auto x = {5}; é uma lista.
  • Use auto quando o tipo é óbvio ou comprido demais; escreva o tipo quando ele ajuda o leitor (e enquanto você está aprendendo).