Menu

pair e tuple em C++: agrupe valores sem usar struct

Como std::pair e std::tuple empacotam dois ou mais valores em um único objeto: como criá-los, acessar os campos, structured bindings e onde cada um se encaixa.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Dois valores, um objeto

Às vezes você precisa manter duas coisas juntas: um nome e uma pontuação, um x e um y, um sinalizador de "deu certo?" e o resultado. Você poderia definir um struct para cada agrupamento desses, mas para agrupamentos descartáveis isso é cerimônia demais. std::pair (de <utility>) empacota exatamente dois valores em um único objeto, e std::tuple (de <tuple>) generaliza isso para qualquer quantidade fixa de valores.

Você já encontrou std::pair indiretamente até chegar aqui: cada elemento de um std::map é um pair<const Key, Value>. Esta página torna isso explícito e mostra as formas modernas e legíveis de construir e desempacotar esses tipos.

Os dois membros sempre se chamam .first e .second: eles não assumem os nomes das suas variáveis. Esse é o preço de um tipo genérico: os nomes dos campos são posicionais, não descritivos.

Criando pairs

Há três formas comuns de criar um pair, e todas produzem o mesmo objeto.

make_pair deduz os tipos dos elementos para você, o que era prático antes do C++17. Hoje a inicialização com chaves mais a dedução de argumentos de template de classe (pair p{"Boris", 85};) cobre a maioria dos casos, mas você ainda vai ver make_pair por toda parte em código já existente.

Uma armadilha com a dedução: make_pair("hi", 3) deduz pair<const char*, int>, não pair<string, int>. Literais de string não são std::string. Se você precisa de um string, diga isso explicitamente —make_pair(string("hi"), 3) ou escreva o tipo do pair por extenso— caso contrário, você pode obter comparações ou cópias surpreendentes mais adiante.

Desempacotando com structured bindings

Ler .first e .second por toda parte fica ilegível rápido, porque os nomes não dizem nada. Os structured bindings do C++17 permitem dar nomes reais aos dois campos em uma única linha:

Isso se destaca em um for baseado em intervalo sobre um map, em que cada elemento é um pair. Em vez de it->first / it->second, você nomeia diretamente a chave e o valor:

Use const auto& no laço, assim como faria com qualquer elemento de contêiner: evita copiar cada pair e sinaliza que você está apenas lendo. Tire o & e você copia cada entrada; isso é um bug de desempenho silencioso em um map grande.

Quando dois não bastam: tuple

Um pair para em dois valores. Quando você precisa de três ou mais, std::tuple é a mesma ideia com uma quantidade arbitrária. Você o constrói com inicialização com chaves ou make_tuple, e o lê com std::get<N>, onde N é um índice conhecido em tempo de compilação.

O índice dentro de get<> precisa ser uma constante conhecida em tempo de compilação. get<i>(record) em que i é uma variável em tempo de execução não vai compilar: os campos de um tuple podem ter tipos diferentes, então o tipo do elemento precisa ser resolvido durante a compilação, não em tempo de execução. Se você se pegar querendo um índice em tempo de execução, provavelmente o que você quer é um vector.

Os structured bindings também funcionam com tuples, e esta é a forma legível de consumir um:

Retornando vários valores

A razão do dia a dia para recorrer a esses tipos é retornar mais de um valor de uma função sem inventar um struct nem fazer malabarismos com parâmetros de saída. Empacote os resultados em um pair ou tuple e desempacote no local da chamada.

Para três resultados ou mais, retorne um tuple da mesma forma. Há também o std::tie, um truque mais antigo que desempacota em variáveis já existentes em vez de declarar novas, útil quando você quer ignorar um campo com std::ignore:

Mas saiba quando parar: se o mesmo grupo de campos aparece em mais de um lugar, ou se você sempre esquece se .second é a pontuação ou a contagem, defina um struct com membros nomeados. pair e tuple são ideais para agrupamentos locais e de vida curta; campos nomeados vencem no instante em que os dados vivem mais do que uma única expressão.

Comparando e ordenando

Um bônus prático: pair e tuple vêm com operadores de comparação embutidos que funcionam de forma lexicográfica: comparam o primeiro elemento e só recorrem ao próximo quando os primeiros são iguais. Isso os torna chaves de ordenação perfeitas.

Note que a ordem dos campos importa: colocar age primeiro ordena principalmente por idade, e depois por nome como critério de desempate. Se você quisesse ordenação por nome primeiro, trocaria a ordem dos elementos do tuple. Essa comparação padrão é exatamente o motivo pelo qual pair<priority, item> é um idioma comum para filas de prioridade.

A seguir: Iteradores

Você já viu .first, .second, it->first e *it aparecerem ao redor dos contêineres; o que de fato conecta um elemento pair ao map em que ele vive é um iterador. A próxima página explica os iteradores de verdade: o que begin() e end() realmente retornam, como ++it percorre um contêiner e as armadilhas de invalidação de iteradores que causam alguns dos comportamentos indefinidos mais cruéis de C++.

Perguntas frequentes

Qual é a diferença entre pair e tuple em C++?

std::pair guarda exatamente dois valores, acessados com .first e .second. std::tuple guarda qualquer quantidade fixa de valores (zero, dois, três ou mais), acessados com std::get<N>(t). Um pair é basicamente uma tupla de dois elementos com nomes de membro mais amigáveis; recorra ao tuple apenas quando precisar de três campos ou mais.

Como acessar os elementos de um tuple em C++?

Use std::get<N>(t) com um índice conhecido em tempo de compilação, por exemplo std::get<0>(t). A partir do C++17 você também pode desempacotar com structured bindings: auto [a, b, c] = t; dá a cada elemento sua própria variável nomeada. Você não pode indexar um tuple com uma variável em tempo de execução: std::get<i> exige que i seja uma constante.

Como retornar vários valores de uma função em C++?

Retorne um std::pair ou um std::tuple e desempacote no local da chamada com structured bindings: auto [ok, value] = parse(text);. Isso é mais limpo do que parâmetros de saída e evita definir um struct de uso único, embora um struct nomeado seja mais legível quando os campos vivem além de uma única chamada.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

COMEÇAR