Menu

pair et tuple en C++ : regrouper des valeurs sans struct

Comment std::pair et std::tuple regroupent deux valeurs ou plus dans un seul objet : comment les créer, accéder aux champs, les structured bindings et la place de chacun.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Deux valeurs, un seul objet

Parfois, vous devez garder deux choses ensemble : un nom et un score, un x et un y, un indicateur « ça a marché ? » et le résultat. Vous pourriez définir un struct pour chaque regroupement de ce genre, mais pour des regroupements jetables, c'est beaucoup de cérémonie. std::pair (de <utility>) regroupe exactement deux valeurs dans un seul objet, et std::tuple (de <tuple>) généralise cela à n'importe quel nombre fixe de valeurs.

Vous avez déjà croisé std::pair indirectement en arrivant ici : chaque élément d'un std::map est un pair<const Key, Value>. Cette page le rend explicite et montre les façons modernes et lisibles de construire et de décomposer ces types.

Les deux membres s'appellent toujours .first et .second : ils ne prennent pas les noms de vos variables. C'est le prix d'un type générique : les noms des champs sont positionnels, pas descriptifs.

Créer des pairs

Il existe trois façons courantes de créer un pair, et toutes produisent le même objet.

make_pair déduit les types des éléments à votre place, ce qui était pratique avant C++17. Aujourd'hui, l'initialisation par accolades combinée à la déduction des arguments de template de classe (pair p{"Boris", 85};) couvre la plupart des cas, mais vous verrez encore make_pair partout dans le code existant.

Un piège avec la déduction : make_pair("hi", 3) déduit pair<const char*, int>, et non pair<string, int>. Les littéraux de chaîne ne sont pas des std::string. Si vous avez besoin d'un string, dites-le explicitement —make_pair(string("hi"), 3) ou écrivez le type du pair en toutes lettres— sinon vous risquez d'obtenir plus tard des comparaisons ou des copies surprenantes.

Décomposer avec les structured bindings

Lire .first et .second partout devient vite illisible, car les noms ne vous apprennent rien. Les structured bindings de C++17 vous permettent de donner aux deux champs de vrais noms en une seule ligne :

Cela brille dans un for basé sur une plage parcourant un map, où chaque élément est un pair. Au lieu de it->first / it->second, vous nommez directement la clé et la valeur :

Utilisez const auto& dans la boucle, comme vous le feriez pour n'importe quel élément de conteneur : cela évite de copier chaque pair et signale que vous ne faites que lire. Retirez le & et vous copiez chaque entrée ; c'est un bug de performance silencieux sur un grand map.

Quand deux ne suffisent pas : tuple

Un pair s'arrête à deux valeurs. Quand vous en avez besoin de trois ou plus, std::tuple est la même idée avec un nombre arbitraire. Vous en construisez un par initialisation par accolades ou avec make_tuple, et vous le lisez avec std::get<N>, où N est un indice connu à la compilation.

L'indice à l'intérieur de get<> doit être une constante connue à la compilation. get<i>(record)i est une variable d'exécution ne compilera pas : les champs d'un tuple peuvent avoir des types différents, donc le type de l'élément doit être résolu pendant la compilation, et non à l'exécution. Si vous vous surprenez à vouloir un indice d'exécution, c'est probablement un vector qu'il vous faut.

Les structured bindings fonctionnent aussi sur les tuples, et c'est la façon lisible d'en consommer un :

Renvoyer plusieurs valeurs

La raison quotidienne de recourir à ces types est de renvoyer plus d'une valeur depuis une fonction sans inventer un struct ni jongler avec des paramètres de sortie. Regroupez les résultats dans un pair ou un tuple et décomposez-les sur le site d'appel.

Pour trois résultats ou plus, renvoyez un tuple de la même manière. Il existe aussi std::tie, une astuce plus ancienne qui décompose dans des variables existantes plutôt que d'en déclarer de nouvelles, pratique lorsque vous voulez ignorer un champ avec std::ignore :

Mais savoir s'arrêter : si le même groupe de champs apparaît à plus d'un endroit, ou si vous oubliez sans cesse si .second est le score ou le compte, définissez un struct avec des membres nommés. pair et tuple sont parfaits pour des regroupements locaux et éphémères ; les champs nommés l'emportent dès que les données vivent plus longtemps qu'une seule expression.

Comparer et trier

Un bonus pratique : pair et tuple sont fournis avec des opérateurs de comparaison intégrés qui fonctionnent de façon lexicographique : ils comparent le premier élément et ne passent au suivant que lorsque les premiers sont égaux. Cela en fait des clés de tri parfaites.

Remarquez que l'ordre des champs compte : placer age en premier trie principalement par âge, puis par nom en cas d'égalité. Si vous vouliez un tri par nom d'abord, vous échangeriez l'ordre des éléments du tuple. Cette comparaison par défaut est exactement la raison pour laquelle pair<priority, item> est un idiome courant pour les files de priorité.

Suite : Les itérateurs

Vous avez maintenant vu apparaître .first, .second, it->first et *it autour des conteneurs ; ce qui relie réellement un élément pair au map dans lequel il vit est un itérateur. La page suivante décortique correctement les itérateurs : ce que begin() et end() renvoient vraiment, comment ++it parcourt un conteneur, et les pièges d'invalidation d'itérateurs qui provoquent certains des comportements indéfinis les plus pernicieux de C++.

Questions fréquentes

Quelle est la différence entre pair et tuple en C++ ?

std::pair contient exactement deux valeurs, auxquelles on accède avec .first et .second. std::tuple contient n'importe quel nombre fixe de valeurs (zéro, deux, trois ou plus), auxquelles on accède avec std::get<N>(t). Un pair est essentiellement un tuple à deux éléments avec des noms de membres plus parlants ; ne recourez à tuple que lorsque vous avez besoin de trois champs ou plus.

Comment accéder aux éléments d'un tuple en C++ ?

Utilisez std::get<N>(t) avec un indice connu à la compilation, par exemple std::get<0>(t). Depuis C++17, vous pouvez aussi le décomposer avec les structured bindings : auto [a, b, c] = t; donne à chaque élément sa propre variable nommée. Vous ne pouvez pas indexer un tuple avec une variable d'exécution : std::get<i> exige que i soit une constante.

Comment renvoyer plusieurs valeurs depuis une fonction en C++ ?

Renvoyez un std::pair ou un std::tuple et décomposez-le sur le site d'appel avec des structured bindings : auto [ok, value] = parse(text);. C'est plus propre que des paramètres de sortie et cela évite de définir un struct à usage unique, même si un struct nommé est plus lisible lorsque les champs survivent à un seul appel.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER