XIX. Annexe B : Directives de programmation▲
Cet appendice est une collection de suggestions pour la programmation en C++. Elles ont été rassemblé au cours de mon enseignement, de mon expérience et sont
également le fruit de la perspicacité d'amis y compris Dan Saks (coauteur avec Tom Plum de C++ Programming Guidelines, Plum Hall, 1991), Scott Meyers (auteur de Effective C++, 2d edition, Addison-Wesley, 1998), et Rob Murray (auteur de C++ Strategies & Tactics, Addison-Wesley, 1993). Enfin, beaucoup de conseils sont des résumés des pages de Thinking in C++.
- Faites-le d'abord fonctionner, puis rendez-le rapide. C'est vrai même si vous êtes certain qu'un morceau de code est réellement important et que ce sera un goulot d'étranglement majeur de votre système. Ne le faites pas. Faites un système qui fonctionne avec un design aussi simple que possible. Ensuite, si cela ne va pas assez vite, profilez-le. Vous découvrirez presque toujours que votre goulot d'étranglement n'est pas le problème. Economisez votre temps pour les choses réellement importantes.
- L'élégance paye toujours. Ce n'est pas une recherche frivole. Non seulement cela vous donnera un programme plus facile à construire et à déboguer, mais il sera aussi plus facile à comprendre et à maintenir, et c'est là que se trouve la valeur financière. Ce point peut nécessiter un peu d'expérience pour être cru, car il peut sembler que quand vous écrivez un morceau de code élégant, vous n'êtes pas productif. La productivité vient quand le code s'intègre sans heurts dans votre système, et plus encore, quand le code ou le système sont modifiés.
- Souvenez-vous du principe “diviser pour mieux régner”. Si le problème que vous essayez de résoudre est trop confus, essayez d'imaginer l'opération de base du programme, en donnant naissance à un “morceau magique » qui va gérer les parties difficiles. Ce “morceau” est un objet – écrivez le code qui utilise cet objet, puis regardez l'objet et encapsulez ses parties difficiles dans d'autres objets, etc.
- Ne réécrivez pas automatiquement tout le code C existant en C++ à moins que vous n'ayez besoin de changer significativement ses fonctionnalités (autrement dit: ne réparez pas ce qui n'est pas cassé). Recompiler du C dans du C++ est une activité utile, car elle permet de révéler des bugs cachés. Cependant, prendre du code C qui fonctionne bien et le réécrire en C++ peut ne pas être le meilleur usage de votre temps, à moins que la version C++ ne fournisse beaucoup d'opportunités de réemplois, comme une classe.
- Si vous avez affaire à une grande portion de code C qui doit être changée, isolez d'abord les parties de code qui ne doivent pas être modifiées, placez-les dans une “classe API” en tant que fonctions membres statiques. Ensuite, concentrez-vous sur le code qui doit être changé, en le refactorisant dans des classes pour faciliter les processus de modifications et de maintenance.
- Séparez le créateur de la classe de l'utilisateur de la classe ( programmeur client). L'utilisateur de la classe est le “client” et n'a aucun besoin ou ne veut pas savoir ce qui se passe derrière la scène de la classe. Le créateur de la classe doit être l'expert en design de classe et doit écrire la classe de manière à ce qu'elle puisse être utilisée par un programmeur le plus novice possible, et fonctionner de manière robuste dans l'application. L'usage d'une bibliothèque ne sera facile que si elle est transparente.
- Quand vous créez une classe, rendez les noms aussi clairs que possible. Votre but est de rendre l'interface du client programmeur conceptuellement simple. Essayez de rendre vos noms suffisamment clairs pour que les commentaires deviennent inutiles. À cette fin, utilisez la surcharge de fonction et les arguments par défaut pour créer une interface intuitive et facile d'usage.
- Le contrôle d'accès vous permet (en tant que créateur de la classe) de faire autant de changements que possible dans le futur sans causer de dommages au code du client dans lequel la classe est utilisée. De ce point de vue, gardez tout ce que vous pouvez aussi private que possible, et rendez seulement l'interface publique, utilisez toujours des fonctions plutôt que des données. Ne rendez des données public que quand vous y êtes forcés. Si l'utilisateur de la classe n'a pas besoin d'avoir accès à une fonction, rendez-la private Si une partie de votre classe doit être exposée à des héritiers en tant que protected, fournissez une fonction d'interface plutôt que d'exposer la donnée elle-même. De cette manière les changements d'implémentation auront un impact minime sur les classes dérivées.
- Ne sombrez pas dans la paralysie de l'analyse. Il y a plusieurs choses qui ne s'apprennent qu'en commençant à coder et en faisant fonctionner un type ou l'autre de système. Le C++ a ses propres pare-feux ; laissez les travailler pour vous. Vos erreurs dans une classe ou dans un jeu de classes ne détruiront pas l'intégrité de tout le système.
- Votre analyse et votre conception doivent produire, au minimum, les classes de votre système, leurs interfaces publiques, et leurs relations aux autres classes. Spécialement les classes de base. Si votre méthodologie de design produit plus que cela, demandez-vous si toutes les pièces produites par cette méthodologie ont de la valeur sur la durée de vie de votre programme. Si elles n'en ont pas, les maintenir aura un prix. Les membres des équipes de développement ne tendent pas à maintenir autre chose que ce qui contribue à leur productivité ; c'est un fait de la vie que beaucoup de méthodes de conception ne prennent pas en compte.
- Écrivez le test du code en premier (avant d'écrire la classe), et gardez-le avec la classe. Automatisez l'exécution de vos tests au travers d'un makefile ou d'un outil similaire. De cette manière, chaque changement peut être automatiquement vérifié en exécutant le code de test, et vous découvrirez immédiatement les erreurs. Comme vous savez que vous disposez du filet de sécurité de votre cadre de tests, vous serez plus enclins à effectuer des changements quand vous en découvrirez le besoin. Retenez que les plus grandes améliorations dans le langage viennent de la vérification intégrée, que procurent la vérification de type, la gestion des exceptions… mais cela ne fera pas tout. Vous devez parcourir le reste du chemin en créant un système robuste par des tests qui vérifieront les aspects spécifiques à votre classe ou à votre programme.
- Écrivez le code de test en premier (avant d'écrire la classe) de manière à vérifier que le design de votre classe est complet. Si vous n'êtes pas en mesure d'écrire le code de test, vous ne savez pas de quoi votre classe a l'air. De plus, le fait d'écrire du code de test permettra parfois de faire ressortir des dispositions supplémentaires ou des contraintes dont vous avez besoin dans la classe – ces contraintes ou dispositions n'apparaissent pas forcément durant la phase d'analyse et de conception.
- Retenez une règle fondamentale du génie logiciel (65): tout problème de conception de logiciel peut être simplifié en introduisant un niveau supplémentaire d'indirection conceptuelle. Cette idée est la base de l'abstraction, le premier dispositif de la programmation orientée-objet.
- Rendez les classes aussi atomiques que possible ; autrement dit, donnez à chaque classe un but simple et clair. Si vos classes ou votre système deviennent trop compliqués, séparez les classes les plus complexes en d'autres classes plus simples. L'indicateur le plus évident est sa petite taille : si une classe est grosse, il y a des chances pour qu'elle en fasse trop et qu'elle doive être éclatée.
- Cherchez les longues définitions de fonctions membres. Une fonction qui est longue et compliquée est difficile et onéreuse à maintenir, et essaye peut-être d'en faire trop par elle-même. Si vous voyez une telle fonction, elle indique, au minimum, qu'elle devrait être divisée en plusieurs fonctions. Elle peut aussi suggérer la création d'une nouvelle classe.
- Cherchez les longues listes d'arguments. Les appels de fonctions deviennent alors difficiles à écrire, à lire et à maintenir. À l'opposé, essayez de déplacer les fonctions membres vers une classe où ce serait plus approprié, et/ou passez des objets comme arguments.
- Ne vous répétez pas. Si une partie de code est récurrente dans de nombreuses fonctions dans des classes dérivées, placez-en le code dans une fonction simple dans la classe de base et appelez-la depuis les classes dérivées. Non seulement vous épargnerez de l'espace de code, mais vous facilitez aussi la propagation des changements. Vous pouvez utiliser une fonction inline par souci d'efficacité. Parfois, la découverte de ce code commun permettra d'ajouter une fonctionnalité utile à votre interface.
- Cherchez les instructions switch ou les suites de clauses if-else. C'est typiquement l'indicateur du codage de vérification de type, qui signifie que vous choisissez quel code exécuter sur un type d'information (le type exact peut ne pas être évident au début). Vous pouvez habituellement remplacer ce type de code par l'héritage et le polymorphisme ; l'appel d'une fonction polymorphique va effectuer le test du type pour vous, et facilite l'extension du code.
- Du point de vue de la conception, cherchez et séparez les éléments qui changent de ceux qui restent identiques. C'est-à-dire, cherchez les éléments dans le système que vous pourriez vouloir modifier sans imposer une reconception, puis encapsulez ces éléments dans des classes. Vous trouverez beaucoup plus d'information sur ce concept dans le chapitre traitant des design patterns dans le deuxième volume de ce livre, disponible sur le site www.BruceEckel.com.
- Cherchez la variance. Deux objets sémantiquement différents peuvent avoir des actions, ou des responsabilités identiques, et il y a une tentation naturelle de créer une sous-classe de l'autre juste pour bénéficier de l'héritage. Cela s'appelle la variance, mais il n'y a pas de réelle justification à forcer une relation de superclasse à sous-classe là où elle n'existe pas. Une meilleure solution est de créer une classe de base générale qui produit une interface pour toutes les dérivées » cela requiert un peu plus d'espace, mais vous bénéficiez toujours de l'héritage et vous ferez peut-être une découverte importante au niveau du design.
- Recherchez les limitations durant l'héritage. Les designs les plus clairs ajoutent de nouvelles capacités à celles héritées. Un design suspect retire les anciennes capacités durant l'héritage sans en ajouter de nouvelles. Mais les règles sont faites pour être contournées, et si vous travaillez sur une ancienne bibliothèque de classes, il peut être plus efficace de restreindre une classe existante dans son héritière que de restructurer la hiérarchie de manière à ce que votre nouvelle classe puisse trouver sa place au-dessus de l'ancienne.
- N'étendez pas une fonctionnalité fondamentale dans une classe héritée. Si un élément d'interface est essentiel à une classe, il doit être dans la classe de base, et non ajouté en cours de dérivation. Si vous ajoutez des fonctions membres par héritage, vous devriez peut-être repenser le design.
- Le moins est plus. Commencez avec une interface de la classe aussi petite et aussi simple que ce qu'il vous faut pour résoudre le problème en cours, mais n'essayez pas d'anticiper toutes les façons dont votre classe peut être utilisée. Quand la classe sera utilisée, vous découvrirez les façons dont vous devrez étendre l'interface. Cependant, une fois qu'une classe est en usage, vous ne pouvez pas diminuer l'interface sans déranger le code client. Si vous devez ajouter plus de fonctions, c'est bon ; cela ne va pas déranger le code, autrement qu'en forçant une recompilation. Mais même si de nouvelles fonctions membres remplacent la fonctionnalité d'anciennes, laissez l'interface existante tranquille (vous pouvez combiner les fonctionnalités dans l'implémentation sous-jacente si vous le voulez). Si vous devez étendre l'interface d'une fonction existante en ajoutant des arguments, laissez les arguments existants dans l'ordre actuel, et fournissez des valeurs par défaut pour tous les nouveaux arguments ; de cette manière, vous ne dérangerez pas les appels existants à cette fonction.
- Lisez vos classes à voix haute pour vous assurez qu'elles sont logiques, en vous référant à une relation d'héritage entre une classe de base et une classe dérivée comme “est un(e)” et aux objets membres comme “a un(e)”.
- Quand vous décidez entre l'héritage et la composition, demandez-vous si vous devez transtyper vers le type de base. Sinon, préférez la composition (objets membres) à l'héritage. Ceci peut éliminer la nécessitée ressentie de recours à l'héritage multiple. Si vous faites hériter une classe, les utilisateurs vont penser qu'il sont supposés transtyper.
- Parfois, vous devez faire hériter une classe afin de permettre l'accès aux membres protected de la classe de base. Cela peut mener à l'impression d'avoir besoin de l'héritage multiple. Si vous n'avez pas besoin de transtyper l'objet, commencez par dériver une nouvelle classe pour effectuer l'accès protected. Puis faites-en un objet membre à l'intérieur de toute classe qui en a besoin, plutôt que de la faire hériter.
- Typiquement, une classe de base sera utilisée pour en premier lieu pour créer une interface vers les classes qui en dérivent. Ainsi, quand vous créez une classe de base, créez les fonctions membres en tant que virtuelles pures par défaut. Le destructeur peut aussi être virtuel pur (pour forcer les classes héritières à le surcharger explicitement), mais rappelez-vous de donner un corps de fonction au destructeur, car tous les destructeurs de la hiérarchie de classe sont toujours appelés.
- Quand vous décidez de créer une fonction virtual dans une classe, rendez toutes les fonctions dans cette classe virtual, et rendez le destructeur virtual. Cette approche évite les surprises dans le comportement de l'interface. Ne retirez le mot clé virtual que lorsque vous en êtes à l'optimisation et que votre profiler vous a dirigé par là.
- Utilisez des données membres pour les variations de valeurs et des fonctions virtual pour des variations de comportement. C'est-à-dire que si vous trouvez une classe qui utilise des variables d'état tout au long de ses fonctions et qui modifie son comportement en fonction de ces variables, vous devriez probablement refaire le design pour exprimer ces différences de comportement à l'intérieur de classes héritées et de fonctions virtuelles surchargées.
- Si vous devez faire quelque chose de non portable, faites une abstraction de ce service et localisez-le à l'intérieur d'une classe. Ce niveau supplémentaire d'indirection évite que la non-portabilité ne se répande au travers de tout votre programme.
- Évitez les héritages multiples. Cela vous épargnera de vous trouver en mauvaises situations, principalement le fait de devoir réparer une interface dont vous ne contrôlez pas la classe endommagée (voir volume 2). Vous devriez être un programmeur expérimenté avant de vouloir implémenter l’héritage multiple dans votre système.
- N'utilisez pas l'héritage private. Bien que ce soit admis par le langage et que cela semble avoir certaines fonctionnalités, cela introduit des ambiguïtés importantes lorsque c'est combiné avec l'identification de type à l'exécution. Créez un objet membre privé plutôt que d'utiliser l'héritage privé.
- Si deux classes sont fonctionnellement associées (comme les conteneurs et les itérateurs), essayez de rendre l'une des classes friend imbriqué public de l'autre, à la manière de ce que fait la bibliothèque standard du C++ avec les itérateurs dans les conteneurs (des exemples en sont montrés dans la dernière partie du chapitre 16). Cela permet non seulement l'association entre les classes, mais cela permet aussi de réutiliser le nom en le nichant au sein une autre classe. La bibliothèque standard du C++ le fait en définissant un iterator niché au sein de chaque classe conteneur, fournissant ainsi une interface commune aux conteneurs. L'autre raison pour laquelle vous voudriez nicher une classe au sein d'une autre est en tant que partie de l'implémentation private. Ici, ce sera bénéfique pour cacher l'implémentation au lieu d'utiliser une association de classe et pour éviter la pollution des espaces de nommages dont il a été question plus haut.
- La surcharge d'opérateur n'est qu'une “douceur syntaxique“ : une manière différente d'effectuer un appel à une fonction. Si la surcharge d'un opérateur ne rend pas l'interface plus claire et plus facile à utiliser, ne le surchargez pas. Ne créez qu'un seul opérateur de conversion de type automatique pour une classe. En général, suivez les règles et formats donnés dans le chapitre 12 lorsque vous surchargez des opérateurs.
- Ne vous laissez pas tenter par l'optimisation précoce. Ce chemin est une folie. En particulier, ne vous souciez pas d'écrire (ou d'éviter) des fonctions inline, de rendre certaines fonctions non virtual, ou de tordre le système pour être efficace quand vous construisez le système. Votre but premier est de prouver le design, à moins que le design ne requière une certaine efficacité.
- Normalement, ne laissez pas le compilateur créer les constructeurs et les destructeurs, ou l' operator= pour vous. Les concepteurs de classes devraient toujours dire ce que la classe devrait faire et garder la classe entièrement sous contrôle. Si vous ne voulez pas d'un constructeur de copie ou d'un operator=, déclarez le private. Rappelez-vous que si vous créez un constructeur, il empêche la création automatique du constructeur par défaut.
- Si votre classe contient des pointeurs, vous devez créer un constructeur par copie, un operator= et un destructeur pour que la classe fonctionne correctement.
- Quand vous écrivez un constructeur de copie pour une classe dérivée, souvenez-vous d'appeler le constructeur de copie de la classe de base de manière explicite (ainsi que ceux des objets membres) (cf. chapitre 14.) Si vous ne le faites pas, le constructeur par défaut sera appelé pour la classe de base (ou pour les objets membres) et ce n'est probablement pas ce que vous voulez. Pour appeler le constructeur de copie de la classe de base, passez-lui l'objet dérivé que vous êtes en train de copier: Derived(const Derived& d) : Base(d) { // …
- Quand vous écrivez un opérateur d'assignation pour une classe dérivée, pensez à appeler explicitement l'opérateur d'assignation de la classe de base (cf. chapitre 14.) Si vous ne le faites pas, rien ne se passera (la même chose est vraie pour les objets membres). Pour appeler l'opérateur d'assignation de la classe de base, utilisez le nom de la classe de base et la résolution de portée: Derived& operator=(const Derived& d) { Base::operator=(d);
- Si vous avez besoin de minimiser les temps de compilation en phase de développement d'un gros projet, utilisez la technique des classes chats de Cheshire expliquée au chapitre 5 et retirez-la uniquement si l'efficacité à l'exécution est un problème.
- Évitez le préprocesseur. Utilisez toujours const pour la substitution de valeur et inline pour les macros.
- Gardez les portées aussi petites que possible, pour que la visibilité et la durée de vie de vos objets soient elles aussi aussi courtes que possible. Cela réduit les risques d'utilisation d'un objet dans un mauvais contexte et de cacher un bug difficile à trouver. Par exemple, supposons que vous ayez un conteneur et une partie de code qui le parcourt de manière itérative. Si votre copiez ce code pour l'utiliser avec un nouveau conteneur, vous pouvez accidentellement terminer en utilisant la taille de l'ancien conteneur en tant que limite supérieure du nouveau. Par contre, si l'ancien conteneur est hors de portée, l'erreur sera affichée lors de la compilation.
- Évitez les variables globales. Tâchez de placer les données à l'intérieur de classes. Les fonctions globales sont plus susceptibles d'apparaître naturellement que les variables globales, cependant vous découvrirez sans doute plus tard qu'une fonction globale devrait être une membre static d'une classe.
- Si vous devez déclarer une classe ou une fonction issue d'une bibliothèque, faites-le toujours en incluant un fichier d'en-tête. Par exemple, si vous voulez créer une fonction pour écrire dans un ostream, ne déclarez jamais ostream vous-même en utilisant une spécification de type incomplète telle que class ostream. Cette approche rend votre code vulnérable aux éventuels changements de représentation. (Par exemple, ostream pourrait être en fait un typedef.) Au lieu de cela, utilisez le fichier d'en-tête : #include <iostream>. Lorsque vous créez vos propres classes, si la bibliothèque est importante fournissez une forme abrégée à vos utilisateurs en leur fournissant des spécifications incomplètes (déclarations de nom de classes) pour le cas où elles ne doivent utiliser que des pointeurs. (Cela peut accélérer la compilation.)
- Lorsque vous choisissez le type de retour d'un opérateur surchargé, considérez ce qui peut arriver si les expressions sont écrites à la suite. Renvoyez une copie ou une référence vers une lvalue( return *this) de manière à permettre le chaînage d'expression ( A = B = C). Quand vous définissez l' operator= rappelez-vous du cas x=x.
- Quand vous écrivez une fonction, préférez le passage des arguments par référence const. Tant que vous ne devez pas modifier l'objet passé, cette pratique est la meilleure, car elle a la simplicité d'un passage par valeur, mais ne nécessite ni construction ni destruction pour créer un objet local, ce qui arrive lorsque l'on passe un objet par valeur. Normalement, vous ne devriez pas vous inquiéter de l'efficacité en concevant et en construisant votre système, mais, cette habitude est gagnante à tous les coups.
- Soyez conscients des temporaires . Lorsque vous cherchez à améliorer les performances, cherchez la création de temporaires, principalement avec surcharge d'opérateurs. Si vos constructeurs et destructeurs sont compliqués, le coût de la création et de la destruction d'objets temporaires peut être élevé. Quand vous retournez une valeur depuis une fonction, essayez toujours de construire un objet “sur place” avec l'appel à un constructeur lors du retour: return MyType(i, j); plutôt que MyType x(i, j);return x; Le premier retour (appelée optimisation de la valeur de retour) élimine l'appel à un constructeur par copie et à un destructeur.
- Lorsque vous créez des constructeurs, envisagez les exceptions. Dans le meilleur des cas, le constructeur ne fera rien qui lance une exception. Dans le meilleur scénario suivant, la classe sera composée par des classes héritées robustes, et elles pourront alors se nettoyer correctement d'elles-mêmes si une exception est lancée. Si vous devez avoir devez avoir des pointeurs nus, vous êtes responsables de la récupération de vos propres exceptions et de veiller à la déallocation de chaque ressource pointée avant de lancer une exception au sein de votre constructeur. Si un constructeur doit échouer, l'action appropriée est de lancer une exception.
- Faites juste le minimum dans vos constructeurs. Non seulement cela produit des constructeurs moins coûteux lors de leur appel (dont certains ne sont peut-être pas sous votre contrôle), mais alors il sera moins probable que vos constructeurs lancent une exception ou posent problème.
- La responsabilité du destructeur est de libérer les ressources allouées durant la vie de l'objet, pas seulement durant la construction.
- Utilisez une hiérarchie d'exceptions, de préférence dérivée de la hiérarchie des exceptions du Standard C++, et imbriquée comme une classe publique dans la classe qui lance l'exception. La personne qui les récupérera peut ainsi récupérer les types spécifiques d'exceptions, suivis par le type de base. Si vous ajoutez de nouvelles exceptions dérivées, le code client existant pourra toujours récupérer les exceptions au travers de leur type de base.
- Lancez les exceptions par valeur, et récupérez-les par référence. Laissez la gestion de la mémoire au mécanisme de gestion des exceptions. Si vous lancez des pointeurs vers des objets exceptions qui ont été créés sur tas, le responsable de la récupération de l'exception devra être en mesure de détruire l'exception, ce qui est un mauvais couplage. Si vous récupérez les exceptions par valeur, vous occasionnez des constructions et destructions supplémentaires ; pire encore, les portions dérivées de votre exception peuvent être perdues en cas de transtypage par valeur.
- N'écrivez vos propres templates de classes que si vous en avez besoin. Regardez d'abord dans la bibliothèque Standard C++, puis vers les fournisseurs qui créent des outils dans un but particulier. Devenez efficace dans leur usage et vous augmenterez considérablement votre productivité.
- Lorsque vous créez des templates, cherchez le code qui ne dépend pas d'un type particulier et placez le dans une classe de base non-template pour éviter le gonflement de code. En utilisant l'héritage ou la composition, vous pouvez créer des templates dans lesquels les parties de code qu'ils contiennent dépendent du type et sont donc essentielles.
- N'utilisez pas les fonctions de <cstdio> telles que printf( ). Apprenez à utiliser les iostream à la place (flux d'entrée/sortie, NdC) ; Ils sont sécurisés et malléables au niveau du type, et significativement plus efficaces. Votre investissement sera régulièrement récompensé. En général, préférez utiliser une bibliothèque C++ plutôt qu'une bibliothèque C.
- Evitez d'avoir recours aux types intégrés du C. Ils sont supportés pour des raisons de rétrocompatibilité, mais sont bien moins robustes que les classes C++, ce qui risque d'augmenter le temps que vous passerez à la chasse aux bugs.
- Chaque fois que vous utilisez des types intégrés comme variables globales ou comme variables automatiques, ne les définissez pas jusqu'à ce que vous puissiez également les initialiser. Définissez une variable par ligne, en même temps que leur initialisation. Quand vous définissez des pointeurs, placez l'étoile ‘ *' à côté du nom du type. Vous pouvez le faire sans risque quand vous définissez une variable par ligne. Cette manière de faire tend à être moins confuse pour le lecteur.
- Garantissez l'apparition de l'initialisation dans tous les aspects de votre code. Effectuez l'initialisation de tous les membres dans la liste d'initialisation du constructeur, même pour les types intégrés (en utilisant l'appel à de pseudo constructeurs). L'utilisation de la liste d'initialisation du constructeur est souvent plus efficace lors de l'initialisation de sous-objets; autrement, le constructeur par défaut est appelé, et vous vous retrouvez en plus de cela à appeler d'autres fonctions membres (probablement operator=) de manière à avoir l'initialisation que vous désirez.
- N'utilisez pas la forme MyType a= b; pour définir un objet. Ce dispositif est une source importante de confusion parce qu'elle appelle un constructeur au lieu de l' operator=. Pour la clarté du code, utilisez la forme MyType a(b); à la place. Le résultat est identique, mais cela évitera d'égarer les programmeurs.
- Utilisez les transtypages explicites décrits au chapitre 3. Un transtypage supplante le système de typage normal et est un lieu d'erreur potentiel. Comme les transtypages explicites divisent le transtypage unique-qui-fait-tout du C en classes bien délimitées de transtypage, toute personne déboguant et maintenant le code peut facilement trouver tous les endroits où les erreurs logiques ont le plus de chances de se produire.
- Pour qu'un programme soit robuste, chaque composant doit être robuste. Utilisez tous les outils fournis par le C++ : contrôle d'accès, exceptions, type const ou non correct, vérification de type, etc. dans chaque classe que vous créez. De cette manière vous pouvez passer au niveau d'abstraction suivant lorsque vous créez votre système.
- Veillez à la l'exactitude de la const ance. Cela permet au compilateur de pointer certains bugs subtils qui seraient autrement difficiles à trouver. Cette pratique demande un peu de discipline et doit être utilisée de manière consistante à travers toutes vos classes, mais c'est payant.
- Utilisez les vérifications d'erreur du compilateur à votre avantage. Effectuez toutes les compilations avec tous les avertissements, et corrigez votre code pour supprimer tous les avertissements. Écrivez du code qui utilise les erreurs de compilation plutôt qu'un code qui cause des erreurs d'exécution (par exemple, n'utilisez pas de liste d'arguments variables, qui suppriment le test de type). Utilisez assert( ) pour le débogage, mais utilisez les exceptions pour les erreurs à l'exécution.
- Préférez les erreurs à la compilation aux erreurs à l'exécution. Essayez de gérer une erreur aussi près que possible de l'endroit où elle se produit. Préférez la gestion d'une erreur en ce point plutôt que de lancer une exception. Capturez une exception dans le premier gestionnaire (handler, ndc) qui dispose de suffisamment d'information pour la gérer. Faites ce que vous pouvez avec l'exception au niveau actuel ; si cela ne résout pas le problème, relancez l'exception (voir Volume 2 pour plus de détails.)
- Si vous employez les caractéristiques d'exception (voir le volume 2 de ce livre, téléchargeable sur www.BruceEckel.com, pour en apprendre davantage sur la manipulation des exceptions), installez votre propre fonction unexpected( ) en utilisant set_unexpected( ). Votre unexpected( ) devrait reporter l'erreur dans un fichier avant de relancer l'exception courante. De cette manière si une fonction existante se trouve supplantée et se met à lancer une exception, vous aurez une sauvegarde du coupable et pourrez modifier votre code pour manipuler l'exception.
- Créez une fonction terminate( ) définie par l'utilisateur (indiquant une erreur du programmeur) pour reporter l'erreur qui a causé l'exception, puis libérer les ressources système, et quittez le programme
- Si un destructeur appelle des fonctions, ces fonctions peuvent lancer des exceptions. Un destructeur ne peut pas lancer une exception (qui pourrait résulter en un appel à terminate( ), qui indique une erreur de programmation), si bien que les destructeurs qui appellent des fonctions doivent récupérer et gérer leurs propres exceptions.
- Ne créez pas vos propres noms de membre décorés (par ajout d'underscores, par notation hongroise, etc.) à moins que vous n'ayez un grand nombre de valeurs globales préexistantes ; autrement, laissez les classes et les espaces de nommage gérer la portée des noms pour vous.
- Vérifiez les surcharges. Une fonction ne devrait pas exécuter conditionnellement du code selon la valeur d'un argument, qu'il soit fourni par défaut ou non. Dans ce cas, vous devriez créer deux fonctions surchargées ou plus.
- Cachez vos pointeurs à l'intérieur de classes conteneurs. Ne les sortez que quand vous devez faire des opérations directement dessus. Les pointeurs ont toujours été une source majeure de bugs. Quand vous utilisez new, essayez de placer le pointeur résultant dans un conteneur. Préférez qu'un conteneur « possède » ses pointeurs, de manière à ce qu'il soit responsable de leur nettoyage. Mieux encore, placez le pointeur à l'intérieur d'une classe ; si vous voulez toujours qu'elle ressemble à un pointeur, surchargez operator-> et operator*. Si vous devez avoir un pointeur libre, initialisez-le toujours, de préférence à l'adresse d'un objet, mais à zéro si nécessaire. Mettez-le à zéro quand vous l'effacez pour éviter les suppressions multiples accidentelles.
- Ne surchargez pas les new et delete globaux ; surchargez-les toujours classe par classe. Surcharger les versions globales affectant l'ensemble du projet du client, chose que seul le créateur du projet doit contrôler. Quand vous surchargez new et delete pour des classes, ne présumez pas que vous connaissez la taille de l'objet ; quelqu'un peut hériter de vos classes. Utilisez les arguments fournis. Si vous faites quelque chose de spécial, considérez l'effet que cela peut avoir sur les héritiers.
- Prévenez le saucissonnage des objets. Il n'y a pratiquement aucun sens à effectuer un transtypage ascendant d'un objet par valeur. Pour éviter le transtypage ascendant par valeur, placez des fonctions virtuelles pures dans votre classe de base.
- Parfois, une simple agrégation fait le travail. Un “système confortable pour les passagers sur une ligne aérienne est fait d'éléments disjoints : sièges, air conditionné, vidéo… et pourtant vous devez en créer beaucoup dans un avion. Allez-vous faire des membres privés et créer une toute nouvelle interface ? Non – dans ce cas, les composants font aussi partie de l'interface publique, et vous devriez donc créer des objets membres publiques. Ces objets ont leur propre implémentation privée, qui est toujours sécurisée. Soyez conscient que l'agrégation simple n'est pas une solution à utiliser souvent, mais cela peut se produire.