Un conteneur associatif est un conteneur dans lequel les éléments peuvent être indexés par des objets, et non plus seulement par des entiers.
Vous allez donc ici voir les différents types de conteneurs associatifs proposés par la STL.
Pour cet exercice, vous modifierez les fichiers :
- chap-05/3-associatives/main.cpp
- chap-05/3-associatives/keys.h
La cible à compiler est c5-3-associatives.
Un set est un conteneur dans lequel les éléments sont “indexés par eux-mêmes”. Il y a deux cas d’utilisation classiques d’un set:
Dans la librairie standard, vous pouvez trouver std::set et std::unordered_set qui permettent de manipuler des sets. En analysant leur documentation, pouvez-vous identifier les différences principales entre ces deux classes ?
Dans les fichiers de l’exercice, on vous fournit deux classes ComparableDog et HashableDog. Ces deux classes sont pour le moment identique : elles contiennent deux attributs _name et _species.
Votre objectif sera d’ajouter les fonctions nécessaires pour utiliser ces classes dans des sets.
std::setPour pouvoir placer des éléments dans un set, nous avons dit qu’ils devaient être comparables. Le plus simple pour cela, c’est de créer un operator< pour la classe. Les opérateurs de comparaison peuvent être définis soit en tant que fonction-membre, soit en tant que fonction libre.
Fonction-membre :
class SomeClass
{
public:
bool operator<(const SomeClass& other) const
{
return /* whether this and other are equal */;
}
};
Fonction libre :
// SomeClass.h
class SomeClass
{
...
};
bool operator<(const SomeClass& a, const SomeClass& b);
// SomeClass.cpp
bool operator<(const SomeClass& a, const SomeClass& b)
{
return /* whether a and b are equal */;
}
Notez bien que si vous voulez utiliser une fonction libre, vous devrez peut-être exposer vos attributs dans des getters ou déclarer operator< comme une fonction amie de la classe.
Commencez par définir un set contenant des ComparableDog dans la fonction main. Essayez de compiler. Jusque là, tout devrait bien se passer.
Instanciez maintenant une variable de type ComparableDog, et ajoutez-là à votre set.
Essayez à nouveau de compiler. Vos yeux devraient désormais commencer à saigner, ce qui est normal. Je vous rassure, avec un peu d’entraînement, cela n’arrivera plus.
Du coup, essuyez vos larmes et enfilez vos lunettes si vous en avez, et tentez d’identifier ce que le compilateur essaye de vous communiquer.
Implémentez ce qu’il manque pour que votre programme compile.
Créez maintenant une nouvelle instance de ComparableDog, différente de la première, et ajoutez-là au set.
Affichez le nombre d’éléments du set pour vérifier qu’il en contient maintenant 2.
Instanciez maintenant une copie de votre premier chien, et ajoutez-là à votre set.
Combien y a-t-il d’éléments dans le set après cette opération ? Pourquoi ?
std::unordered_setVous allez maintenant faire le même exercice, mais avec un std::unordered_set plutôt qu’un set.
Commencez par définir un unordered_set contenant des HashableDog dans la fonction main et essayez de compiler. Vous devriez vous retrouver avec un paquet d’erreurs.
Comme tout à l’heure, essayez d’identifier la cause de leur déclenchement.
Afin de rendre une classe hashable, il faut spécialiser une fonction de la librairie standard que l’on appelle hash. Pour ce faire, vous devez d’abord inclure le header <functional>, puis écrire le code suivant :
namespace std {
template <>
struct hash<ClassToMakeHashable>
{
size_t operator()(const ClassToMakeHashable& c) const
{
return /* some value that can be computed from c */;
}
};
}
Ne vous posez pas trop de questions pour le moment sur la syntaxe du code ci-dessus. Vous pouvez directement le copier-coller à la fin du fichier keys.h, en remplaçant ClassToMakeHashable par le nom de votre classe.
En ce qui concerne le contenu de la fonction, vous allez devoir générer un hash en combinant les hashs des attributs de la classe HashableDog.
Pour récupérer le hash d’une string, vous devez déclarer une variable de type std::hash<std::string>, puis appeler son operator() sur la string en question. Ca donne quelque chose comme ça :
std::hash<std::string> hash_fcn;
size_t hash_of_blablabla = hash_fcn("blablabla");
Vous pouvez ensuite combiner plusieurs valeurs de hash ensemble en utilisant des opérateurs binaires (^, +, *, ou n’importe quoi d’autres, ce n’est pas vraiment important du moment que vous renvoyer bien quelque chose à la fin) :
std::hash<std::string> hash_fcn;
size_t hash_final = hash_fcn("first") ^ hash_fcn("second") ^ hash_fcn("third");
Lorsqu’on défini un operator() dans une classe, il est ensuite possible d’utiliser les instances de cette classe comme des fonctions. C’est pour cela que ce genre d’objets est appelé foncteur.
Implémentez le contenu de hash<HashableDog>::operator().
Afin de pouvoir accéder aux attributs de HashableDog depuis cette fonction, vous pouvez les déplacer dans la partie publique.
Une fois que cela compile, refactorisez votre code pour remettre les attributs dans la partie privée.
Pour cela, définissez une fonction-membre publique get_hash dans la classe HashableDog, que vous appelerez depuis hash<HashableDog>::operator().
Instanciez maintenant une variable de type HashableDog, ajoutez-là à votre set et essayez de compiler.
Comme vous pouvez le voir, aujourd’hui, le compilateur de vous apprécie pas. Déterminez à partir des erreurs de compilation ce qu’il manque dans la classe HashableDog pour que le programme compile.
Ajoutez la fonction manquante (même signature que operator<) et vérifiez ensuite que tout fonctionne.
Vous pouvez ensuite essayer d’ajouter de nouveaux éléments dans votre unordered_set. Vous devriez avoir le même comportement qu’avec la classe set : les éléments sont ajoutés seulement s’ils n’apparaissent pas déjà dans le conteneur.
Une map est un conteneur dans lequel chaque valeur est indexé par une clef. Dans la STL, vous pouvez trouver les classes std::map et std::unordered_map.
Les différences entre map et unordered_map sont exactement les mêmes que les différences entre set et unordered_set. D’ailleurs, std::[unordered_]map, c’est la même chose que std::[unordered_]set, si ce n’est que pour chaque élément du conteneur, on stocke une valeur en plus de la clef.
En particulier, vous retrouvez les mêmes contraintes sur le type des clefs :
map,unordered_map.Vous allez maintenant définir une variable de type std::map<key, value> qui stocke pour chaque chien (= clef) le nom et prénom de son propriétaire (= valeur).
Quel type pouvez-vous utiliser pour la clef ? Pour la valeur, vous pouvez utiliser une std::pair<std::string, std::string> (n’hésitez pas à créer un alias).
Pour insérer des éléments dans une map, vous pouvez utiliser plein de fonctions différentes. Consultez la documentation pour trouver le nom de ces fonctions et identifiez leurs différences (comportement / syntaxe).
Essayez d’ajouter plusieurs éléments à votre map, en utilisant chacune de ces fonctions.
Que faut-il maintenant faire si vous souhaitez utiliser une unordered_map plutôt qu’une map ?