FAQ C++Consultez toutes les FAQ
Nombre d'auteurs : 34, nombre de questions : 368, dernière mise à jour : 14 novembre 2021 Ajouter une question
Cette FAQ a été réalisée à partir des questions fréquemment posées sur les forums de http://www.developpez.com et de l'expérience personnelle des auteurs.
Je tiens à souligner que cette FAQ ne garantit en aucun cas que les informations qu'elle propose sont correctes ; les auteurs font le maximum, mais l'erreur est humaine. Cette FAQ ne prétend pas non plus être complète. Si vous trouvez une erreur ou si vous souhaitez devenir rédacteur, lisez ceci.
Sur ce, nous vous souhaitons une bonne lecture.
- Quels sont les enjeux associés aux techniques Orientées Objets ?
- Qu'est-ce qu'un objet ?
- Qu'est-ce que l'héritage ?
- Qu'est-ce que l'encapsulation ?
- L'encapsulation constitue-t-elle un mécanisme de sécurité ?
- Comment le C++ permet-il d'améliorer le compromis entre fiabilité et simplicité d'utilisation ?
- Comment savoir si je dois dériver une classe ou l'encapsuler ?
- Qu'est-ce qu'une bonne interface ?
- Que sont les accesseurs / mutateurs ?
- Quand et comment faut-il utiliser des accesseurs / mutateurs ?
- La conception d'une classe doit-elle se faire plutôt par l'extérieur ou par l'intérieur ?
- Qu'est-ce que le polymorphisme ?
- Qu'est-ce que la coercition ?
- Qu'est-ce que le polymorphisme paramétrique ?
- Qu'est-ce que le polymorphisme d'inclusion ?
- Qu'est-ce qui est entendu par « paramétrer un comportement » ?
- Comment varier le comportement au moment de l'exécution par le polymorphisme d'inclusion ?
- Comment varier le comportement au moment de l'écriture de code (template) ?
- Comment varier le comportement à la compilation par les directives du préprocesseur ?
- Comment varier le comportement à l'édition des liens ?
- Comment varier le comportement à l'exécution par le chargement dynamique de bibliothèque ?
- Comment varier le comportement au moment de l'exécution par agrégation ?
- Comment choisir entre les différents types de paramétrage de comportement ?
Les techniques OO sont la meilleure façon connue de développer de grosses applications ou des systèmes complexes.
L'industrie du logiciel n'arrive pas à satisfaire les demandes pour des systèmes logiciels aussi imposants que complexes, mais cet échec est dû à nos succès : nos réussites ont habitué les utilisateurs à toujours en demander plus. Malheureusement, nous avons ainsi créé une demande du marché que les techniques 'classiques' de programmation ne pouvaient satisfaire. Cela nous a obligé à créer un meilleur paradigme.
Le C++ permet de programmer OO, mais il peut aussi être utilisé comme un langage classique (« un C amélioré »). Si vous comptez l'utiliser de cette façon, n'espérez pas profiter des bénéfices apportés par la programmation OO.
Une zone de stockage avec une sémantique associée.
Après la déclaration suivante,
Code c++ : | Sélectionner tout |
int i;
L'héritage consiste à construire une classe (appelée classe fille) par spécialisation d'une autre classe (classe mère). On peut illustrer ce principe en prenant l'exemple des mammifères (classe mère) et l'homme d'un côté (classe fille1) et les chiens (classe fille2). En effet, les chiens et les hommes sont tous deux des mammifères mais ont des spécificités.
Il s'agit d'éviter des accès non autorisés à certaines informations et/ou fonctionnalités.
L'idée clé est de séparer la partie volatile de la partie stable. L'encapsulation permet de dresser un mur autour d'une partie du code, ce qui permet d'empêcher une autre partie d'accéder à cette partie dite volatile ; les autres parties du code ne peuvent accéder qu'à la partie stable. Cela évite que le reste du code ne fonctionne plus correctement lorsque le code volatile est changé. Dans le cadre de la programmation objet, ces parties de code sont normalement une classe ou un petit groupe de classe.
Les « parties volatiles » sont les détails d'implémentation. Si le morceau de code est une seule classe, la partie volatile est habituellement encapsulée en utilisant les mots-clés private et protected. S'il s'agit d'un petit groupe de classe, l'encapsulation peut être utilisée pour interdire à des classes entières de ce groupe. L'héritage peut aussi être utilisé comme une forme d'encapsulation.
Les parties stables sont les interfaces. Une bonne interface procure une vue simplifiée exprimée dans le vocabulaire de l'utilisateur, et est créée dans l'optique du client. (un utilisateur, dans le cas présent, signifie un autre développeur, non pas le client qui achètera l'application). Si le morceau de code est une classe unique, l'interface est simplement l'ensemble de ses membres publics et des fonctions amies. S'il s'agit d'un groupe de classes, l'interface peut inclure un certain nombre de classes.
Concevoir une interface propre et séparer cette interface de son implémentation permet aux utilisateurs de l'utiliser convenablement. Mais encapsuler (mettre dans une capsule) l'implémentation force l'utilisateur à utiliser l'interface.
Non.
L'encapsulation ne constitue pas un mécanisme de sécurité. Il s'agit d'une protection contre les erreurs, pas contre l'espionnage.
En généralisant le concept d'encapsulation.
Chaque classe ne propose à son utilisateur qu'un nombre minimal de fonctions publiques très spécifiques et dont le comportement est clairement déterminé. Chaque fonction publique fournie par une classe correspond à un service que l'on attend d'elle.
Ces fonctions définissent ce que l'on appelle l'interface de la classe en question.
Le résultat final est comme une « structure encapsulée ». Cela améliore le compromis entre fiabilité (dissimulation de l'information) et facilité d'utilisation (les instances multiples).
J'applique une méthode simple : la question à se poser est la suivante : est-ce que X est un genre de Y, ou est-ce que X utilise un Y ?
Si la réponse est X est un genre de Y, il s'agit d'un cas où je dérive une classe.
Si la réponse est X utilise Y, il s'agit d'un cas où je vais encapsuler une classe.
Quand elle présente une vue simplifiée d'un bout de logiciel, et est exprimée dans les termes de l'utilisateur (le bout de logiciel correspond habituellement à une classe ou un petit groupe de classes et l'utilisateur est un autre développeur, non le client final).
« Vue simplifiée » signifie que les détails sont intentionnellement cachés. Cela réduit donc le risque d'erreur lors de l'utilisation de la classe.
« Vocabulaire de l'utilisateur » veut dire que l'utilisateur n'a pas besoin d'apprendre de nouveaux mots ou concepts. Cela réduit donc la courbe d'apprentissage de l'utilisateur.
Un accesseur (accessor en anglais) est une fonction membre renvoyant la valeur d'une propriété d'un objet. Un mutateur (mutator en anglais) ou encore modifieur (modifier en anglais) est une fonction membre qui modifie la valeur d'une propriété d'un objet.
L'utilisation d'accesseurs / mutateurs permet de masquer l'implémentation des données de la classe (encapsulation) et de faire évoluer celle-ci sans contraintes pour l'utilisateur final. Si ce dernier est obligé de passer par des accesseurs / mutateurs au lieu d'accéder directement aux données internes, ces dernières peuvent être changées à tout moment et il suffit alors d'adapter le code des accesseurs / mutateurs. Le code qui utilisait l'ancienne classe peut utiliser la nouvelle sans s'apercevoir des changements effectués, alors qu'un accès direct aux données internes aurait nécessité de tout reprendre.
Les accesseurs / mutateurs permettent donc de séparer l'utilisation des données de leur implémentation, en plus de pouvoir effectuer des traitements ou des contrôles annexes lors de l'assignation des membres.
Dans l'exemple suivant :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Person { public: // accesseur : renvoie le nom const std::string & GetName() const // notez le const { return name; } // mutateur : change le nom void SetName( const std::string & NewName ) { name = NewName; } private: std::string name; // nom de la personne }; |
Comme le montre cet exemple, il est courant de préfixer le nom des accesseurs / mutateurs respectivement par Get / Set. Pour cette raison, on appelle aussi les accesseurs / mutateurs des getter / setter.
Les accesseurs ne modifiant pas l'objet mais se contentant de fournir un accès (d'où leur nom) en lecture seule sur une de ses propriétés, c'est une bonne pratique que de rendre une telle fonction membre constante comme cela est le cas ici pour GetName (lire à ce sujet Pourquoi certaines fonctions membres possèdent le mot clé const après leur nom ?.
Un point important est que les accesseurs / mutateurs ne s'appliquent pas forcément sur des données membres existantes d'une classe, mais peuvent être utilisés pour simuler l'existence d'une propriété qui n'est pas directement stockées en interne dans la classe. Lire à ce sujet Quand et comment faut-il utiliser des accesseurs / mutateurs ?.
Parmi les fonctions publiques d'une classe, certaines miment la présence d'une donnée membre. On nomme aussi de telles fonctions des accesseurs. Il n'y a pas forcément de relation un-pour-un entre un accesseur et une donnée membre (comme cela est le cas pour l'accesseur GetName et la variable name dans l'exemple de la question Que sont les accesseurs / mutateurs ?. Une donnée encapsulée ne doit pas forcement être exposée via à un accesseur. L'état interne d'un objet est… interne, et doit le rester.
Il faut distinguer deux choses lorsque l'on écrit une classe : son interface et son implémentation. Le but des accesseurs / mutateurs est d'effectuer le lien entre les deux, lien qui n'a pas à être direct. L'interface, qui sera visible du reste du monde et qui est donc la première chose à déterminer quand on écrit une classe, expose un certain nombre de propriétés, qui peuvent ou non être directement stockées dans la classe. Ce dernier point est un détail d'implémentation qui n'a pas à être connu, et c'est le rôle des accesseurs / mutateurs de le masquer.
Prenons l'exemple d'une classe qui permet de connaître l'âge d'un individu :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | #include "date.h" // classe permettant de stocker une date (pour l'exemple) class Person { public: // âge de la personne int GetAge() const; private: // date de naissance Date date_of_birth; }; |
Cet exemple illustre bien le fait qu'un accesseur exporte une propriété qui n'a nullement l'obligation d'exister de manière explicite dans la classe. De même, une variable membre ne doit pas forcément être exportée via un accesseur, comme dans cet exemple avec la date de naissance.
Un autre exemple typique est celui de la classe Temperature qui permet de manipuler des températures en degrés Celsius ou Fahrenheit :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class Temperature { public: // degrés Celsius double GetCelsius() const { return this->temp_celsius; } void SetCelsius( double NewTemp ) { this->temp_celsius = NewTemp; } // degrés Fahrenheit double GetFahrenheit() const { return ( ( this->temp_celsius * 9.0 ) / 5.0 ) + 32.0; } void SetFahrenheit( double NewTemp ) { this->temp_celsius = ( NewTemp - 32.0 ) * 5.0 / 9.0; } private: // en interne, on stocke en degrés Celsius double temp_celsius; }; |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class Temperature { public: // degrés Celsius double GetCelsius() const { return ( this->temp_fahrenheit - 32.0 ) * 5.0 / 9.0; } void SetCelsius( double NewTemp ) { this->temp_fahrenheit = ( ( NewTemp * 9.0 ) / 5.0 ) + 32.0; } // degrés Fahrenheit double GetFahrenheit() const { return this->temp_fahrenheit; } void SetFahrenheit( double NewTemp ) { this->temp_fahrenheit = NewTemp; } private: // en interne, on stocke en degrés Fahrenheit double temp_fahrenheit; }; |
Vous l'aurez compris : le choix de définir des accesseurs / mutateurs doit être en accord avec la conception et l'analyse du problème. Il ne faut pas systématiser leur définition pour toutes les données membres d'une classe.
Par l'extérieur !
Une bonne interface fournit une vue simplifiée exprimée dans le vocabulaire de l'utilisateur. Dans le cas de la programmation par objets, une interface est généralement représentée par une classe unique ou par un groupe de classes très proches.
Réfléchissez d'abord à ce qu'un objet de la classe est du point de vue logique, plutôt que de réfléchir à la façon dont vous allez le représenter physiquement. Imaginez par exemple que vous ayez une classe Stack (une pile) et que vous vouliez que son implémentation utilise une LinkedList (une liste chaînée)
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 | class Stack { public: // ... private: LinkedList list_; }; |
Voyons maintenant un cas un peu plus subtil. Supposez que l'implémentation de la classe LinkedList soit basée sur une liste chaînée d'objets Node (nouds), et que chaque Node ait un pointeur sur le Node suivant :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | class Node { /*...*/ }; class LinkedList { public: // ... private: Node* first_; }; |
Une réponse parmi d'autres : une LinkedList n'est pas une chaîne d'objets Nodes. C'est peut-être bien comme ça qu'elle est implémentée, mais ce n'est pas ce qu'elle est. Ce qu'elle est, c'est une suite d'éléments. L'abstraction LinkedList doit donc être fournie avec une classe « LinkedListIterator », et c'est cette classe « LinkedListIterator » qui doit disposer d'un operator++ permettant de passer à l'élément suivant, ainsi que de fonctions get()/set() donnant accès à la valeur stockée dans un Node (la valeur stockée dans un Node est sous l'unique responsabilité de l'utilisateur de la LinkedList, c'est pourquoi il faut des fonctions get()/set() permettant à cet utilisateur de la manipuler comme il l'entend).
Toujours du point de vue de l'utilisateur, il pourrait être souhaitable que la classe LinkedList offre un moyen d'accéder à ses éléments qui mimique la façon dont on accède aux éléments d'un tableau en utilisant l'arithmétique des pointeurs :
Code c++ : | Sélectionner tout |
1 2 3 4 5 | void userCode(LinkedList& a) { for (LinkedListIterator p = a.begin(); p != a.end(); ++p) cout << *p << '\n'; } |
Le code se trouve ci-dessous. L'idée centrale est que la classe LinkedList n'a pas de fonction donnant accès aux Nodes. Les Nodes sont une technique d'implémentation, technique qui est complètement masquée. Les internes de la classe LinkedList pourraient tout à fait être remplacés par une liste doublement chaînée, ou même par un tableau, avec pour seule différence une modification au niveau de la performance des fonctions prepend(elem) et append(elem).
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #include <cassert> // Succédané de gestion d'exceptions class LinkedListIterator; class LinkedList; class Node { // Pas de membres public, c'est une « classe privée » friend LinkedListIterator; // Une classe amie friend LinkedList; Node* next_; int elem_; }; class LinkedListIterator { public: bool operator== (LinkedListIterator i) const; bool operator!= (LinkedListIterator i) const; void operator++ (); // Aller à l'élément suivant int& operator* (); // Accéder à l'élément courant private: LinkedListIterator(Node* p); Node* p_; }; class LinkedList { public: void append(int elem); // Ajoute elem après le dernier élément void prepend(int elem); // Ajoute elem avant le premier élément // ... LinkedListIterator begin(); LinkedListIterator end(); // ... private: Node* first_; }; |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | inline bool LinkedListIterator::operator== (LinkedListIterator i) const { return p_ == i.p_; } inline bool LinkedListIterator::operator!= (LinkedListIterator i) const { return p_ != i.p_; } inline void LinkedListIterator::operator++() { assert(p_ != NULL); // ou bien if (p_==NULL) throw ... p_ = p_->next_; } inline int& LinkedListIterator::operator*() { assert(p_ != NULL); // ou bien if (p_==NULL) throw ... return p_->elem_; } inline LinkedListIterator::LinkedListIterator(Node* p) : p_(p) { } inline LinkedListIterator LinkedList::begin() { return first_; } inline LinkedListIterator LinkedList::end() { return NULL; } |
Ainsi, les seules fonctions get()/set() présentes sont là pour permettre la modification des éléments de la liste chaînée, mais ne permettent absolument pas la modification des données d'implémentation de la liste. Et la liste chaînée ayant complètement masqué son implémentation, elle peut donner des garanties très fortes concernant cette implémentation (dans le cas d'une liste doublement chaînée par exemple, la garantie pourrait être qu'il y a pour chaque pointeur avant, un pointeur arrière dans le Node suivant).
Nous avons donc vu un exemple dans lequel les valeurs de certaines des données d'une classe étaient sous la responsabilité des utilisateurs de la classe (et la classe a besoin d'exposer des fonctions get()/set() pour ces données) mais dans lequel les données contrôlées uniquement par la classe ne sont pas nécessairement accessibles par des fonctions get()/set().
Note : le but de cet exemple n'était pas de vous montrer comment écrire une classe de liste chaînée. Et d'abord, vous ne devriez pas « pondre » votre propre classe liste, vous devriez plutôt utiliser l'une des classes de type « conteneur standard » fournie avec votre compilateur. La meilleure solution est d'utiliser l'une des classes conteneurs du standard C++, par exemple la classe template list<T>.
Le polymorphisme, c'est la capacité d'une expression à être valide quand les valeurs présentes ont des types différents. On trouve différents types de polymorphismes :
- ad-hoc : surcharge et coercition ;
- universel (ou non ad-hoc) : paramétrique et d'inclusion.
Types et polymorphisme, de Jean-Marc Bourguet
Connaissance des langages de programmation, de Ph. Narbel (Cours de MASTER 1 BORDEAUX 1 [2006-2007])
Qu'est-ce que la coercition ?
Qu'est-ce que la surcharge ?
Qu'est-ce que le polymorphisme paramétrique ?
Qu'est-ce que le polymorphisme d'inclusion ?
Comme Mr Jourdain écrivait de la prose sans le savoir, vous avez certainement déjà utilisé la coercition sans le savoir. Derrière cette expression se cache tout simplement les mécanismes de conversion implicite :
Code c++ : | Sélectionner tout |
1 2 3 | int op1(1); double op2(2.1); double result = op1 + op2; // polymorphisme de coercition |
Une classe s'appuie sur la définition d'opérateur de conversion pour pouvoir être utilisée dans ce type de polymorphisme :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <iostream> class CMyClass { public: operator bool()const { return true; } }; int main() { CMyClass a; std::cout << std::boolalpha << a << std::endl; return 0; } |
Le polymorphisme paramétrique passe par l'utilisation des techniques génériques pour offrir un même service pour tout un ensemble de types :
Code c++ : | Sélectionner tout |
1 2 3 4 5 | template<class T> void dump(T var) { std::cout << Timestamp() << " : " << var << std::endl; } |
On parle parfois de polymorphisme contraint ou borné lorsqu'il s'agit d'introduire des contraintes sur les types avec lesquels une fonction ou une classe générique peut effectivement être instanciée. Cela est possible avec le C++ en combinant les classes traits et des bibliothèques comme std::enable_if ou static_assert. La notion de concept a pour but d'étendre cette notion de contraintes. Un TS (technical specification, une sorte de version beta d'un standard) sur ce sujet a même été accepté.
Souvent résumé tout simplement (et trop hativement) à « polymorphisme », le polymorphisme d'inclusion s'appuie sur l'héritage public :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void function(IInterface const &var_) { var_.Action(); } class IInterface { // ... }; class CConcrete : public IInterface { // ... }; int main() { CConcrete c; Fonction(c); return 0; } |
Le polymorphisme d'inclusion doit faire sens avec l'héritage public. Il ne doit pas être utilisé uniquement pour bénéficier d'une surcharge.
Il s'agit simplement en fait d'introduire un point de variabilité dans votre code, de faire en sorte que selon <on ne sait pas trop quoi>, le comportement de ce morceau de code soit différent.
Le comportement d'un code C++ peut être paramétré de différentes façons :
- pendant l'écriture du code : les templates ;
- à la compilation : surcharges, conversions implicites et directives de compilation ;
- à l'édition des liens ;
- à l'exécution par le chargement dynamique de bibliothèque ;
- à l'exécution par le polymorphisme d'inclusion (fonctions virtuelles).
Multi-Paradigm Design for C++, de James O. Coplien
Comment varier le comportement au moment de l'exécution par le polymorphisme d'inclusion ?
Comment varier le comportement au moment de l'écriture de code (template) ?
Comment varier le comportement à la compilation par les directives du préprocesseur ?
Comment varier le comportement à l'édition des liens ?
Comment varier le comportement à l'exécution par le chargement dynamique de bibliothèque ?
Comment varier le comportement au moment de l'exécution par agrégation ?
Comment choisir entre les différents types de paramétrage de comportement ?
En C++, on utilise souvent l'héritage pour ce faire. En effet, imaginez que nous soyons en présence d'une hiérarchie de composants graphiques, dont la classe de base serait Widget. On aurait ainsi Button et Textfield qui hériteraient de Widget par exemple. Enfin, chacun possèderait une méthode show() qui permet d'afficher le composant en question. Bien entendu, un Button et un Textfield étant de natures différentes, leur affichage le serait aussi.
C'est grâce au polymorphisme d'héritage, mis en ouvre en C++ grâce au mot clé virtual, que l'on peut réaliser cela dynamiquement : à l'exécution du programme, il sera choisi d'utiliser la méthode Button::show() ou la méthode Textfield::show() selon le type réel de l'objet sur lequel on appelle show(). Voici un exemple minimal illustrant cela.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class Widget { public: virtual ~Widget() { /* ... */ } void show() { // ... do_show(); // ... } // ... private : virtual void do_show()=0; // fonction virtuelle pure }; class Button : public Widget { private : virtual void do_show() { std::cout << "Button" << std::endl; } // ... }; class Textfield : public Widget { private : virtual void do_show() { std::cout << "Textfield" << std::endl; } // ... }; void show_widget(Widget& w) { w.show(); } // ... Button b; Textfield t; show_widget(b); // affiche "Button" show_widget(t); // affiche "Textfield" |
Imaginez que vous ayez conçu une classe qui encapsule un calcul très lourd, au point que vous ayez mis sur pieds deux implémentations, l'une monothreadée, l'autre multithreadée. Il serait dommage de les faire hériter d'une classe abstraite et d'en hériter pour chacune des versions, induisant un coût à cause de la virtualité, qui est ici superflue. Vous avez une possibilité qui vous permettra de tout gérer à la compilation, en utilisant les templates. Nous allons illustrer avec une fonction qui mesure le temps pris par le calcul.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | template <class Computation> void do_something() { std::time_t start = time(NULL); Computation::compute(); std::time_t end = time(NULL); std::cout << end - start << " seconds." << std::endl; } struct SingleThreadedComputation { static void compute() { // implémentation monothread } }; struct MultiThreadedComputation { static void compute() { // implémentation multithread } }; // par exemple : #ifdef STCOMPUTATION do_something<SingleThreadedComputation>(); #elif defined MTCOMPUTATION do_something<MultiThreadedComputation>(); #endif // comportement que l'on peut choisir soit avec un #define, // soit avec l'option de compilation -DSTCOMPUTATION ou -DMTCOMPUTATION |
Cette façon de faire s'approche de ce que l'on appelle le Policy Based Design, qui permet de paramétrer de manière très flexible le comportement, dès la compilation, avec une utilisation intelligente des templates.
C'est une sorte d'équivalent du Design Pattern Strategy, à la sauce C++ et templates.
Ensuite vient le polymorphisme issu de la manipulation du préprocesseur de votre compilateur. En effet, en jouant avec les #ifdef, nous pouvons par exemple sélectionner un certain code ou un autre selon des directives de compilation, qui permettent de modifier le comportement de l'application générée, et ce au moment de compiler. Cela se base sur le schéma basique suivant (qui a été utilisé pour l'exemple de calcul mono|multithread) :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | #ifdef OPTION1 // code 1 #elif defined OPTION2 // code 2 #elif defined OPTION3 // code 3 #else // code 4 #endif |
Vous pouvez également obtenir du polymorphisme en jouant sur la liaison avec des bibliothèques. A partir d'une même interface, vous pouvez avoir différentes implémentations produisant des bibliothèques statiques différentes (.lib, .a…). La commande d'édition des liens (ou dans votre makefile ou dans les options d'un projet avec un I.D.E.) précise la bibliothèque avec laquelle les liens doivent être résolus. L'exécutable généré fait alors appel à l'interface implémentée dans la bibliothèque avec laquelle il a été liée.
Une application peut choisir de varier son comportement en chargeant dynamiquement des bibliothèques (.dll, .so…) et en allant chercher dans celles-ci l'implémentation de l'interface variable. Le comportement va alors changer selon la DLL proposée à l'exécution du moment qu'elle respecte l'interface qu'attend le programme.
S'il est nécessaire de pouvoir modifier le comportement d'un objet au cour de l'exécution, la solution la plus adaptée est sans doute l'application du design pattern Strategy.
Le principe de ce patron de conception est de définir autant de classes que de comportements différents. Toutes ces classes implémentent une même interface. La classe à paramétrer possède une agrégation vers un objet du type de l'interface.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | // Interface de comportement pour baladeur (utilisant le pattern NVI) class walkman_behaviour { public: virtual ~walkman_behaviour(){} void click_back_button() { do_click_back_button(); } void click_forward_button() { do_click_forward_button(); } private: virtual void do_click_back_button() = 0; virtual void do_click_forward_button() = 0; }; // Collection de comportements pour baladeur namespace walkman_behaviours { class mp3_reader: public walkman_behaviour { public: void do_click_back_button() { // Lire chanson précédente... } void do_click_forward_button() { // Lire chanson suivante... } }; class fm_tuner: public walkman_behaviour { public: void do_click_back_button() { // Passer à la fréquence précédente... } void do_click_forward_button() { // Passer à la fréquence suivante... } }; } //baladeur audio class walkman { public: walkman(walkman_behaviour& c): behaviour_(&c) { } void behaviour(walkman_behaviour& c) { behaviour_ = &c; } void click_back_button() { comportement_->click_back_button(); } void click_forward_button() { comportement_->click_forward_button(); } private: walkman_behaviour* behaviour_; }; int main() { walkman_behaviours::mp3_reader behave_mp3; walkman_behaviours::fm_tuner behave_fm; walkman b(behave_mp3); // Comportement par défaut : lecteur mp3 b.click_forward_button(); // Lit la chanson suivante b.behaviour(behave_fm); // Changement de comportement b.click_back_button(); // Passe à la fréquence précédente return 0; } |
Il faut désormais choisir celui qui convient au type de paramétrage de comportement que vous voulez introduire. Une application complexe met souvent en ouvre les différentes solutions pour sa variabilité et son extension. Selon les cas, la variabilité est intégrée par les templates (générique), par l'héritage (ex. : Widget), par les directives de compilation ou l'édition statique de liens (ex. : dépendance de plateforme), ou par le chargement dynamique de bibliothèque (ex. : plugin).
Proposer une nouvelle réponse sur la FAQ
Ce n'est pas l'endroit pour poser des questions, allez plutôt sur le forum de la rubrique pour çaLes sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2024 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.