Menu

pair y tuple en C++: agrupar valores sin un struct

Cómo std::pair y std::tuple agrupan dos o más valores en un solo objeto: cómo crearlos, acceder a los campos, los structured bindings y dónde encaja cada uno.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Dos valores, un objeto

A veces necesitas mantener dos cosas juntas: un nombre y una puntuación, una x y una y, un indicador de "¿funcionó?" y el resultado. Podrías definir un struct para cada agrupación de este tipo, pero para agrupaciones desechables eso es mucha ceremonia. std::pair (de <utility>) agrupa exactamente dos valores en un solo objeto, y std::tuple (de <tuple>) generaliza eso a cualquier número fijo de valores.

Ya te encontraste con std::pair indirectamente al llegar hasta aquí: cada elemento de un std::map es un pair<const Key, Value>. Esta página lo hace explícito y muestra las formas modernas y legibles de construir y desempaquetar estos tipos.

Los dos miembros siempre se llaman .first y .second: no adoptan los nombres de tus variables. Ese es el precio de un tipo genérico: los nombres de los campos son posicionales, no descriptivos.

Crear pairs

Hay tres formas habituales de crear un pair, y todas producen el mismo objeto.

make_pair deduce por ti los tipos de los elementos, lo que resultaba cómodo antes de C++17. Hoy la inicialización con llaves más la deducción de argumentos de plantilla de clase (pair p{"Boris", 85};) cubre la mayoría de los casos, pero todavía verás make_pair por todas partes en el código existente.

Un detalle peligroso con la deducción: make_pair("hi", 3) deduce pair<const char*, int>, no pair<string, int>. Los literales de cadena no son std::string. Si necesitas un string, dilo explícitamente —make_pair(string("hi"), 3) o escribe el tipo del pair completo— o de lo contrario podrías obtener comparaciones o copias sorprendentes más adelante.

Desempaquetar con structured bindings

Leer .first y .second por todas partes se vuelve ilegible rápido, porque los nombres no te dicen nada. Los structured bindings de C++17 te permiten dar a los dos campos nombres reales en una sola línea:

Esto brilla en un for basado en rango sobre un map, donde cada elemento es un pair. En lugar de it->first / it->second, nombras directamente la clave y el valor:

Usa const auto& en el bucle, igual que harías con cualquier elemento de contenedor: evita copiar cada pair y señala que solo estás leyendo. Quita el & y copiarás cada entrada; eso es un error de rendimiento silencioso en un map grande.

Cuando dos no bastan: tuple

Un pair se queda en dos valores. Cuando necesitas tres o más, std::tuple es la misma idea con un número arbitrario. Lo construyes con inicialización con llaves o con make_tuple, y lo lees con std::get<N>, donde N es un índice conocido en tiempo de compilación.

El índice dentro de get<> debe ser una constante conocida en tiempo de compilación. get<i>(record) donde i es una variable en tiempo de ejecución no compilará: los campos de un tuple pueden tener tipos diferentes, así que el tipo del elemento tiene que resolverse durante la compilación, no en tiempo de ejecución. Si te encuentras queriendo un índice en tiempo de ejecución, probablemente lo que quieres es un vector.

Los structured bindings también funcionan con tuples, y esta es la forma legible de consumir uno:

Devolver varios valores

La razón cotidiana para recurrir a estos tipos es devolver más de un valor desde una función sin inventar un struct ni hacer malabares con parámetros de salida. Agrupa los resultados en un pair o un tuple y desempaquétalos en el punto de llamada.

Para tres resultados o más, devuelve un tuple de la misma forma. También existe std::tie, un truco más antiguo que desempaqueta en variables ya existentes en lugar de declarar nuevas, útil cuando quieres ignorar un campo con std::ignore:

Cuándo parar, eso sí: si el mismo grupo de campos aparece en más de un lugar, o si sigues olvidando si .second es la puntuación o el recuento, define un struct con miembros con nombre. pair y tuple son ideales para agrupaciones locales y de corta vida; los campos con nombre ganan en cuanto los datos viven más que una sola expresión.

Comparar y ordenar

Una ventaja práctica: pair y tuple vienen con operadores de comparación integrados que funcionan lexicográficamente: comparan el primer elemento y solo recurren al siguiente cuando los primeros son iguales. Eso los convierte en claves de ordenación perfectas.

Fíjate en que el orden de los campos importa: poner age primero ordena principalmente por edad, y luego por nombre como desempate. Si quisieras un orden por nombre primero, intercambiarías el orden de los elementos del tuple. Esta comparación por defecto es exactamente la razón por la que pair<priority, item> es un modismo habitual para las colas de prioridad.

Siguiente: Iteradores

Ya has visto aparecer .first, .second, it->first y *it alrededor de los contenedores; lo que en realidad conecta un elemento pair con el map en el que vive es un iterador. La siguiente página desentraña los iteradores como es debido: qué devuelven realmente begin() y end(), cómo ++it recorre un contenedor y las trampas de invalidación de iteradores que provocan algunos de los comportamientos indefinidos más desagradables de C++.

Preguntas frecuentes

¿Cuál es la diferencia entre pair y tuple en C++?

std::pair contiene exactamente dos valores, a los que se accede con .first y .second. std::tuple contiene cualquier número fijo de valores (cero, dos, tres o más), a los que se accede con std::get<N>(t). Un pair es esencialmente una tupla de dos elementos con nombres de miembro más amigables; usa tuple solo cuando necesites tres campos o más.

¿Cómo se accede a los elementos de un tuple en C++?

Usa std::get<N>(t) con un índice conocido en tiempo de compilación, por ejemplo std::get<0>(t). Desde C++17 también puedes desempaquetarlo con structured bindings: auto [a, b, c] = t; da a cada elemento su propia variable con nombre. No puedes indexar un tuple con una variable en tiempo de ejecución: std::get<i> requiere que i sea una constante.

¿Cómo se devuelven varios valores desde una función en C++?

Devuelve un std::pair o un std::tuple y desempaquétalo en el punto de llamada con structured bindings: auto [ok, value] = parse(text);. Esto es más limpio que los parámetros de salida y evita definir un struct de un solo uso, aunque un struct con nombre es más legible cuando los campos sobreviven más allá de una sola llamada.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR