Concevoir un conteneur Exercice Les conteneurs standards tels que std::vector respectent un ensemble de contraintes, que ce soit du point de vue des opérations, des noms ou de la rigueur. Pour nous familiariser avec ce style de programmation et pour raffiner nos propres techniques, nous allons entreprendre la conception et le peaufinage d un conteneur conforme au standard. Votre mandat sera d écrire votre propre conteneur générique liste_double qui représentera une liste doublement chaînée de quelque chose. Parce que nous souhaiterons que votre conteneur soit aussi conforme que possible aux normes et standards en vigueur dans le monde STL, nous aurons une longue liste d exigences à rencontrer. Vous remarquerez sans peine que la grande majorité des opérations à implémenter seront simples et courtes (et rapides!); il s agit d une caractéristique de ce type de programmation très rigoureuse. Considérations générales La qualification const doit être appliquée chaque fois que cela s avérera possible. Il en va de même pour la qualification throw() (au sens de No-Throw). La documentation de chaque type générique et de chaque opération générique doit inclure le contrat applicable aux types sur la base desquels la généricité s opère. L encapsulation devra être bien en place. Ne devraient être publics que les membres qui devraient l être. Il n y a aucune raison d avoir des membres protégés dans un conteneur standard (une telle classe devrait être terminale). Vous pourrez utiliser des types internes pour réaliser votre travail. En particulier, il est hautement probable que le type liste_double<t>::iterator soit un type à part entière plutôt qu un simple alias sur un pointeur à travers typedef comme dans le cas de la classe Tableau rédigée plus tôt cette session. La documentation de chaque méthode doit inclure sa complexité algorithmique, ses préconditions, ses postconditions et toute note explicative pertinente. Par exemple, si une méthode a recours à de l allocation dynamique de mémoire ou si son comportement est indéterminé si on lui passe un paramètre ne respectant pas certains critères, alors il faut document cet état de fait. Préparé par Patrice Roy pour le Collège Lionel-Groulx Page 1
Le programme de test suivant 1 devra fonctionner correctement : #include "liste_double.h" #include <iostream> #include <algorithm> #include <iterator> template <class C> void afficher(const C &conteneur, std::ostream &os) { typedef typename C::value_type value_type; os << conteneur.size() << " éléments: "; std::copy(conteneur.begin(), conteneur.end(), std::ostream_iterator<value_type>(os, " ")); os << std::endl; } template <class C> void test_find (const C &conteneur, const typename C::value_type &elem, std::ostream &os) { typename C::const_iterator itt = std::find(conteneur.begin(), conteneur.end(), elem); if (itt!= conteneur.end()) os << "Trouvé " << elem << " à la position " << std::distance(conteneur.begin(), itt) << std::endl; } int main() { using std::advance; using std::copy; using std::cin; using std::cout; using std::endl; using std::front_inserter; using std::back_inserter; liste_double<int> lst; int source[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; copy(source, source+10, front_inserter(lst)); test_find(lst, 2, cout); lst.clear(); copy(source, source+10, back_inserter(lst)); test_find(lst, 2, cout); int vals[] = { 1, 2, 3, 4, 5 }; 1 dont vous trouverez une copie sur le site Web du cours. Ne retranscrivez pas tout ça manuellement! Préparé par Patrice Roy pour le Collège Lionel-Groulx Page 2
Note : } lst = liste_double<int>(vals, vals+ 5); liste_double<int>::iterator itta = lst.begin(); liste_double<int>::iterator ittb = itta; itta++; advance(ittb, 3); ++ittb; ++ittb; ++ittb; lst.erase(itta, ittb); lst.insert(lst.begin(), 18); itta = lst.begin(); ++itta; lst.insert(itta, liste_double<int>::size_type(3), -2); itta++; lst.insert(itta, vals, vals+ 5); cout << "... pressez une touche puis <return>..."; char c; cin >> c; vous trouverez aussi sur le site Web du cours un exécutable opérationnel pour voir les résultats attendus et un programme pour comparer les performances de votre conteneur avec ceux des autres. Préparé par Patrice Roy pour le Collège Lionel-Groulx Page 3
Attentes Voici les attentes spécifiques envers votre conteneur. Vous remarquerez que la liste est longue, mais c est normal. Ces attentes s ajoutent à celles listées dans la rubrique Considérations générales ci-dessus. Une instance de liste_double<t> devra définir les types internes et publics suivants, en accord avec les usages des conteneurs standards : le type liste_double<t>::size_type; le type liste_double<t>::value_type; le type liste_double<t>::reference; le type liste_double<t>::const_reference; le type liste_double<t>::pointer; et le type liste_double<t>::const_pointer. Le type liste_double<t>::iterator sera une classe publique de votre cru. Une instance de cette classe offrira au minimum les opérations suivantes : passer à l élément suivant à l aide de l opérateur ++ (dans ses deux déclinaisons); passer à l élément précédent à l aide de l opérateur -- (dans ses deux déclinaisons); offrir les opérateurs == et!= pour comparer entre eux deux itérateurs d une même séquence. On dira de deux itérateurs qu ils sont égaux s ils mènent au même élément; (l opérateur *); aux membres (l opérateur ->); et, au besoin les constructeurs, le destructeur et l affectation. Personnellement, je vous recommande de créer d abord une classe privée Noeud représentant correctement un nœud d une liste doublement chaînée de type T, ce qui fera en sorte de régler avec élégance un problème simple, puis d écrire une classe publique iterator qui permettra de passer d un Noeud à l autre. Votre code sera plus simple à rédiger ainsi. Cela dit, il s agit d un détail d implémentation et vous n avez pas à appliquer cette suggestion si vous ne le souhaitez pas. accéder à l élément vers lequel mène l itérateur à l aide de l opérateur de déréférencement invoquer une méthode de l élément vers lequel mène l itérateur à l aide de l opérateur d accès Le type liste_double<t>::const_iterator sera l équivalent constant du type liste_double<t>::iterator. Le type liste_double<t> définira les méthodes begin() et end() dans leurs déclinaisons constantes et non constantes. Le type liste_double<t> définira les méthodes size() et empty(). Le type liste_double<t> définira les méthodes front() et back() qui retourneront respectivement une référence sur le premier et sur le dernier élément de la liste. Ces méthodes seront offertes en déclinaisons constante et non constante. Préparé par Patrice Roy pour le Collège Lionel-Groulx Page 4
Le type liste_double<t> offrira les constructeurs suivants. Dans chaque cas, à vous de déterminer comment répondre aux attentes (p. ex. : de définir ce que signifie liste vide) : un constructeur par défaut, qui construira une liste_double<t> vide; un constructeur par copie; un constructeur de conversion; et un constructeur de séquence. Le type liste_double<t> offrira un destructeur assurant le bon nettoyage du système lors de la fin de la vie d une instance de ce type. Le type liste_double<t> offrira les méthodes d insertion suivantes : une méthode push_back(), qui ajoutera un élément en fin de séquence; une méthode push_front(), qui ajoutera un élément en début de séquence; une méthode insert() prenant en paramètre un itérateur et une valeur, et insérant la valeur à la position juste avant l itérateur dans la séquence. Cette méthode retournera un itérateur sur l élément tout juste inséré; une méthode insert() prenant en paramètre un itérateur, un entier n et une valeur, et insérant n instances de la valeur à la position juste avant l itérateur dans la séquence. Cette méthode ne retournera rien; et une méthode insert() prenant en paramètre un itérateur et une séquence déterminée par une paire d itérateurs, et insérant une copie des éléments de la séquence en question à la position juste avant l itérateur dans la séquence tout en conservant l ordre des éléments dans cette séquence. Cette méthode ne retournera rien. Le type liste_double<t> offrira les méthodes de suppression suivantes : une méthode clear(), qui videra la liste; une méthode erase() prenant en paramètre un itérateur et supprimant de la séquence l élément vers lequel mène cet itérateur; et une méthode erase() prenant en paramètre une séquence déterminée par une paire d itérateurs et supprimant cette séquence de la liste. Le type liste_double<t> définira la méthode swap() permettant d échanger le contenu de l instance active avec celui d une autre instance de liste_double<t>. Une spécialisation de l algorithme standard std::swap() sera aussi offert pour tirer profit de cette méthode. Le type liste_double<t> définira un opérateur conventionnel d affectation et un opérateur d affectation de type apparenté. Des points bonis seront ajoutés si liste_double<t> supporte correctement la sémantique de mouvement. Préparé par Patrice Roy pour le Collège Lionel-Groulx Page 5
Organisation du travail Vous aurez deux semaines pour faire ce travail. Cela implique de votre part une lecture attentive des consignes et une réflexion quant aux meilleures manières de procéder dans chaque cas. Vous pourrez comparer vos résultats dans les tests avec ceux de vos collègues et avec les résultats affichés par les démonstrateurs livrés sur le site. Exceptionnellement, vous pourrez travailler en équipes de deux personnes. Ceci vous permettra de discuter stratégie, ce qui sera fécond. Une manière dommageable d utiliser cette opportunité serait de laisser un membre de l équipe tout faire alors que l autre observe; soyez plus vives et plus vifs d esprit que cela et travaillez vraiment en équipe. Vous livrerez une version imprimée de votre classe, pleinement testée et documentée. Soyez rigoureuses et rigoureux dans votre démarche. Préparé par Patrice Roy pour le Collège Lionel-Groulx Page 6