C++ pair, tuple e Desestruturação

Às vezes dois ou três valores andam sempre juntos: o nome do aluno e a nota, o produto e a quantidade, o menor e o maior número de uma lista. Criar uma struct para cada combinação dá trabalho. Para esses casos rápidos, o C++ tem o std::pair (par: dois valores) e o std::tuple (tupla: quantos você quiser). E, desde o C++17, uma forma elegante de "desmontar" esses pacotes em variáveis com nome: auto [nome, nota] = …;.

Você vai aprender
  • Criar um std::pair e ler as duas partes com .first e .second; o que faz o std::make_pair.
  • Criar um std::tuple e ler cada parte com std::get<0>, std::get<1>…
  • Usar pair e tuple para uma função devolver vários valores.
  • Desmontar tudo isso com structured bindings (auto [a, b] = …;): com pair, tuple, struct e no for de um std::map.
  • Quando é melhor usar uma struct.

Os programas foram compilados com o g++ 15.2 na máquina fictícia da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: o envelope com dois papéis

Pense num envelope com duas folhas dentro: a primeira com o nome do aluno, a segunda com a nota. Você entrega o envelope inteiro, e quem recebe abre e tira as duas folhas. Um pair é esse envelope. Um tuple é um envelope que cabe três, quatro, quantas folhas forem necessárias. E as structured bindings são o gesto de abrir o envelope e dar um nome a cada folha.


1. std::pair: dois valores juntos

#include <iostream>
#include <string>
#include <utility>

int main() {
    std::pair<std::string, double> aluno = {"Ana", 9.5};

    std::cout << "Nome: " << aluno.first << '\n';
    std::cout << "Nota: " << aluno.second << '\n';

    aluno.second = 10.0;
    std::cout << aluno.first << " agora tem " << aluno.second << '\n';

    auto outro = std::make_pair(std::string("Bruno"), 7.0);
    std::cout << outro.first << ": " << outro.second << '\n';
}
#include <utility>
O cabeçalho do std::pair (utility = "utilidades").
std::pair<std::string, double>
Um par cuja primeira parte é um texto e a segunda, um número com vírgula. Os dois tipos podem ser diferentes.
= {"Ana", 9.5};
Os dois valores, na ordem.
aluno.first
A primeira parte (first = "primeiro").
aluno.second
A segunda parte (second = "segundo"). Dá para ler e também mudar: aluno.second = 10.0;.
std::make_pair(a, b)
Cria um par descobrindo os tipos pelos valores. Era muito usado antes do C++17; hoje, a forma com chaves é mais comum.
$ g++ -std=c++23 -Wall -Wextra par.cpp -o par
$ ./par
Nome: Ana
Nota: 9.5
Ana agora tem 10
Bruno: 7

Você já encontrou pares sem saber: cada elemento de um std::map é um pair de chave e valor, por isso se escreve item.first e item.second ao percorrer um map.

Pares sabem se comparar

Dois pares são comparados primeiro pelo first; só em caso de empate, pelo second. Por isso um vetor de pares pode ser ordenado direto com std::sort:

