Menu

pair e tuple in C++: raggruppare valori senza una struct

Come std::pair e std::tuple riuniscono due o più valori in un solo oggetto: crearli, accedere ai campi, structured binding e quando usare l'uno o l'altro.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Due valori, un oggetto

A volte devi tenere insieme due cose: un nome e un punteggio, una x e una y, un flag "ha funzionato?" e il risultato. Potresti definire una struct per ogni abbinamento del genere, ma per raggruppamenti usa e getta è un bel po' di cerimonia. std::pair (da <utility>) riunisce esattamente due valori in un solo oggetto, e std::tuple (da <tuple>) generalizza l'idea a un numero fisso qualsiasi di valori.

Hai già incontrato std::pair indirettamente arrivando qui: ogni elemento di una std::map è una pair<const Key, Value>. Questa pagina lo rende esplicito e mostra i modi moderni e leggibili per costruire e spacchettare questi tipi.

I due membri si chiamano sempre .first e .second: non prendono i nomi delle tue variabili. È il prezzo di un tipo generico: i nomi dei campi sono posizionali, non descrittivi.

Creare coppie

Ci sono tre modi comuni per creare una pair, e producono tutti lo stesso oggetto.

make_pair deduce per te i tipi degli elementi, cosa comoda prima di C++17. Oggi l'inizializzazione con le graffe più la deduzione degli argomenti dei template di classe (pair p{"Boris", 85};) copre la maggior parte dei casi, ma vedrai ancora make_pair ovunque nel codice esistente.

Una trappola della deduzione: make_pair("hi", 3) deduce pair<const char*, int>, non pair<string, int>. I letterali stringa non sono std::string. Se ti serve una string, dillo esplicitamente, con make_pair(string("hi"), 3) o scrivendo per intero il tipo della pair, altrimenti più avanti potresti ottenere confronti o copie inattesi.

Spacchettare con le structured binding

Leggere .first e .second ovunque diventa presto illeggibile, perché i nomi non ti dicono nulla. Le structured binding di C++17 ti permettono di dare ai due campi nomi veri in una sola riga:

Danno il meglio in un for basato su range su una map, dove ogni elemento è una pair. Invece di it->first / it->second, dai direttamente un nome alla chiave e al valore:

Usa const auto& nel ciclo, come faresti per qualsiasi elemento di un container: evita di copiare ogni pair e segnala che stai solo leggendo. Togli la & e copi ogni voce; su una map grande è un bug di prestazioni silenzioso.

Quando due non bastano: tuple

Una pair si ferma a due valori. Quando te ne servono tre o più, std::tuple è la stessa idea con un numero arbitrario di elementi. Ne costruisci una con le graffe o con make_tuple, e la leggi con std::get<N>, dove N è un indice noto in fase di compilazione.

L'indice dentro get<> deve essere una costante nota in fase di compilazione. get<i>(record) con i variabile a runtime non compila: i campi di una tupla possono avere tipi diversi, quindi il tipo dell'elemento va risolto durante la compilazione, non durante l'esecuzione. Se ti ritrovi a volere un indice a runtime, probabilmente ti serve un vector.

Le structured binding funzionano anche con le tuple, ed è il modo leggibile di usarne una:

Restituire più valori

Il motivo quotidiano per usare questi tipi è restituire più di un valore da una funzione senza inventare una struct o destreggiarsi con parametri di output. Riunisci i risultati in una pair o in una tuple e spacchettali nel punto della chiamata.

Per tre o più risultati, restituisci una tuple allo stesso modo. Esiste anche std::tie, un trucco più vecchio che spacchetta in variabili esistenti invece di dichiararne di nuove, utile quando vuoi ignorare un campo con std::ignore:

Quando fermarsi, però: se lo stesso gruppo di campi compare in più di un punto, o continui a dimenticare se .second è il punteggio o il conteggio, definisci una struct con membri dal nome esplicito. pair e tuple sono ideali per raggruppamenti locali e di breve durata; i campi con nome vincono non appena i dati vivono più a lungo di una singola espressione.

Confrontare e ordinare

Un bonus comodo: pair e tuple hanno operatori di confronto integrati che lavorano in modo lessicografico: confrontano il primo elemento e passano al successivo solo quando i primi sono uguali. Questo li rende chiavi perfette per l'ordinamento.

Nota che l'ordine dei campi conta: mettere age per primo ordina principalmente per età, poi per nome in caso di parità. Se volessi ordinare prima per nome, scambieresti l'ordine degli elementi della tupla. Questo confronto di default è proprio il motivo per cui pair<priority, item> è un idioma comune per le code con priorità.

Prossimo passo: gli iteratori

Ormai hai visto .first, .second, it->first e *it comparire attorno ai container: ciò che collega davvero un elemento pair alla map in cui vive è un iteratore. La prossima pagina spiega a fondo gli iteratori: cosa restituiscono davvero begin() ed end(), come ++it percorre un container e le trappole dell'invalidazione degli iteratori che causano alcuni dei casi di undefined behavior più insidiosi in C++.

Domande frequenti

Qual è la differenza tra pair e tuple in C++?

std::pair contiene esattamente due valori, a cui accedi con .first e .second. std::tuple contiene un numero fisso qualsiasi di valori (zero, due, tre o più), a cui accedi con std::get<N>(t). Una pair è in pratica una tupla di due elementi con nomi dei membri più comodi; usa tuple solo quando ti servono tre o più campi.

Come si accede agli elementi di una tuple in C++?

Usa std::get<N>(t) con un indice noto in fase di compilazione, per esempio std::get<0>(t). Da C++17 puoi anche spacchettarla con le structured binding: auto [a, b, c] = t; dà a ogni elemento una propria variabile con nome. Non puoi indicizzare una tupla con una variabile a runtime: std::get<i> richiede una i costante.

Come si restituiscono più valori da una funzione in C++?

Restituisci una std::pair o una std::tuple e spacchettala nel punto della chiamata con le structured binding: auto [ok, value] = parse(text);. È più pulito dei parametri di output ed evita di definire una struct usa e getta, anche se una struct con nome è più leggibile quando i campi sopravvivono oltre una singola chiamata.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA