Chegamos ao artigo que fecha a Fase 6. Ao longo dela — e já desde a Fase 4 — dois recursos de sintaxe apareceram repetidamente aliviando o código: o desempacotamento com colchetes (auto [chave, valor] : mapa) e o auto deduzindo tipos por toda parte. Eu os apresentei de relance, prometendo o tratamento completo. É hora de cumprir. Os structured bindings e a dedução de tipos são os pilares da ergonomia do C++ moderno — não adicionam poder novo à linguagem, mas tornam o código drasticamente mais legível e menos propenso a erros de digitação de tipos verbosos. Dominá-los é o que faz seu C++ deixar de parecer traduzido do C e passar a fluir. E, de quebra, a dedução de tipos vai amarrar num só entendimento o auto da Fase 1 e os templates da Fase 5.
Structured bindings: desempacotando em variáveis nomeadas
Você já viu que percorrer um map dá acesso a pares com .first e .second, nomes que não dizem nada sobre o significado. Os structured bindings (C++17) permitem desempacotar um par, tupla ou struct em variáveis com nomes que você escolhe:
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> idades = {{"Ana", 30}, {"Bruno", 25}};
// Sem structured bindings: par.first / par.second (obscuro)
for (const auto& par : idades)
std::cout << par.first << ": " << par.second << '\n';
// Com structured bindings: nomes que revelam a intenção
for (const auto& [nome, idade] : idades)
std::cout << nome << " tem " << idade << " anos\n";
return 0;
}
O [nome, idade] desmonta cada par em duas variáveis nomeadas. A diferença é puramente de legibilidade — o código gerado é equivalente —, mas legibilidade é o que separa código sustentável de código enigmático. nome e idade comunicam; par.first e par.second obrigam a lembrar o que é o quê.
Além de maps: pares, tuplas e retornos múltiplos
O poder real dos structured bindings aparece com funções que precisam devolver mais de um valor. Em C, você usava parâmetros de saída por ponteiro. Em C++ moderno, devolve um par ou tupla, e o chamador o desempacota com nomes limpos:
#include <iostream>
#include <utility> // std::pair
#include <vector>
#include <algorithm>
// Devolve DOIS valores: o menor e o maior de um vetor.
std::pair<int, int> min_max(const std::vector<int>& v) {
auto [it_min, it_max] = std::minmax_element(v.begin(), v.end());
// ^ structured binding também funciona no resultado de minmax_element
return {*it_min, *it_max}; // constrói o par com os dois valores
}
int main() {
std::vector<int> dados = {5, 2, 8, 1, 9, 3};
// Desempacota o par retornado em dois nomes claros.
auto [menor, maior] = min_max(dados);
std::cout << "menor: " << menor << ", maior: " << maior << '\n'; // menor: 1, maior: 9
return 0;
}
Compare com a alternativa em C — void min_max(const int* v, int n, int* out_min, int* out_max) — com seus ponteiros de saída fáceis de confundir. Aqui, auto [menor, maior] = min_max(dados); devolve os dois valores e os nomeia numa linha. Isso também funciona com std::tuple (para três ou mais valores) e com structs simples, cujos membros são desempacotados na ordem de declaração — a mesma ordem que estudamos no artigo A Vida Começa Aqui — Inicialização de Membros a Fundo.
#include <iostream>
#include <string>
struct Ponto { int x; int y; int z; };
int main() {
Ponto p{10, 20, 30};
auto [a, b, c] = p; // desempacota os membros na ORDEM de declaração
std::cout << a << ' ' << b << ' ' << c << '\n'; // 10 20 30
return 0;
}
Dedução de tipos: as regras completas do auto
Agora consolidamos o auto, que você usa desde a Fase 1 mas cujas regras merecem ser vistas por inteiro. A regra fundamental, que já vimos: auto sozinho deduz um tipo por valor, descartando referências e const. Isso significa que auto sempre copia, a menos que você peça explicitamente uma referência:
#include <string>
const std::string& obtem_ref(); // devolve referência const
int main() {
auto a = obtem_ref(); // a é std::string — CÓPIA (auto descarta o const&)
const auto& b = obtem_ref(); // b é const std::string& — referência preservada
auto&& c = obtem_ref(); // c é referência (forwarding); avançado, veja adiante
return 0;
}
O quadro completo, que você deve gravar: auto copia (descarta const e &); auto& é uma referência mutável; const auto& é uma referência só-leitura (a forma preferida para percorrer sem copiar, como firmamos no artigo Adeus NULL, Bem-vindo à Dedução — nullptr e auto); e auto* é para ponteiros. A escolha entre eles é a mesma escolha entre passar por valor, por referência e por const& que você faz em parâmetros de função (artigos Referências e Ponteiros, Frente a Frente, A Disciplina do const — Promessas que o Compilador Cobra) — a coerência da linguagem recompensa quem entende um conceito e o vê reaparecer.
A conexão profunda: auto e templates são o mesmo mecanismo
Aqui está a amarração que ilumina duas fases de uma vez. A dedução do auto segue quase exatamente as mesmas regras da dedução de tipos de template da Fase 5. Quando você escreve auto x = expr;, o compilador deduz o tipo de x da mesma forma que deduziria o T de um template <typename T> void f(T x) chamado com expr. Não é coincidência — é o mesmo maquinário por baixo. Entender isso significa que, ao dominar um, você dominou o outro: as regras de "copia por padrão, referência só se pedida, const descartado" valem para os dois. É por isso que auto it = v.begin(); (Fase 4) e template <typename T> T maior(T, T) (Fase 5) "sentem" parecidos: são a mesma dedução, com roupas diferentes.
Structured bindings dão nome ao que antes era .first e .second, e o ganho é de leitura: quem lê [chave, valor] entende de imediato, enquanto par.second obriga a lembrar. A dedução do auto tem regras que convém conhecer de cor — auto sozinho descarta const e referência, produzindo cópia, e é preciso escrever auto& ou const auto& para conservá-los. O mecanismo, aliás, é o mesmo da dedução de tipos em templates.
Com isso fecha o bloco de tratamento de erros e C++ moderno. Exceções apoiadas no RAII, optional para ausência, variant para alternativas e expected para falha com motivo formam um vocabulário em que o tipo de retorno passa a dizer o que a função realmente pode devolver — em vez de deixar isso para a documentação ou para a memória de quem chama.
Fontes e leituras recomendadas
- cppreference.com/w/cpp/language/structured_bindings: a referência completa sobre structured bindings, cobrindo pares, tuplas e structs.
- cppreference.com/w/cpp/language/auto: as regras precisas de dedução do
auto, incluindoauto&,const auto&eauto&&. - Scott Meyers, Effective Modern C++ (2014), Itens 1–4 (dedução de tipos) e 5–6 (
auto): a exposição definitiva da conexão entre dedução de template eauto. - Bjarne Stroustrup, A Tour of C++ (3ª ed.), seções sobre structured bindings e
auto: a apresentação da ergonomia moderna pelo criador. - ISO C++ Core Guidelines, regras ES.11 (
auto) e ES.63/ES.56: diretrizes sobre uso de dedução de tipos e desempacotamento.
Exercícios
Exercício 1
Crie um std::pair<std::string, double> representando um produto e seu preço, e use structured bindings para extraí-los em duas variáveis nomeadas, imprimindo-as.
Ver resposta
✓ Resposta: Desempacotando um par:
#include <iostream>
#include <utility>
#include <string>
int main() {
std::pair<std::string, double> produto{"café", 12.50};
auto [nome, preco] = produto;
std::cout << nome << ": R$ " << preco << '\n'; // café: R$ 12.5
return 0;
}
Exercício 2
Escreva uma função que devolva um std::pair<bool, int> indicando se uma busca teve sucesso e, em caso positivo, o índice encontrado. No main, desempacote o retorno com structured bindings e trate os dois casos.
Ver resposta
✓ Resposta: Busca com retorno duplo:
#include <iostream>
#include <utility>
#include <vector>
std::pair<bool, int> busca(const std::vector<int>& v, int alvo) {
for (int i = 0; i < (int)v.size(); ++i)
if (v[i] == alvo) return {true, i};
return {false, -1};
}
int main() {
std::vector<int> v = {10, 20, 30};
auto [achou, indice] = busca(v, 20);
if (achou) std::cout << "achado no índice " << indice << '\n'; // achado no índice 1
else std::cout << "não encontrado\n";
return 0;
}
(Nota: para este caso, std::optional<int> do artigo Um Valor que Pode Não Existir — std::optional seria ainda mais idiomático que pair<bool,int>, mas o exercício ilustra o desempacotamento.)
Exercício 3
Para cada declaração, diga o tipo deduzido e se há cópia ou referência, dado que obtem() devolve const std::string&:
auto a = obtem();
auto& b = obtem(); // pense bem neste
const auto& c = obtem();
Ver resposta
✓ Resposta: Os tipos:
- auto a = obtem(); → a é std::string — cópia. O auto sozinho descarta o const e o & da referência retornada, produzindo um tipo por valor.
- auto& b = obtem(); → b é const std::string& — referência. Cuidado com o detalhe: como obtem() devolve const std::string&, o auto& preserva o const (não se pode ligar uma referência não-const a algo const), então b é uma referência const, sem cópia.
- const auto& c = obtem(); → c é const std::string& — referência só-leitura, sem cópia; a forma explícita e mais clara.
Exercício 4
Reescreva o laço abaixo, que usa .first/.second, com structured bindings, e explique por que a versão nova é menos propensa a erros.
#include <map>
#include <iostream>
std::map<int, std::string> m = {{1, "um"}, {2, "dois"}};
for (const auto& par : m)
std::cout << par.first << " = " << par.second << '\n';
Ver resposta
✓ Resposta: Com structured bindings:
#include <map>
#include <iostream>
int main() {
std::map<int, std::string> m = {{1, "um"}, {2, "dois"}};
for (const auto& [chave, valor] : m)
std::cout << chave << " = " << valor << '\n';
return 0;
}
A versão nova é menos propensa a erros porque nomeia explicitamente cada parte: chave e valor dizem o que são, enquanto par.first/par.second exigem que o programador lembre qual é qual. Em pares de tipos parecidos (por exemplo, pair<int, int>), trocar first por second por engano é um bug silencioso; com nomes descritivos, a intenção fica explícita e o erro fica óbvio. A legibilidade reduz a chance de confusão.
Exercício 5
Explique a afirmação da aula de que "a dedução do auto e a dedução de tipos de template são o mesmo mecanismo". Dê um exemplo comparando auto x = v.begin(); com um template equivalente, mostrando que o tipo deduzido é o mesmo.
Ver resposta
✓ Resposta: A afirmação significa que o compilador usa o mesmo algoritmo interno para descobrir o tipo em auto x = expr; e para descobrir o T em template <typename T> void f(T param) quando chamado com expr. Exemplo:
#include <vector>
std::vector<int> v = {1, 2, 3};
auto x = v.begin(); // x é std::vector<int>::iterator
// Equivalente com template:
template <typename T>
void f(T param); // se chamada como f(v.begin()), T = std::vector<int>::iterator
Em ambos os casos, o tipo deduzido para x e para T é std::vector<int>::iterator — o mesmo tipo, deduzido pelas mesmas regras (copia por valor, descarta const/referência de topo). Isso mostra que auto é, em essência, "deduza como se eu fosse o parâmetro T de um template recebendo esta expressão". Dominar a dedução de template (Fase 5) e a dedução de auto (Fases 1 e 6) é, portanto, dominar um único mecanismo — a coerência que faz o C++ moderno ser aprendível apesar de sua extensão.