Comprendre std::tuple

Le langage C++ offre, en plus des conteneurs capables d'entreposes des éléments d'un certain type, deux entités capables d'entreposer plusieurs éléments susceptibles d'être de types distincts :

Ce qui suit propose un bref survol de std::pair, à titre de mise en situation, puis fait un survol de std::tuple. Notez que std::pair est un type C++ « traditionnel » alors que std::tuple fait partie de la bibiothèque standard du langage depuis C++ 11. Le design de std::tuple fait en sorte que ce type soit essentiellement une généralisation de std::pair, au sens où un std::tuple de deux éléments se convertit implicitement en std::pair et vice versa.

Le type std::pair

Une instance de std::pair<K,V> représente une paire de valeurs de type K et V respectivement. Bien que plusieurs conteneurs associatifs utilisent ce type pour l'organisation des données à l'interne, std::pair<K,V> n'est pas associée à ce seul usage et est à la fois simple et polyvalent.

Sur le plan de l'implémentation, le type std::pair<K,V> est un simple enregistrement contenant deux valeurs, soit first, de type K, et second, de type V. Ces deux attributs sont publics : un std::pair<K,V> ne se veut pas tant une entité de sécurisation qu'une entité de regroupement.

À titre d'exemple, voici un petit programme lisant des articles représentés par des paires nom, quantité, puis triant les articles pour les afficher en ordre lexicographique de nom, après quoi (dans le cas de gauche du moins) les mêmes articles sont triés à nouveau pour être affichés en ordre décroissant de quantité. Le même exemple est proposé à l'aide d'un std::vector<std::pair<std::string,int>> et (de manière simplifiée) à l'aide d'une std::map<std::string,int>.

Avec un conteneur séquentiel (std::vector<std::pair<std::string,int>>) Avec un conteneur associatif (std::map<std::string,int>)
#include <iostream>
#include <vector>
#include <string>
#include <utility>
#include <algorithm>
using namespace std;
struct ordre_lexicographique_cle {
   template <class K, class V>
      bool operator()(const pair<K, V> &p0, const pair<K, V> &p1) const {
         return p0.first < p1.first;
      }
};
struct ordre_decroissant_valeur {
   template <class K, class V>
      bool operator()(const pair<K, V> &p0, const pair<K, V> &p1) const {
         return p1.second < p0.second;
      }
};
int main() {
   vector<pair<string, int>> v;
   cout << "En tout temps, entrez . comme nom de produit pour terminer\n";
   try {
      // lecture...
      cout << "Entrez un nom de produit puis <enter> : ";
      for (string s; getline(cin, s) && s != ".";) {
         cout << "Combien y a-t-il de " << s << "? ";
         int combien;
         if (cin >> combien && find_if(begin(v), end(v), [&](const pair<string, int> &p) {
            return p.first == s; 
         }) == end(v))
            v.emplace_back(s, combien);
         else
            throw runtime_error{"Pas un entier"};
         cin.clear();
         cin.ignore();
         cout << "Entrez un nom de produit puis <enter> : ";
      }
      sort(begin(v), end(v), ordre_lexicographique_cle{});
      cout << "En ordre lexicographique:\n";
      for (auto & elem : v)
         cout << '\t' << elem.first << " : " << elem.second << " unites" << endl;
      sort(begin(v), end(v), ordre_decroissant_valeur{});
      cout << "En ordre decroissant de quantite:\n";
      for (auto & elem : v)
         cout << '\t' << elem.first << " : " << elem.second << " unites" << endl;
   } catch (...) {
      cout << "Ceci n'etait pas un entier..." << endl;
   }
}
#include <iostream>
#include <map>
#include <string>
#include <utility>
#include <algorithm>
using namespace std;
int main() {
   map<string, int> m;
   cout << "En tout temps, entrez . comme nom de produit pour terminer\n";
   try {
      // lecture...
      cout << "Entrez un nom de produit puis <enter> : ";
      for (string s; getline(cin, s) && s != ".";) {
         cout << "Combien y a-t-il de " << s << "? ";
         int combien;
         if (cin >> combien)
            m[s] = combien;
         else
            throw runtime_error{"Pas un entier"};
         cin.clear();
         cin.ignore();
         cout << "Entrez un nom de produit puis <enter> : ";
      }
      cout << "En ordre lexicographique:\n";
      for (auto & elem : m)
         cout << '\t' << elem.first << " : " << elem.second << " unites" << endl;
   } catch (...) {
      cout << "Ceci n'etait pas un entier..." << endl;
   }
}

L'exemple montre évidemment à la fois comment il est possible d'utiliser une std::pair de manière explicite (à gauche) comme de manière implicite (à droite).

En pratique, la version utilisant un vecteur sera souvent la plus rapide, mais est relativement complexe. Par exemple, pour éviter d'accepter des duplicats (paires de même nom), elle fait une recherche linéaire dans le conteneur à chaque tentative d'insertion. La version reposant sur un tableau associatif est beaucoup plus simple, probablement un peu plus lente, et n'admet que l'ordonnancement sur la base des clés. Notez d'ailleurs que par défaut, une std::pair<K,V> implémente les opérateurs == et < de manière à offrir un ordonnancement lexicographique, c'est à-dire que si nous avons p0 et p1 de type std::pair<K,V>, alors :

