Développement de classes d'interface en C ++


Les classes d'interface sont trÚs largement utilisées dans les programmes C ++. Mais, malheureusement, des erreurs sont souvent commises lors de l'implémentation de solutions basées sur des classes d'interface. L'article décrit comment concevoir correctement les classes d'interface; plusieurs options sont envisagées. L'utilisation de pointeurs intelligents est décrite en détail. Un exemple d'implémentation d'une classe d'exception et d'un modÚle de classe de collection basé sur des classes d'interface est donné.




Table des matiĂšres


Table des matiĂšres
Présentation
1. Fonctions membres spéciales, création et suppression d'objets
1.1. Fonctions spéciales des membres
1.2. Création et suppression d'objets - détails de base
1.3. Niveau d'accĂšs destructeur
1.4. Créez et supprimez en un seul module
1.5. Suppression polymorphe
1.6. Suppression lorsque la déclaration de classe est incomplÚte
2. Fonctions purement virtuelles et classes abstraites
2.1. Fonctions virtuelles pures
2.2. Classes abstraites
2.3. Destructeur virtuel pur
3. Classes d'interface
3.1. Implémentations
3.2. Création d'objets
3.3. Supprimer un objet
3.3.1. Utilisation de l'opérateur de suppression
3.3.2. Utilisation d'une fonction virtuelle spéciale
3.3.3. Utilisation d'une fonction externe
3.3.4. Suppression automatique Ă  l'aide du pointeur intelligent
3.4. Autres options pour gérer la durée de vie d'une instance d'une classe d'implémentation
3.5. Copier la sémantique
3.6. Constructeur de classe d'interface
3.7. Interaction bidirectionnelle
3.8. Pointeurs intelligents
3.9. Fonctions membres constantes
3.10. Interfaces COM
3.11. Classes d'interface et bibliothĂšques
4. Un exemple de classe d'interface et son implémentation
4.1. Classe d'interface
4.2. Classe d'implémentation
4.3. Pointeurs intelligents standard
4.3.1. Création cÎté client
4.3.2. Création cÎté implémentation
4.4. Implémentation alternative de la classe de base
5. Exceptions et collections implémentées à l'aide de classes d'interface
5.1 Exceptions
5.2 Collections
6. Classes d'interface et classes wrapper
7. Résumé
Les références


Présentation


Une classe d'interface est une classe qui n'a pas de données et se compose principalement de fonctions purement virtuelles. Cette solution vous permet de séparer complÚtement l'implémentation de l'interface - le client utilise la classe d'interface - à un autre endroit, une classe dérivée est créée dans laquelle les fonctions purement virtuelles sont redéfinies et la fonction d'usine est définie. Les détails de l'implémentation sont complÚtement cachés au client. De cette façon, une véritable encapsulation est implémentée, ce qui est impossible avec la classe habituelle. Vous pouvez lire sur les classes d'interface de Scott Meyers [Meyers2]. Les classes d'interface sont également appelées classes de protocole.


L'utilisation de classes d'interface vous permet d'affaiblir les dĂ©pendances entre les diffĂ©rentes parties du projet, ce qui simplifie le dĂ©veloppement de l'Ă©quipe et rĂ©duit le temps de compilation / assemblage. Les classes d'interface facilitent la mise en Ɠuvre de solutions flexibles et dynamiques lorsque les modules sont chargĂ©s de maniĂšre sĂ©lective lors de l'exĂ©cution. L'utilisation des classes d'interface en tant que bibliothĂšque d'interface (API) (SDK) simplifie la solution des problĂšmes de compatibilitĂ© binaire.


Les classes d'interface sont utilisées assez largement, avec leur aide, elles implémentent l'interface (API) des bibliothÚques (SDK), l'interface des plug-ins (plugins) et bien plus encore. De nombreux modÚles de Gang of Four [GoF] sont naturellement implémentés à l'aide de classes d'interface. Les classes d'interface incluent les interfaces COM. Mais, malheureusement, des erreurs sont souvent commises lors de l'implémentation de solutions basées sur des classes d'interface. Essayons de clarifier ce problÚme.



1. Fonctions membres spéciales, création et suppression d'objets


Cette section décrit briÚvement un certain nombre de fonctionnalités C ++ que vous devez connaßtre pour bien comprendre les solutions proposées pour les classes d'interface.



1.1. Fonctions spéciales des membres


