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] = …;.
- Criar um
std::paire ler as duas partes com.firste.second; o que faz ostd::make_pair. - Criar um
std::tuplee ler cada parte comstd::get<0>,std::get<1>… - Usar
pairetuplepara uma função devolver vários valores. - Desmontar tudo isso com structured bindings (
auto [a, b] = …;): com pair, tuple, struct e noforde umstd::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
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::arrayou o retorno de uma função. - const auto& [fruta, quantidade] : estoque
- No
forde um map, cada elemento é um par chave/valor; aqui ele é aberto direto em dois nomes. Oconst 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
| Forma | Quando 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ção | Use | Por quê |
|---|---|---|
| Dois valores, uso rápido e local (o retorno de uma função auxiliar, um par chave/valor) | std::pair | Pronto, compara e ordena sozinho. |
| Três ou mais valores, uso rápido e local | std::tuple | Idem, sem limite de partes. |
| Os dados aparecem em várias partes do programa | struct | aluno.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::optional | Aula 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
| Escrita | O que faz |
|---|---|
std::pair<A, B> p = {a, b}; | Cria um par (<utility>) |
p.first, p.second | As 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:
2. Pegar da tupla
Complete para pegar a terceira parte da tupla t:
3. Desmontar
Complete para abrir o par devolvido por dividir(17, 5) em q e r:
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:.firste.second. Pares se comparam pelofirste desempatam pelosecond.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¶ ler sem copiar;auto¶ mudar o original.- No
forde um map:for (const auto& [chave, valor] : mapa). - Com texto, escreva os tipos do pair: sem eles,
"Ana"viraconst char*. - Dados que aparecem no programa todo merecem uma struct com nomes.