Les autres opérateurs relationnels suivront le même modèle.

Fonction génératrice

Il est possible de construire une std::pair<K,V> à l'aide d'une fonction génératrice, std::make_pair<K,V>(), ce qui peut alléger singulièrement l'écriture dans certains cas.

Par exemple, à droite, les deux exemples font précisément la même chose, mais la version la plus à droite, grâce à la fonction génératrice, et à la fois plus claire et plus concise.

Construction « manuelle » Construction par fonction génératrice
// ...
int main() {
   vector<int> v { 2, 3, 5, 7, 11 };
   auto p = pair<vector<int>::iterator,vector<int>::iterator>{ begin(v), end(v) };
   // ...
}
// ...
int main() {
   vector<int> v { 2, 3, 5, 7, 11 };
   auto p = make_pair(begin(v), end(v));
   // ...
}

Le type std::tuple

Tel qu'indiqué plus haut, le type std::tuple est une généralisation de std::pair pour un nombre arbitrairement grand (mais non-nul) d'éléments.

Opérations de base

Construire un std::tuple par défaut sollicite la construction par défaut de ses éléments.

tuple<int,string,float> u; // 0, string{}, 0.0f

Comparer des std::tuple de même type résulte en une comparaison lexicographique, respectant l'ordre des types impliqués.

tuple<int,string,float> u0{3,string("Yo"), 3.14159f},
                        u1{3,string("Zo"), -4.5f};
assert(u0 < u1); // dû au 2e élément

Le nombre d'éléments d'un std::tuple peut être obtenu par la métafonction std::tuple_size.

tuple<int,string,float> u;
static_assert(tuple_size<decltype(u)>::value == 3);

L'accès au type d'un élément peut se faire par la métafonction std::tuple_element.

tuple<int,string,float> u{3,string("Yo"), 3.14159f};
static_assert(is_same_v<tuple_element<1,decltype(u)>::type,string>);

L'accès à la valeur d'un élément d'un std::tuple peut être obtenu par la fonction std::get<I>() I est une constante entière statique.

tuple<int,string,float> u{3,string("Yo"), 3.14159f};
assert(get<1>(u)=="Yo");

La concaténation d'instances de std::tuple peut être réalisée à l'aide de std::tuple_cat.

tuple<int,string,float> u0{3,string("Yo"), 3.14159f};
tuple<int,double,int> u1{3,3.5,3};
auto concat = tuple_cat(u0, u1);
static_assert(tuple_size<decltype(concat)>::value==6);

Un std::tuple n'est pas un conteneur. On ne peut donc pas itérer à travers ses éléments avec une répétitive classique. Cependant, il est possible d'exprimer une « itération » à travers les éléments d'un std::tuple par métaprogrammation.

Sélection de valeurs – fonction std::tie()

Pour extraire certaines valeurs d'un std::tuple, il est possible d'utiliser la fonction std::tie() :

tuple<int, string, float> u { 3, "J'aime mon prof", 3.14159 };
int n;
float f;
tie(n, ignore, f) = u;
cout << n << ' ' << f << endl; // affichera 3 3.14159

Le recours à std::ignore sur un ou plusieurs éléments d'un std::tuple dans un appel à std::tie() est un signal que ces éléments doivent être escamotés.

Depuis C++ 17, il est aussi possible d'utiliser des Structured Bindings avec divers agrégats, incluant des std::tuple, ce qui permet de remplacer ceci :

#include <tuple>
#include <string>
using namespace std;
auto f() {
   return make_tuple(3, 3.14159, "J'aime mon prof"s); // int, double, string
}
int main() {
   int n;
   double x;
   string s;
   tie(n, x, s) = f();
   // ...
}

... par cela :

#include <tuple>
#include <string>
using namespace std;
auto f() {
   return make_tuple(3, 3.14159, "J'aime mon prof"s); // int, double, string
}
int main() {
   auto [n, x, s] = f();
   // ...
}

Voir ../Divers--cplusplus/affectation_destructurante.html pour en savoir plus.

Fonction génératrice

Il est possible de construire une std::tuple à l'aide d'une fonction génératrice, std::make_tuple(), ce qui peut alléger singulièrement l'écriture dans certains cas.

Par exemple, à droite, les deux exemples font précisément la même chose, mais la version la plus à droite, grâce à la fonction génératrice, et à la fois plus claire et plus concise.

Construction « manuelle » Construction par fonction génératrice
// ...
int main() {
   vector<int> v = { 2, 3, 5, 7, 11 };
   auto u = tuple<
      vector<int>::size_type,
      vector<int>::iterator,
      vector<int>::iterator
   >{ v.size(), begin(v), end(v) };
   // ...
}
// ...
int main() {
   vector<int> v = { 2, 3, 5, 7, 11 };
   auto u = make_tuple(v.size(), begin(v), end(v));
   // ...
}

Lectures complémentaires

Quelques liens pour enrichir le propos.

Quelques critiques :


Valid XHTML 1.0 Transitional

CSS Valide !