Si le programmeur n'a pas dĂ©fini les fonctions membres de la classe dans la liste suivante - le constructeur par dĂ©faut, le constructeur de copie, l'opĂ©rateur d'affectation de copie, le destructeur - alors le compilateur peut le faire pour lui. C ++ 11 a ajoutĂ© un constructeur de dĂ©placement et un opĂ©rateur d'affectation de dĂ©placement Ă  cette liste. Ces fonctions membres sont appelĂ©es fonctions membres spĂ©ciales. Ils ne sont gĂ©nĂ©rĂ©s que s'ils sont utilisĂ©s et des conditions supplĂ©mentaires spĂ©cifiques Ă  chaque fonction sont remplies. Nous attirons l'attention sur le fait que cette utilisation peut s'avĂ©rer assez cachĂ©e (par exemple lors de l'implĂ©mentation de l'hĂ©ritage). Si la fonction requise ne peut pas ĂȘtre gĂ©nĂ©rĂ©e, une erreur est gĂ©nĂ©rĂ©e. (À l'exception des opĂ©rations de relocalisation, elles sont remplacĂ©es par des opĂ©rations de copie.) Les fonctions membres gĂ©nĂ©rĂ©es par le compilateur sont publiques et intĂ©grables.


Les fonctions membres spéciales ne sont pas héritées, si une fonction membre spéciale est requise dans la classe dérivée, le compilateur essaiera toujours de la générer; la présence de la fonction membre correspondante définie dans la classe de base par le programmeur n'affecte pas cela.


Le programmeur peut interdire la génération de fonctions membres spéciales, en C ++ 11 il est nécessaire d'utiliser la construction "=delete" lors de la déclaration, en C ++ 98 déclarer la fonction membre correspondante privée et non définir. Dans l'héritage de classe, l'interdiction de générer une fonction membre spéciale faite dans la classe de base s'applique à toutes les classes dérivées.


Si le programmeur est à l'aise avec les fonctions membres générées par le compilateur, alors en C ++ 11, il peut l'indiquer explicitement, et pas simplement supprimer la déclaration. Pour ce faire, vous devez utiliser la construction "=default" lors de la déclaration, tandis que le code est mieux lu et que des fonctionnalités supplémentaires apparaissent liées à la gestion du niveau d'accÚs.


Des dĂ©tails sur les fonctions spĂ©ciales des membres peuvent ĂȘtre trouvĂ©s dans [Meyers3].



1.2. Création et suppression d'objets - détails de base


La création et la suppression d'objets à l'aide des opérateurs new/delete est une opération deux-en-un typique. Lors de l'appel de new , la mémoire est d'abord allouée à l'objet. Si la sélection réussit, le constructeur est appelé. Si le constructeur lÚve une exception, la mémoire allouée est libérée. Lorsque l'opérateur de delete est appelé, tout se passe dans l'ordre inverse: d'abord, le destructeur est appelé, puis la mémoire est libérée. Le destructeur ne doit pas lever d'exceptions.


