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.