#include <algorithm>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::vector<std::pair<std::string, int>> estoque = {
        {"maçã", 12}, {"banana", 30}, {"abacate", 4}, {"banana", 8}
    };

    std::sort(estoque.begin(), estoque.end());
    for (const auto& item : estoque) {
        std::cout << item.first << ": " << item.second << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra pares_ordenar.cpp -o pares_ordenar
$ ./pares_ordenar
abacate: 4
banana: 8                            « empate em "banana": desempata pelo número
banana: 30
maçã: 12
Cuidado com std::pair p{"Ana", 9.5}; sem os tipos

Desde o C++17, você pode omitir os tipos e deixar o compilador deduzir. Mas "Ana", escrito assim, não é um std::string: é um texto "cru" do C, do tipo const char* (aula Strings). O par sai com o tipo errado, e métodos de string não existem:

$ cat ctad.cpp
#include <iostream>
#include <string>
#include <utility>

int main() {
    std::pair p{"Ana", 9.5};
    std::string nome = p.first;
    std::cout << nome << ' ' << p.second << '\n';
    std::cout << p.first.size() << '\n';
}
$ g++ -std=c++23 -Wall -Wextra ctad.cpp -o ctad
ctad.cpp: In function ‘int main()’:
ctad.cpp:9:26: error: request for member ‘size’ in ‘p.std::pair<const char*, double>::first’, which is of non-class type ‘const char*’
    9 |     std::cout << p.first.size() << '\n';
      |                          ^~~~

A mensagem entrega o segredo: o par virou std::pair<const char*, double>. Escreva os tipos (std::pair<std::string, double>) quando houver texto.


2. std::tuple: três ou mais valores

O std::tuple (cabeçalho <tuple>) é a generalização do par. Como as partes não têm nome (não existe .third), você as pega pela posição, contando a partir de 0, com std::get<posição>:

#include <iostream>
#include <string>
#include <tuple>

int main() {
    std::tuple<std::string, int, double> produto = {"caderno", 3, 12.9};

    std::cout << "Produto: " << std::get<0>(produto) << '\n';
    std::cout << "Quantidade: " << std::get<1>(produto) << '\n';
    std::cout << "Preço: " << std::get<2>(produto) << '\n';

    std::get<1>(produto) = 5;
    std::cout << "Nova quantidade: " << std::get<1>(produto) << '\n';

    auto outro = std::make_tuple(std::string("caneta"), 10, 2.5);
    std::cout << std::get<0>(outro) << " x" << std::get<1>(outro) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra tupla.cpp -o tupla
$ ./tupla
Produto: caderno
Quantidade: 3
Preço: 12.9
Nova quantidade: 5
caneta x10
std::tuple<std::string, int, double>
Três partes: posição 0 é texto, 1 é inteiro, 2 é double.
std::get<0>(produto)
A parte da posição 0. O número vai entre < >, não entre parênteses.
std::get<1>(produto) = 5;
Também serve para mudar a parte.
std::make_tuple(…)
Como o make_pair: cria a tupla deduzindo os tipos.

Por que <0> e não [0], como num vetor? Porque cada posição tem um tipo diferente, e o compilador precisa saber, ao compilar, qual tipo vai sair dali. Por isso a posição tem de ser um número fixo, escrito no código. Uma variável não serve:

$ cat tupla_i.cpp
#include <iostream>
#include <tuple>

int main() {
    std::tuple<int, int, int> t = {1, 2, 3};
    for (int i = 0; i < 3; ++i) {
        std::cout << std::get<i>(t) << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra tupla_i.cpp -o tupla_i
tupla_i.cpp: In function ‘int main()’:
tupla_i.cpp:7:33: error: no matching function for call to ‘get<i>(std::tuple<int, int, int>&)’
    7 |         std::cout << std::get<i>(t) << '\n';
      |                      ~~~~~~~~~~~^~~
tupla_i.cpp:7:33: note: there are 27 candidates
                                     « … (a mensagem tem 312 linhas, quase todas notes sobre os 27 candidatos; no meio delas:)
tupla_i.cpp:7:33: error: the value of ‘i’ is not usable in a constant expression
    7 |         std::cout << std::get<i>(t) << '\n';
      |                      ~~~~~~~~~~~^~~
tupla_i.cpp:6:14: note: ‘int i’ is not const
    6 |     for (int i = 0; i < 3; ++i) {
      |              ^

The value of ‘i’ is not usable in a constant expression = "o valor de i não pode ser usado numa expressão constante", ou seja, num valor conhecido na compilação. Esse é o assunto de constexpr e consteval. Se você precisa percorrer com um índice, o que você quer é um std::vector ou um std::array, não uma tupla.


3. Devolver vários valores de uma função

Uma função só tem um return e devolve uma coisa só (aula Retorno). Mas essa "coisa" pode ser um par ou uma tupla, e aí cabem vários valores:

#include <iostream>
#include <tuple>
#include <utility>
#include <vector>

std::pair<int, int> menor_e_maior(const std::vector<int>& v) {
    int menor = v[0];
    int maior = v[0];
    for (int x : v) {
        if (x < menor) menor = x;
        if (x > maior) maior = x;
    }
    return {menor, maior};
}

std::tuple<int, int, double> resumo(const std::vector<int>& v) {
    int soma = 0;
    for (int x : v) soma += x;
    double media = static_cast<double>(soma) / v.size();
    return {static_cast<int>(v.size()), soma, media};
}

int main() {
    std::vector<int> notas = {7, 10, 4, 8};

    auto extremos = menor_e_maior(notas);
    std::cout << "Menor: " << extremos.first << ", maior: " << extremos.second << '\n';

    auto r = resumo(notas);
    std::cout << std::get<0>(r) << " notas, soma " << std::get<1>(r)
              << ", média " << std::get<2>(r) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra minmax.cpp -o minmax
$ ./minmax
Menor: 4, maior: 10
4 notas, soma 29, média 7.25

O return {menor, maior}; monta o par direto, porque o compilador já sabe, pela primeira linha da função, que ela devolve um std::pair<int, int>. Funciona, mas repare como o main ficou difícil de ler: extremos.first é o menor? std::get<1>(r) era a soma ou a média? A próxima seção resolve isso.


4. Structured bindings: auto [a, b] = …; C++17

A "desestruturação" (em inglês, structured bindings, "ligações estruturadas") abre o pacote e dá um nome a cada parte, numa linha só:

#include <iostream>
#include <map>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

struct Ponto {
    int x;
    int y;
};

std::pair<int, int> menor_e_maior(const std::vector<int>& v) {
    int menor = v[0], maior = v[0];
    for (int x : v) {
        if (x < menor) menor = x;
        if (x > maior) maior = x;
    }
    return {menor, maior};
}

int main() {
    // 1. com pair
    std::pair<std::string, double> aluno = {"Ana", 9.5};
    auto [nome, nota] = aluno;
    std::cout << nome << " tirou " << nota << '\n';

    // 2. com o retorno de uma função
    auto [menor, maior] = menor_e_maior({7, 10, 4, 8});
    std::cout << "De " << menor << " a " << maior << '\n';

    // 3. com tuple
    std::tuple<std::string, int, double> produto = {"caderno", 3, 12.9};
    auto [item, qtd, preco] = produto;
    std::cout << qtd << "x " << item << " = " << qtd * preco << '\n';

    // 4. com struct
    Ponto p = {3, -2};
    auto [x, y] = p;
    std::cout << "x=" << x << " y=" << y << '\n';

    // 5. com map, no for
    std::map<std::string, int> estoque = {{"banana", 30}, {"maçã", 12}};
    for (const auto& [fruta, quantidade] : estoque) {
        std::cout << fruta << ": " << quantidade << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra bindings.cpp -o bindings
$ ./bindings
Ana tirou 9.5
De 4 a 10
3x caderno = 38.7
x=3 y=-2
banana: 30
maçã: 12
auto
Obrigatório: o tipo de cada nome é deduzido sozinho (aula auto). Não dá para escrever std::string [nome, …].
[nome, nota]
Os nomes que você escolhe, um para cada parte, na ordem: o primeiro recebe o first (ou a posição 0, ou o primeiro campo da struct), o segundo recebe o seguinte, e assim por diante.
= aluno;
O pacote a abrir: um pair, uma tuple, uma struct com campos públicos, um std::array ou o retorno de uma função.
const auto& [fruta, quantidade] : estoque
No for de um map, cada elemento é um par chave/valor; aqui ele é aberto direto em dois nomes. O const auto& evita copiar cada par.

Compare o item 5 com a forma antiga, for (const auto& item : estoque) { … item.first … item.second … }. Com nomes, o código "fala" o que está fazendo. Isso é o que torna as structured bindings uma das novidades mais queridas do C++17.

Cópia ou referência?

Com auto [a, b], os nomes se referem a uma cópia do pacote. Para mudar o original, use auto&:

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> estoque = {{"banana", 30}, {"maçã", 12}};

    for (auto [fruta, qtd] : estoque) {
        qtd = 0;
    }
    std::cout << "Depois da cópia: banana=" << estoque["banana"] << '\n';

    for (auto& [fruta, qtd] : estoque) {
        qtd = 0;
    }
    std::cout << "Depois da referência: banana=" << estoque["banana"] << '\n';
}
$ g++ -std=c++23 -Wall -Wextra bind_ref.cpp -o bind_ref
$ ./bind_ref
Depois da cópia: banana=30           « zerou só a cópia
Depois da referência: banana=0       « zerou o estoque de verdade
FormaQuando usar
auto [a, b] = x;Valores pequenos que você só quer ler (ou uma cópia para mexer à vontade).
const auto& [a, b] = x;Ler sem copiar. O padrão no for de um map ou de um vetor de pares.
auto& [a, b] = x;Mudar o original através dos nomes.

Experimente: rode como está. Depois acrescente uma terceira fruta ao map. Por fim, troque const auto& por auto& e, dentro do for, some 10 a quantidade antes de mostrar.

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> estoque = {{"banana", 30}, {"pera", 12}};

    for (const auto& [fruta, quantidade] : estoque) {
        std::cout << fruta << ": " << quantidade << '\n';
    }
}

pair, tuple ou struct? Qual usar

SituaçãoUsePor quê
Dois valores, uso rápido e local (o retorno de uma função auxiliar, um par chave/valor)std::pairPronto, compara e ordena sozinho.
Três ou mais valores, uso rápido e localstd::tupleIdem, sem limite de partes.
Os dados aparecem em várias partes do programastructaluno.nota diz muito mais que aluno.second ou std::get<1>(aluno).
A função pode não ter resultado (busca que não acha)std::optionalAula optional e variant.

Com structured bindings, quem usa o pair/tuple ganha nomes. Mas quem lê a assinatura da função std::tuple<int, int, double> resumo(…) continua sem saber o que é cada int. Se a função é importante, uma struct com campos nomeados documenta melhor, e ela também funciona com auto [a, b, c] = …;.


Quando dá errado

error: only 2 names provided for structured binding
$ cat bind_erro.cpp
#include <string>
#include <tuple>

int main() {
    std::tuple<std::string, int, double> produto = {"caderno", 3, 12.9};
    auto [item, qtd] = produto;
}
$ g++ -std=c++23 -Wall -Wextra bind_erro.cpp -o bind_erro
bind_erro.cpp: In function ‘int main()’:
bind_erro.cpp:6:10: error: only 2 names provided for structured binding
    6 |     auto [item, qtd] = produto;
      |          ^~~~~~~~~~~
bind_erro.cpp:6:10: note: while ‘std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int, double>’ decomposes into 3 elements

O que significa: "só 2 nomes foram dados", enquanto a tupla "se decompõe em 3 elementos". (O nome comprido std::__cxx11::basic_string<char, …> é como o g++ escreve std::string por extenso.)

Como consertar: um nome para cada parte, sem pular. Se uma parte não interessa, dê um nome qualquer a ela mesmo assim.

error: static assertion failed: tuple index must be in range
$ g++ -std=c++23 -Wall -Wextra tupla_erro.cpp -o tupla_erro      « std::get<3> numa tupla de 3 partes
In file included from /usr/include/c++/15/bits/uses_allocator_args.h:41,
                                     « … (cortamos umas 10 linhas de "required by")
tupla_erro.cpp:6:23:   required from here
    6 |     return std::get<3>(produto);
      |            ~~~~~~~~~~~^~~~~~~~~
/usr/include/c++/15/tuple:2421:25: error: static assertion failed: tuple index must be in range
 2421 |       static_assert(__i < sizeof...(_Types), "tuple index must be in range");
      |                     ~~~~^~~~~~~~~~~~~~~~~~~
/usr/include/c++/15/tuple:2421:25: note: the comparison reduces to ‘(3 < 3)’
                                     « … e mais 28 linhas com outros erros da mesma causa

O que significa: "o índice da tupla precisa estar dentro do intervalo". As posições de uma tupla de 3 partes são 0, 1 e 2; a 3 não existe. A nota the comparison reduces to ‘(3 < 3)’ mostra a conta que deu falso. Procure a linha com required from here: é a sua.

Como consertar: conte a partir de 0. Melhor ainda: troque os std::get por auto [a, b, c] = …;, que não tem número para errar.

request for member ‘size’ in … which is of non-class type ‘const char*’

O que significa: o par ou a tupla foi criado sem os tipos (std::pair p{"Ana", 9.5}) e o texto virou const char*, que não tem métodos.

Como consertar: escreva os tipos (std::pair<std::string, double>) ou use std::string("Ana").


Referência rápida

EscritaO que faz
std::pair<A, B> p = {a, b};Cria um par (<utility>)
p.first, p.secondAs duas partes
std::make_pair(a, b)Cria um par deduzindo os tipos
std::tuple<A, B, C> t = {a, b, c};Cria uma tupla (<tuple>)
std::get<0>(t)A parte da posição 0 (número fixo, conhecido na compilação)
std::make_tuple(a, b, c)Cria uma tupla deduzindo os tipos
return {a, b};Devolve um par/tupla montado na hora
auto [x, y] = algo;Desmonta em cópias com nome
const auto& [x, y] = algo;Desmonta sem copiar, só leitura
auto& [x, y] = algo;Desmonta com acesso ao original
for (const auto& [k, v] : mapa)Percorre um map com nomes para chave e valor

Exercícios

1. A segunda parte

Complete para mostrar a nota guardada no par aluno:

std::cout << aluno. << '\n';

2. Pegar da tupla

Complete para pegar a terceira parte da tupla t:

std::get<>(t)

3. Desmontar

Complete para abrir o par devolvido por dividir(17, 5) em q e r:

[q, r] = dividir(17, 5);

4. Um vetor de pares {{"b", 2}, {"a", 9}, {"b", 1}} é ordenado com std::sort. Qual a ordem final?

5. Num for (auto [k, v] : mapa) { v = 0; }, o que acontece com os valores do mapa?

Missão: dividir com resto

Missão: complete a função dividir para devolver o quociente (a / b) e o resto (a % b) num par. No main, abra o resultado com auto [q, r] = dividir(17, 5);. A saída deve ser 17 / 5 = 3, resto 2.

#include <iostream>
#include <utility>

std::pair<int, int> dividir(int a, int b) {
    return {0, 0}; // conserte aqui
}

int main() {
    // abra o resultado de dividir(17, 5) em q e r
    std::cout << "17 / 5 = " << 0 << ", resto " << 0 << '\n';
}
#include <iostream>
#include <utility>

std::pair<int, int> dividir(int a, int b) {
    return {a / b, a % b};
}

int main() {
    auto [q, r] = dividir(17, 5);
    std::cout << "17 / 5 = " << q << ", resto " << r << '\n';
}

Missão final: o carrinho de compras

Missão: cada item do carrinho é uma tupla (produto, quantidade, preço). Percorra o vetor com for (const auto& [produto, qtd, preco] : carrinho), mostre 2x caderno = 25 para cada item e, no fim, Total: 124.9.

#include <iostream>
#include <string>
#include <tuple>
#include <vector>

int main() {
    std::vector<std::tuple<std::string, int, double>> carrinho = {
        {"caderno", 2, 12.5},
        {"caneta", 5, 2.0},
        {"mochila", 1, 89.9}
    };

    double total = 0.0;
    // percorra o carrinho aqui

    std::cout << "Total: " << total << '\n';
}
#include <iostream>
#include <string>
#include <tuple>
#include <vector>

int main() {
    std::vector<std::tuple<std::string, int, double>> carrinho = {
        {"caderno", 2, 12.5},
        {"caneta", 5, 2.0},
        {"mochila", 1, 89.9}
    };

    double total = 0.0;
    for (const auto& [produto, qtd, preco] : carrinho) {
        double subtotal = qtd * preco;
        std::cout << qtd << "x " << produto << " = " << subtotal << '\n';
        total += subtotal;
    }
    std::cout << "Total: " << total << '\n';
}

A solução, no laboratório:

$ g++ -std=c++23 -Wall -Wextra carrinho.cpp -o carrinho
$ ./carrinho
2x caderno = 25
5x caneta = 10
1x mochila = 89.9
Total: 124.9

Resumo

  • std::pair<A, B> junta dois valores: .first e .second. Pares se comparam pelo first e desempatam pelo second.
  • std::tuple<A, B, C…> junta quantos você quiser: std::get<posição>(t), com posição fixa a partir de 0.
  • Uma função devolve vários valores devolvendo um pair ou tuple: return {a, b};.
  • auto [a, b] = …; dá nomes às partes de um pair, tuple, struct ou array. const auto& para ler sem copiar; auto& para mudar o original.
  • No for de um map: for (const auto& [chave, valor] : mapa).
  • Com texto, escreva os tipos do pair: sem eles, "Ana" vira const char*.
  • Dados que aparecem no programa todo merecem uma struct com nomes.