Si le new opérateur est utilisé pour créer un tableau d'objets, la mémoire est d'abord allouée à l'ensemble du tableau. Si la sélection réussit, le constructeur par défaut est appelé pour chaque élément du tableau à partir de zéro. Si un constructeur lÚve une exception, alors pour tous les éléments créés du tableau, le destructeur est appelé dans l'ordre inverse de l'appel du constructeur, la mémoire allouée est libérée. Pour supprimer un tableau, vous devez appeler l'opérateur delete[] (appelé l'opérateur delete pour les tableaux), et pour tous les éléments du tableau, le destructeur est appelé dans l'ordre inverse de l'appel du constructeur, puis la mémoire allouée est libérée.


Attention! Vous devez appeler la forme correcte de l'opĂ©rateur de delete , selon qu'un seul objet ou tableau est supprimĂ©. Cette rĂšgle doit ĂȘtre strictement respectĂ©e, sinon vous pouvez obtenir un comportement indĂ©fini, c'est-Ă -dire que tout peut arriver: fuites de mĂ©moire, crash, etc. Voir [Meyers2] pour plus de dĂ©tails.


Les fonctions d'allocation de mĂ©moire standard ne std::bad_alloc Ă  satisfaire la requĂȘte, std::bad_alloc une exception de type std::bad_alloc .


Il est sûr d'appliquer n'importe quelle forme de l'opérateur de delete à un pointeur nul.


Dans la description ci-dessus, une clarification est nĂ©cessaire. Pour les soi-disant types triviaux (types intĂ©grĂ©s, structures de style C), le constructeur ne peut pas ĂȘtre appelĂ© et le destructeur ne fait en aucun cas quoi que ce soit. Voir Ă©galement la section 1.6.



1.3. Niveau d'accĂšs destructeur


Lorsque l'opĂ©rateur de delete est appliquĂ© Ă  un pointeur sur une classe, le destructeur de cette classe doit ĂȘtre disponible au point d'appel de delete . (Il existe une exception Ă  cette rĂšgle, abordĂ©e dans la section 1.6.) Ainsi, en sĂ©curisant ou en fermant le destructeur, le programmeur interdit l'utilisation de l'opĂ©rateur de delete lorsque le destructeur n'est pas disponible. Souvenez-vous que si aucun destructeur n'est dĂ©fini dans la classe, le compilateur le fera seul et ce destructeur sera ouvert (voir section 1.1).



1.4. Créez et supprimez en un seul module


Si le new opĂ©rateur a créé un objet, alors l'opĂ©rateur de delete doit se trouver dans le mĂȘme module pour le delete . Au figurĂ©, "mettez-le lĂ  oĂč vous l'avez pris". Cette rĂšgle est bien connue, voir par exemple [Sutter / Alexandrescu]. Si cette rĂšgle est violĂ©e, une «non-concordance» des fonctions d'allocation et de libĂ©ration de mĂ©moire peut se produire, ce qui, en rĂšgle gĂ©nĂ©rale, entraĂźne une interruption anormale du programme.



1.5. Suppression polymorphe


Si vous concevez une hiérarchie polymorphe de classes dont les instances sont supprimées à l'aide de l'opérateur de delete , il doit y avoir un destructeur virtuel ouvert dans la classe de base, cela garantit que le destructeur du type réel de l'objet est appelé lorsque l'opérateur de delete est appliqué au pointeur sur la classe de base. Si cette rÚgle est violée, un appel au destructeur de classe de base peut se produire, ce qui peut entraßner une fuite de ressources.



1.6. Suppression lorsque la déclaration de classe est incomplÚte


Le caractĂšre omnivore de l'opĂ©rateur de delete peut crĂ©er certains problĂšmes; il peut ĂȘtre appliquĂ© Ă  un pointeur de type void* ou Ă  un pointeur sur une classe qui a une dĂ©claration incomplĂšte (prĂ©emptive). Dans ce cas, aucune erreur ne se produit, seul l'appel au destructeur est ignorĂ©, seule la fonction de libĂ©ration de la mĂ©moire est appelĂ©e. Prenons un exemple:


 class X; //   X* CreateX(); void Foo() {    X* p = CreateX();    delete p; } 

Ce code se compile mĂȘme si une dĂ©claration de classe X complĂšte n'est pas disponible sur le pair de delete numĂ©rotation. Vrai, lors de la compilation (Visual Studio), un avertissement est Ă©mis:


warning C4150: deletion of pointer to incomplete type 'X'; no destructor called


S'il existe une implémentation de X et CreateX() , le code est CreateX() , si CreateX() renvoie un pointeur vers l'objet créé par le new opérateur, alors l'appel Foo() correctement exécuté, le destructeur n'est pas appelé. Il est clair que cela peut conduire à un épuisement des ressources, donc encore une fois sur la nécessité de faire attention aux avertissements.


Cette situation n'est pas farfelue, elle peut facilement survenir lors de l'utilisation de classes telles que des pointeurs intelligents ou des classes de descripteurs. Scott Meyers traite de ce problĂšme dans [Meyers3].



2. Fonctions purement virtuelles et classes abstraites


Le concept de classes d'interface est basé sur des concepts C ++ tels que les fonctions virtuelles pures et les classes abstraites.



2.1. Fonctions virtuelles pures


Une fonction virtuelle déclarée à l'aide de la construction "=0" est appelée virtuelle pure.


 class X { // ...    virtual void Foo() = 0; }; 

Contrairement Ă  une fonction virtuelle classique, une fonction purement virtuelle ne peut pas ĂȘtre dĂ©finie (Ă  l'exception du destructeur, voir section 2.3), mais elle doit ĂȘtre redĂ©finie dans l'une des classes dĂ©rivĂ©es.


Des fonctions purement virtuelles peuvent ĂȘtre dĂ©finies. Emblem Sutter propose plusieurs utilisations utiles pour cette fonction [Shutter].



2.2. Classes abstraites


Une classe abstraite est une classe qui a au moins une fonction purement virtuelle. Une classe dĂ©rivĂ©e d'une classe abstraite et ne remplaçant pas au moins une fonction purement virtuelle sera Ă©galement abstraite. La norme C ++ interdit la crĂ©ation d'instances d'une classe abstraite; vous ne pouvez crĂ©er que des instances de dĂ©rivĂ©s de classes non abstraites. Ainsi, une classe abstraite est créée pour ĂȘtre utilisĂ©e comme classe de base. Par consĂ©quent, si un constructeur est dĂ©fini dans une classe abstraite, alors cela n'a pas de sens de le rendre ouvert, il doit ĂȘtre protĂ©gĂ©.



2.3. Destructeur virtuel pur


Dans certains cas, il est conseillé de faire un destructeur virtuel pur. Mais cette solution a deux fonctionnalités.


  1. Un destructeur purement virtuel doit ĂȘtre dĂ©fini. (La dĂ©finition par dĂ©faut est gĂ©nĂ©ralement utilisĂ©e, c'est-Ă -dire en utilisant la construction "=default" .) Le destructeur de classe dĂ©rivĂ© appelle des destructeurs de classe de base tout au long de la chaĂźne d'hĂ©ritage et, par consĂ©quent, la file d'attente est garantie d'atteindre la racine - un destructeur purement virtuel.
  2. Si le programmeur n'a pas redéfini un destructeur virtuel pur dans la classe dérivée, le compilateur le fera pour lui (voir section 1.1). Ainsi, une classe dérivée d'une classe abstraite avec un destructeur purement virtuel peut perdre son caractÚre abstrait sans écraser explicitement le destructeur.

Un exemple d'utilisation d'un destructeur virtuel pur peut ĂȘtre trouvĂ© dans la section 4.4.



3. Classes d'interface


Une classe d'interface est une classe abstraite qui n'a pas de données et se compose principalement de fonctions purement virtuelles. Une telle classe peut avoir des fonctions virtuelles ordinaires (non purement virtuelles), par exemple, un destructeur. Il peut également y avoir des fonctions membres statiques, telles que des fonctions d'usine.



3.1. Implémentations


Une implĂ©mentation d'une classe d'interface sera appelĂ©e une classe dĂ©rivĂ©e dans laquelle des fonctions purement virtuelles sont redĂ©finies. Il peut y avoir plusieurs implĂ©mentations de la mĂȘme classe d'interface, et deux schĂ©mas sont possibles: horizontal, lorsque plusieurs classes diffĂ©rentes hĂ©ritent de la mĂȘme classe d'interface, et vertical, lorsque la classe d'interface est la racine de la hiĂ©rarchie polymorphe. Bien sĂ»r, il peut y avoir des hybrides.


Le point clé du concept de classes d'interface est la séparation complÚte de l'interface de l'implémentation - le client ne travaille qu'avec la classe d'interface, l'implémentation n'est pas disponible pour elle.



3.2. Création d'objets


L'inaccessibilitĂ© de la classe d'implĂ©mentation provoque certains problĂšmes lors de la crĂ©ation d'objets. Le client doit crĂ©er une instance de la classe d'implĂ©mentation et obtenir un pointeur sur la classe d'interface via laquelle l'objet sera accessible. Étant donnĂ© que la classe d'implĂ©mentation n'est pas disponible, vous ne pouvez pas utiliser le constructeur, par consĂ©quent, la fonction d'usine est utilisĂ©e, qui est dĂ©finie du cĂŽtĂ© de l'implĂ©mentation. Cette fonction crĂ©e gĂ©nĂ©ralement un objet Ă  l'aide du new opĂ©rateur et renvoie un pointeur sur l'objet créé, transtypĂ© en pointeur sur une classe d'interface. Une fonction de fabrique peut ĂȘtre un membre statique d'une classe d'interface, mais elle n'est pas nĂ©cessaire, par exemple, elle peut ĂȘtre membre d'une classe de fabrique spĂ©ciale (qui, Ă  son tour, peut elle-mĂȘme ĂȘtre une classe d'interface) ou une fonction libre. Une fonction d'usine peut renvoyer non pas un pointeur brut vers une classe d'interface, mais un pointeur intelligent. Cette option est discutĂ©e dans les sections 3.3.4 et 4.3.2.



3.3. Supprimer un objet


La suppression d'un objet est une opĂ©ration extrĂȘmement critique. Une erreur entraĂźne une fuite de mĂ©moire ou une double suppression, ce qui entraĂźne gĂ©nĂ©ralement un plantage du programme. Ci-dessous, ce problĂšme est considĂ©rĂ© aussi dĂ©taillĂ© que possible, avec une grande attention portĂ©e Ă  la prĂ©vention des actions erronĂ©es des clients.


Il existe quatre options principales:


  1. Utilisation de l'opérateur de delete .
  2. Utilisation d'une fonction virtuelle spéciale.
  3. Utilisation d'une fonction externe.
  4. Suppression automatique Ă  l'aide du pointeur intelligent.


3.3.1. Utilisation de l'opérateur de delete


Pour ce faire, vous devez avoir un destructeur virtuel ouvert dans la classe d'interface. Dans ce cas, l'opérateur de delete , appelé pour un pointeur vers une classe d'interface cÎté client, fournit un appel au destructeur de la classe d'implémentation. Cette option peut fonctionner, mais il est difficile de la reconnaßtre comme réussie. Nous recevons des appels des new opérateurs et delete différents cÎtés de la «barriÚre», new du cÎté de l'implémentation, delete du cÎté client. Et si l'implémentation de la classe d'interface se fait dans un module séparé (ce qui est assez courant), alors nous obtenons une violation de la rÚgle de la section 1.4.



3.3.2. Utilisation d'une fonction virtuelle spéciale


Plus progressive est une autre option: la classe d'interface doit avoir une fonction virtuelle spĂ©ciale qui supprime l'objet. Une telle fonction, au final, revient Ă  appeler delete this , mais cela se produit dĂ©jĂ  du cĂŽtĂ© de l'implĂ©mentation. Une telle fonction peut ĂȘtre appelĂ©e de diffĂ©rentes maniĂšres, par exemple, Delete() , mais d'autres options sont Ă©galement utilisĂ©es: Release() , Destroy() , Dispose() , Free() , Close() , etc. En plus de suivre la rĂšgle de la section 1.4, cette option prĂ©sente plusieurs avantages supplĂ©mentaires.


  1. Vous permet d'utiliser des fonctions personnalisées d'allocation / désallocation de mémoire pour la classe d'implémentation.
  2. Vous permet d'implémenter un schéma plus complexe pour contrÎler la durée de vie de l'objet d'implémentation, par exemple, à l'aide d'un compteur de référence.

Dans ce mode de rĂ©alisation, une tentative de suppression d'un objet Ă  l'aide de l'opĂ©rateur de delete peut ĂȘtre compilĂ©e et mĂȘme effectuĂ©e, mais il s'agit d'une erreur. Pour l'empĂȘcher dans la classe d'interface, il suffit d'avoir un destructeur protĂ©gĂ© vide ou purement virtuel (voir section 1.3). Notez que l'utilisation de l'opĂ©rateur de delete peut ĂȘtre assez masquĂ©e, par exemple, les pointeurs intelligents standard utilisent l'opĂ©rateur de suppression pour supprimer un objet par dĂ©faut et le code correspondant est profondĂ©ment enfoui dans leur implĂ©mentation. Un destructeur protĂ©gĂ© vous permet de dĂ©tecter toutes ces tentatives au stade de la compilation.



3.3.3. Utilisation d'une fonction externe


Cette option peut attirer une certaine symétrie des procédures de création et de suppression d'un objet, mais en réalité elle n'a aucun avantage par rapport à la version précédente, mais il existe de nombreux problÚmes supplémentaires. Cette option n'est pas recommandée pour une utilisation et n'est pas envisagée à l'avenir.



3.3.4. Suppression automatique Ă  l'aide du pointeur intelligent


Dans ce cas, la fonction d'usine ne renvoie pas un pointeur brut vers une classe d'interface, mais un pointeur intelligent correspondant. Ce pointeur intelligent est créé cĂŽtĂ© implĂ©mentation et encapsule l'objet de suppression, qui supprime automatiquement l'objet implĂ©mentation lorsque le pointeur intelligent (ou sa derniĂšre copie) sort du domaine cĂŽtĂ© client. Dans ce cas, une fonction virtuelle spĂ©ciale pour supprimer l'objet d'implĂ©mentation peut ne pas ĂȘtre requise, mais un destructeur protĂ©gĂ© est toujours nĂ©cessaire, il est nĂ©cessaire d'empĂȘcher l'utilisation erronĂ©e de l'opĂ©rateur de delete . (Certes, il convient de noter que la probabilitĂ© d'une telle erreur est sensiblement rĂ©duite.) Cette option est examinĂ©e plus en dĂ©tail Ă  la section 4.3.2.



3.4. Autres options pour gérer la durée de vie d'une instance d'une classe d'implémentation


Dans certains cas, le client peut recevoir un pointeur vers la classe d'interface, mais ne le possĂšde pas. La gestion de la durĂ©e de vie de l'objet d'implĂ©mentation est entiĂšrement du cĂŽtĂ© de l'implĂ©mentation. Par exemple, un objet peut ĂȘtre un objet singleton statique (cette solution est typique pour les usines). Un autre exemple est liĂ© Ă  l'interaction bidirectionnelle, voir la section 3.7. Le client ne doit pas supprimer un tel objet, mais un destructeur protĂ©gĂ© pour une telle classe d'interface est nĂ©cessaire, il est nĂ©cessaire d'empĂȘcher l'utilisation erronĂ©e de l'opĂ©rateur de delete .



3.5. Copier la sémantique


Pour une classe d'interface, la création d'une copie de l'objet d'implémentation à l'aide du constructeur de copie n'est pas possible, donc si la copie est requise, la classe doit avoir une fonction virtuelle qui crée une copie de l'objet d'implémentation et renvoie un pointeur vers la classe d'interface. Une telle fonction est souvent appelée constructeur virtuel et son nom traditionnel est Clone() ou Duplicate() .


L'utilisation de l'opĂ©rateur d'affectation de copie n'est pas interdite, mais ne peut pas ĂȘtre considĂ©rĂ©e comme une bonne idĂ©e. L'opĂ©rateur d'affectation de copie est toujours associĂ©; il doit ĂȘtre associĂ© au constructeur de copie. L'opĂ©rateur gĂ©nĂ©rĂ© par le compilateur par dĂ©faut n'a pas de sens, il ne fait rien. Il est thĂ©oriquement possible de dĂ©clarer un opĂ©rateur d'affectation purement virtuel avec redĂ©finition ultĂ©rieure, mais l'affectation virtuelle n'est pas une pratique recommandĂ©e, des dĂ©tails peuvent ĂȘtre trouvĂ©s dans [Meyers1]. De plus, l'affectation semble trĂšs peu naturelle: l'accĂšs aux objets de la classe d'implĂ©mentation se fait gĂ©nĂ©ralement via un pointeur vers la classe d'interface, donc l'affectation ressemblera Ă  ceci:


 * = *; 

Il est préférable d'interdire à l'opérateur d'affectation, et si nécessaire, une telle sémantique a dans la classe d'interface la fonction virtuelle correspondante.


Il existe deux façons d'interdire l'attribution.


  1. Déclarez l'opérateur d'affectation supprimé ( =delete ). Si les classes d'interface forment une hiérarchie, cela suffit pour le faire dans la classe de base. L'inconvénient de cette méthode est qu'elle affecte la classe d'implémentation, l'interdiction s'y applique également.
  2. DĂ©clarez une instruction d'affectation protĂ©gĂ©e avec une dĂ©finition par dĂ©faut ( =default ). Cela n'affecte pas la classe d'implĂ©mentation, mais dans le cas d'une hiĂ©rarchie de classes d'interface, une telle annonce doit ĂȘtre faite dans chaque classe.


3.6. Constructeur de classe d'interface


Souvent, le constructeur d'une classe d'interface n'est pas dĂ©clarĂ©. Dans ce cas, le compilateur gĂ©nĂšre le constructeur par dĂ©faut nĂ©cessaire pour implĂ©menter l'hĂ©ritage (voir section 1.1). Ce constructeur est ouvert, bien que suffisant pour ĂȘtre sĂ©curisĂ©. Si dans la classe d'interface le constructeur copiant est dĂ©clarĂ© supprimĂ© ( =delete ), alors la gĂ©nĂ©ration par le compilateur du constructeur est supprimĂ©e par dĂ©faut, et un tel constructeur doit ĂȘtre explicitement dĂ©clarĂ©. Il est naturel de le sĂ©curiser avec une dĂ©finition par dĂ©faut ( =default ). En principe, la dĂ©claration d'un tel constructeur protĂ©gĂ© peut toujours se faire. Un exemple est dans la section 4.4.



3.7. Interaction bidirectionnelle


Les classes d'interface sont pratiques pour utiliser la communication bidirectionnelle. Si certains modules sont accessibles via des classes d'interface, le client peut également créer des implémentations de certaines classes d'interface et leur transmettre des pointeurs dans le module. Grùce à ces pointeurs, le module peut recevoir des services du client et également transmettre des données ou des notifications au client.



3.8. Pointeurs intelligents


Étant donnĂ© que l'accĂšs aux objets de la classe d'implĂ©mentation se fait gĂ©nĂ©ralement via un pointeur, il est naturel d'utiliser des pointeurs intelligents pour contrĂŽler leur durĂ©e de vie. Mais il faut garder Ă  l'esprit que si la deuxiĂšme option pour supprimer des objets est utilisĂ©e, alors avec le pointeur intelligent standard, il est nĂ©cessaire de transfĂ©rer un suppresseur utilisateur (type) ou une instance de ce type. Si cela n'est pas fait, le pointeur intelligent utilisera l'opĂ©rateur de suppression pour supprimer l'objet, et le code ne sera tout simplement pas compilĂ© (grĂące au destructeur protĂ©gĂ©). Les pointeurs intelligents standard (y compris l'utilisation de dissolvants personnalisĂ©s) sont discutĂ©s en dĂ©tail dans [Josuttis], [Meyers3]. Un exemple d'utilisation d'un dĂ©capant personnalisĂ© peut ĂȘtre trouvĂ© dans la section 4.3.1.


, , , .



3.9. -


- const. , , -, .



3.10. COM-


COM- , , COM — , COM- , C, , . COM- C++ , COM.



3.11.


(API) (SDK). . -, -, . , (Windows DLL), : -. . , , . LoadLibrary() , -, .



4.



4.1.


, .


 class IBase { protected:    virtual ~IBase() = default; //   public:    virtual void Delete() = 0; //      IBase& operator=(const IBase&) = delete; //   }; 

.


 class IActivatable : public IBase { protected:    ~IActivatable() = default; //   public:    virtual void Activate(bool activate) = 0;    static IActivatable* CreateInstance(); // - }; 

, , . , IBase . , (. 1.3). , .



4.2.


 class Activator : private IActivatable { // ... private:    Activator(); protected:    ~Activator(); public:    void Delete() override;    void Activate(bool activate) override;    friend IActivatable* IActivatable::CreateInstance(); }; Activator::Activator() {/* ... */} Activator::~Activator() {/* ... */} void Activator::Delete() { delete this; } void Activator::Activate(bool activate) {/* ... */} IActivatable* IActivatable::CreateInstance() {    return static_cast<IActivatable*>(new Activator()); } 

, , , - , .



4.3.



4.3.1.


. - ( IBase ):


 struct BaseDeleter {    void operator()(IBase* p) const { p->Delete(); } }; 

std::unique_ptr<> - :


 template <class I> // I —  IBase using UniquePtr = std::unique_ptr<I, BaseDeleter>; 

, , - , UniquePtr .


-:


 template <class I> // I —  - CreateInstance() UniquePtr<I> CreateInstance() {    return UniquePtr<I>(I::CreateInstance()); } 

:


 template <class I> // I —  IBase UniquePtr<I> ToPtr(I* p) {    return UniquePtr<I>(p); } 

std::shared_ptr<> std::unique_ptr<> , , std::shared_ptr<> . Activator .


 auto un1 = CreateInstance<IActivatable>(); un1->Activate(true); auto un2 = ToPtr(IActivatable::CreateInstance()); un2->Activate(true); std::shared_ptr<IActivatable> sh = CreateInstance<IActivatable>(); sh->Activate(true); 

( — -):


 std::shared_ptr<IActivatable> sh2(IActivatable::CreateInstance()); 

std::make_shared<>() , ( ).


: , . : , - . 4.4.



4.3.2.


. -. std::shared_ptr<> , , ( ). std::shared_ptr<> ( ) - , delete . std::shared_ptr<> - ( ) - . .


 #include <memory> class IActivatable; using ActPtr = std::shared_ptr<IActivatable>; //   class IActivatable { protected:    virtual ~IActivatable() = default; //      IActivatable& operator=(const IActivatable&) = default; //   public:    virtual void Activate(bool activate) = 0;    static ActPtr CreateInstance(); // - }; //   class Activator : public IActivatable { // ... public:    Activator();  //      ~Activator(); //      void Activate(bool activate) override; }; Activator::Activator() {/* ... */} Activator::~Activator() {/* ... */} void Activator::Activate(bool activate) {/* ... */} ActPtr IActivatable::CreateInstance() {    return ActPtr(new Activator()); } 

- std::make_shared<>() :


 ActPtr IActivatable::CreateInstance() {    return std::make_shared<Activator>(); } 

std::unique_ptr<> , , - , .



4.4.


C# Java C++ «», . . IBase .


 class IBase { protected:    IBase() = default;    virtual ~IBase() = 0; // ,       virtual void Delete(); //   public:    IBase(const IBase&) = delete;            //      IBase& operator=(const IBase&) = delete; //      struct Deleter        // -    {        void operator()(IBase* p) const { p->Delete(); }    };    friend struct IBase::Deleter; }; 

, Delete() , .


 IBase::~IBase() = default; void IBase::Delete() { delete this; } 

IBase . Delete() , . - IBase . Delete() , - . Delete() , . , 4.3.1.



5. ,



5.1


, , , , .


, , IException Exception .


 class IException {    friend class Exception;    virtual IException* Clone() const = 0;    virtual void Delete() = 0; protected:    virtual ~IException() = default; public:    virtual const char* What() const = 0;    virtual int Code() const = 0;    IException& operator=(const IException&) = delete; }; class Exception {    IException* const m_Ptr; public:    Exception(const char* what, int code);    Exception(const Exception& src) : m_Ptr(src.m_Ptr->Clone()) {}    ~Exception() { m_Ptr->Delete(); }    const IException* Ptr() const { return m_Ptr; } }; 

Exception , IException . , throw , . Exception , . - , .


Exception , , .


IException :


 class ExcImpl : IException {    friend class Exception;    const std::string m_What;    const int m_Code;    ExcImpl(const char* what, int code);    ExcImpl(const ExcImpl&) = default;    IException* Clone() const override;    void Delete() override; protected:    ~ExcImpl() = default; public:    const char* What() const override;    int Code() const override; }; ExcImpl::ExcImpl(const char* what, int code)    : m_What(what), m_Code(code) {} IException* ExcImpl::Clone() const { return new ExcImpl(*this); } void ExcImpl::Delete() { delete this; } const char* ExcImpl::What() const { return m_What.c_str(); } int ExcImpl::Code() const { return m_Code; } 

Exception :


 Exception::Exception(const char* what, int code)    : m_Ptr(new ExcImpl(what, code)) {} 

, — .NET — , — , C++/CLI. , , , C++/CLI.



5.2


- :


 template <typename T> class ICollect { protected:    virtual ~ICollect() = default; public:    virtual ICollect<T>* Clone() const = 0;    virtual void Delete() = 0;    virtual bool IsEmpty() const = 0;    virtual int GetCount() const = 0;    virtual T& GetItem(int ind) = 0;    virtual const T& GetItem(int ind) const = 0;    ICollect<T>& operator=(const ICollect<T>&) = delete; }; 

, -, .


 template <typename T> class ICollect; template <typename T> class Iterator; template <typename T> class Contain {    typedef ICollect<T> CollType;    CollType* m_Coll; public:    typedef T value_type;    Contain(CollType* coll);    ~Contain(); //     Contain(const Contain& src);    Contain& operator=(const Contain& src); //     Contain(Contain&& src);    Contain& operator=(Contain&& src);    bool mpty() const;    int size() const;    T& operator[](int ind);    const T& operator[](int ind) const;    Iterator<T> begin();    Iterator<T> end(); }; 

. , . , , , , - begin() end() , . (. [Josuttis]), for . . , , .



6. -


. -, . . , ++. , .NET, Java Pyton. . , , . .NET Framework C++/CLI C++. .



7.


-, .


.


  1. delete .
  2. .
  3. .

.


, delete . , .


- , . , , delete .


.


, , , , .




Les références


[GoF]
Gamma E., Helm R., Johnson R., Vlissides J. Méthodes de conception orientée objet. ModÚles de conception.: Per. de l'anglais - Saint-Pétersbourg: Peter, 2001.


[Josuttis]
Josattis, Nikolai M. C ++ Standard Library: Reference Guide, 2nd ed.: Per. de l'anglais - M.: LLC «I.D. Williams, 2014.


[Dewhurst]
, . C++. .: . . — .: , 2012.


[Meyers1]
, . C++. 35 .: . . — .: , 2000.


[Meyers2]
, . C++. 55 .: . . — .: , 2014.


[Meyers3]
, . C++: 42 C++11 C++14.: . . — .: «.. », 2016.


[Sutter]
, . C++.: . . — : «.. », 2015.


[Sutter/Alexandrescu]
, . , . ++.: . . — .: «.. », 2015.




Source: https://habr.com/ru/post/fr427281/


All Articles