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 :
- Le type
std::pair,
qui représente une paire {clé,valeur}, et
- Le type
std::tuple,
qui généraliser le concept de paire pour décrire un n-uplet
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.
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 :
- L'expression p0 == p1 s'avèrera si
p0.first == p1.first && p0.second == p1.second
- L'expression p0 != p1 s'avèrera si
!(p0==p1)
- L'expression p0 < p1 s'avèrera si
p0.first < p1.first || p0.first == p1.first && p0.second < p1.second
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));
// ...
}
|
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>()
où 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.
- Aide en ligne officielle, à toutes fins pratiques :
- La technique du piecewise_construct, pour lever certaines ambiguïtés à la
construction d'un uplet :
http://en.cppreference.com/w/cpp/utility/piecewise_construct_t
- Une implémentation maison de tuple, à titre
illustratif : ../../Sources/uplet_maison.html
- Sans donner vraiment dans l'implémentation d'un uplet,
Raymond Chen
propose, avec ce texte de 2015, une stratégie
permettant en quelque sorte d'implémenter une sélective sur un uplet :
http://blogs.msdn.com/b/oldnewthing/archive/2015/03/26/10602905.aspx
- Série d'articles sur la conception d'un type tel que
std::tuple, par
Sasha Goldshtein en 2015 :
- Décomposer un std::tuple pour en passer les
éléments en paramètre à une fonction :
http://www.cppsamples.com/common-tasks/apply-tuple-to-function.html
- Quelques exemples d'utilisation d'un std::tuple,
colligés par Varun (?) en 2017 :
http://thispointer.com/c11-stdtuple-tutorial-examples/
- Quelques exemples d'utilisation de std::make_tuple(),
colligés par Varun (?) en 2017 :
http://thispointer.com/c11-make_tuple-tutorial-example/
- En 2016, Murray Cumming propose quelques
utilitaires pour faciliter la manipulation de tuple :
http://www.murrayc.com/permalink/2016/01/09/c-tuple-utils/ (cela dit, si
vous trouvez ces outils intéressants, considérez expérimenter avec
Boost.Hana, de
Louis Dionne,
qui est un petit bijou)
- En 2016, Viktor Laskin présente une
technique, qu'il attribue à Manu Sánchez, pour identifier les éléments d'un
tuple par un nom :
http://vitiy.info/named-tuple-for-cplusplus/
- Réflexion de
Louis Dionne
en 2015, à propos des mathématiques associées aux
instances vides de
std::variant et à
std::tuple :
http://ldionne.com/2015/07/14/empty-variants-and-tuples/
- Accéder aux éléments d'un std::tuple avec la
syntaxe familière de l'accès aux éléments d'un tableau, texte de Jonathan
Müller en 2017 (sur le blogue d'Arne Mertz) :
https://arne-mertz.de/2017/03/tuple-compile-time-access/ puis complément
technique sur son propre site :
http://foonathan.net/blog/2017/03/01/tuple-iterator.html
- Accéder aux éléments d'un std::tuple avec un
indice connu à l'exécution, exercice de style par Anthony
Williams en 2017 :
https://www.justsoftwaresolutions.co.uk/cplusplus/getting-tuple-elements-with-runtime-index.html
(voir aussi
https://accu.org/index.php/journals/2382)
- Textes de Nils Deppe en 2017 :
- Comme le rappelle Zhihao Yuan en 2020,
CTAD et les types
comme std::tuple font bon ménage :
https://simpleroseinc.github.io/2020/05/22/ctad-for-tuple-like-types.html
Quelques critiques :