O Conforto do C++ Moderno — Structured Bindings e Dedução de Tipos

O Conforto do C++ Moderno — Structured Bindings e Dedução de Tipos

Dar nome ao que vinha como .first e .second muda mais a leitura do que parece, e é só metade do assunto. A outra é a dedução do auto e suas regras exatas: quando ele copia, quando descarta const e referência, e por que esse mesmo mecanismo é o que sustenta a dedução de tipos nos templates.
Linguagem C++

11 min de leitura

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, incluindo auto&, const auto& e auto&&.
  • 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 e auto.
  • 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::stringcó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.

Comentários

Mais em Linguagem C++

A Disciplina do const — Promessas que o Compilador Cobra
A Disciplina do const — Promessas que o Compilador Cobra

Em C o const é etiqueta modesta, que muita gente ignora sem prejuízo. Em C++…

O Cadeado que Protege — std::mutex e lock_guard
O Cadeado que Protege — std::mutex e lock_guard

O cadeado resolve a corrida serializando o acesso, e o lock_guard garante que…

Provando que Funciona — Testes Automatizados com Catch2
Provando que Funciona — Testes Automatizados com Catch2

Rodar e olhar a saída não protege contra regressão, porque ninguém repete a…