Les grands projets logiciels sont généralement conduits par de grandes équipes de développeurs. ²Pour que la qualité d'un code, produit par de grandes équipes, soit à la taille du projet, il doit être écrit conformément au projet et soumis à un standard. Les grandes équipes de projet doivent donc établir un standard de programmation ou un ensemble d'instructions.
L'utilisation d'un standard de programmation permet également de :
Ce document a pour objectif de présenter les règles de programmation C++, les instructions et les astuces (généralement appelées instructions) qui peuvent être utilisées en tant que base d'un standard. Il s'adresse aux ingénieurs logiciels travaillant dans de grandes équipes projet.
La version actuelle est délibérément centrée sur la programmation (même s'il est parfois difficile de faire la distinction entre programmation et conception). Les instructions de conception seront ajoutées ultérieurement.
Les instructions présentées couvrent les aspects du développement C++ suivants :
Elles ont été rassemblées à partir d'une importante base de connaissances informatiques. (Voir la bibliographie des sources : auteurs et références.) Elles sont basées sur :
La plupart des ces instructions sont tirées de la première catégorie, et beaucoup de la seconde et de la troisième. Malheureusement, quelques-unes sont également basées sur la dernière catégorie, principalement car la programmation est une activité fortement subjective : il n'existe pas de "meilleure" ou de "bonne" manière de coder, qui soit largement approuvée.
Un code source C++ clair et compréhensible est l'objectif premier de la plupart des règles et instructions : c'est en effet un facteur majeur contribuant à la fiabilité et à la maintenabilité d'un logiciel. Un code clair et compréhensible peut être défini par les trois simples principes fondamentaux suivants [Kruchten, 94] :
Principe de moindre surprise : durant toute son existence, le code source est plus souvent lu qu'écrit, particulièrement les spécifications. Dans l'idéal, le code devrait être lu comme une description, en anglais, de ce qui est fait, avec l'avantage de s'exécuter. Les programmes sont davantage écrits pour les personnes que pour les ordinateurs. La lecture d'un code est un processus mental complexe qui peut être facilité par l'uniformité : il s'agit du principe de moindre surprise. Un style uniforme dans un projet entier est une raison majeure pour une équipe de développeurs logiciel de s'accorder sur des standards de programmation. Il ne doit pas être considéré comme une sorte de punition ou d'obstacle à la créativité et à la productivité.
Point de maintenance unique - Dans la mesure du possible, une décision de conception doit être exprimée à un seul point dans la source, et la plupart de ses conséquences doivent dériver par voie de programmation à partir de ce point. Les violations de ce principe compromettent grandement la maintenabilité et la fiabilité, ainsi que la clarté.
Principe du bruit minimal - Enfin, comme contribution majeure à la lisibilité, le principe de moindre bruit est appliqué. Cela signifie qu'un effort est fait pour éviter l'encombrement du code source par des "bruits" visuels : barres, cases et autre texte contenant un faible contenu informationnel ou des informations ne contribuant pas à la compréhension de l'objet du logiciel.
Les instructions rassemblées dans ce document n'ont pas pour objectif d'être excessivement restrictives, mais plutôt d'essayer d'offrir des conseils pour une utilisation correcte et sûre des fonctions du langage. La clé d'un bon logiciel réside dans :
Les instructions présentées dans ce document partent du principe que :
Les instructions ne sont pas toutes de même importance, elles sont évaluées selon l'échelle suivante :
Une instruction identifiée par le symbole ci-dessus est un simple conseil pouvant être suivi, ou ignoré sans risque.
Une instruction identifiée par le symbole ci-dessus est une recommandation traitant généralement de domaines plus techniques : l'encapsulation, la cohésion, le couplage, la portabilité, la réutilisation ou encore la performance dans certaines implémentations. Les recommandations doivent être suivies, sauf en cas de raison justifiée.
Une instruction identifiée par le symbole ci-dessus est une exigence ou une restriction. Le non respect de cette instruction entraîne un code incorrect, peu fiable ou non transportable. Les exigences et restrictions ne peuvent pas être transgressées sans dérogation
Lorsque vous ne trouvez aucune règle ou instruction applicable, qu'une règle ne s'applique manifestement pas ou que toute autre solution échoue : utilisez le sens commun et vérifiez les principes fondamentaux. Cette règle l'emporte sur toutes les autres. Le sens commun est requis même en cas de règles et d'instructions.
Ce chapitre fournit des conseils sur la présentation et la structure des programmes.
Les grands systèmes sont généralement développés par des sous-systèmes fonctionnels plus petits. Les sous-systèmes sont eux-mêmes généralement construits à partir d'un nombre de modules de code. Dans le C++, un module contient normalement l'implémentation pour un seul, ou rarement, pour un ensemble d'abstractions étroitement associées. Dans C++, une abstraction est normalement mise en oeuvre en tant que classe. Une classe a deux composants distincts : une interface visible aux clients de la classe, qui fournit une déclaration ou une spécification des responsabilités et des fonctions de la classe, ainsi qu'une implémentation de la spécification déclarée (la définition de la classe).
Tout comme la classe, un module a une interface et une implémentation : l'interface du module contient les spécifications des abstractions de module (déclarations de classe), et l'implémentation du module contient l'implémentation exacte des abstractions (définitions de classe).
Dans la construction du système, les sous-systèmes peuvent également être organisés en couches ou en groupes collaboratifs dans le but de minimiser et de contrôler leurs liens de dépendance.
La spécification d'un module doit être placée dans un fichier distinct de son implémentation. Le fichier de spécification est appelé en-tête. L'implémentation d'un module peut être placée dans un ou plusieurs fichier(s) d'implémentation.
Si l'implémentation d'un module contient des fonctions inline étendues, une implémentation commune (déclarations privées, code de test ou plate-forme), un code spécifique, séparez alors ces éléments dans leurs propres fichiers et nommez chaque fichier après le contenu de son élément.
Si les tailles des programmes exécutables représentent un souci, les fonctions rarement utilisées devraient alors être placées dans leurs propres fichiers individuels.
Construisez un nom de fichier d'élément comme suit :
File_Name::=
<Module_Name> [<Separator> <Part_Name>] '.' <File_Extension>
Voici un exemple de schéma de désignation et de partitionnement de module :
inlines
distinct (voir "Placer les définitions de fonction inline de module dans un fichier distinct").Module.Test
. La déclaration
du code de test en tant que classe amie facilite le développement indépendant
du module et son code de test. Elle permet également au code de test d'être omis
par le code objet du module final sans modification de la source.SymaNetwork.hh // Contains the declaration for a // class named "SymaNetwork". SymaNetwork.Inlines.cc // Inline definitions sub-unit SymaNetwork.cc // Module's main implementation unit SymaNetwork.Private.cc // Private implementation sub-unit SymaNetwork.Test.cc // Test code sub-unit
La séparation de la spécification de module de son implémentation facilite le développement indépendant du code fournisseur et utilisateur.
Le portionnement de l'implémentation d'un module en plusieurs unités de translation fournit une meilleur prise en charge pour le retrait du code objet, et résulte en de plus petites tailles d'éléments exécutables.
L'utilisation d'une convention prévisible et régulière de partitionnement et de désignation de fichier permet à l'organisation et au contenu du module d'être compris sans inspection de ses contenus.
La transmission de noms du code vers le nom de fichier accroît la prévisibilité et facilite la construction d'outils basés sur le fichier, sans nécessiter une correspondance de nom complexe [Ellemtel, 1993].
Les extensions de nom de fichier communément utilisées sont : .h, .H, .hh, .hpp
et .hxx
pour les fichiers d'en-tête ; et .c, .C, .cc, .cpp
et
.cxx
pour les implémentations. Sélectionnez un ensemble d'extensions et utilisez-les
systématiquement.
SymaNetwork.hh // The extension ".hh" used to designate // a "SymaNetwork" module header. SymaNetwork.cc // The extension ".cc" used to designate // a "SymaNetwork" module implementation.
Le document de travail du projet de norme C++ utilise également l'extension ".ns" pour les en-têtes encapsulées par un espace de nom.
Sauf cas exceptionnel, plusieurs classes ne doivent pas être rassemblées dans un module. Si c'est nécessaire, elles doivent être étroitement associées (par exemple, un conteneur et son itérateur). Il est possible de placer une classe principale d'un module et ses classes de support dans le même fichier d'en-tête si toutes les classes doivent être toujours visibles par un module de client.
Réduit l'interface d'un module et tous les autres qui y sont dépendants.
A part les membres de classe privés, les déclarations propres à l'implémentation d'un module (par exemple les classes de support et les types d'implémentation) ne doivent pas apparaître dans la spécification du module. Ces déclarations doivent être placées dans les fichiers d'implémentation nécessaires, à moins que les déclarations soient requises par plusieurs fichiers d'implémentation. Dans ce cas, les déclarations doivent être placées dans un fichier d'en-tête privé secondaire. Le fichier d'en-tête privé secondaire doit alors être inclus par d'autres fichiers d'implémentation si nécessaire.
Cette méthode garantit que :
// Specification of module foo, contained in file "foo.hh" // class foo { .. declarations }; // End of "foo.hh" // Private declarations for module foo, contained in file // "foo.private.hh" and used by all foo implementation files. ... private declarations // End of "foo.private.hh" // Module foo implementation, contained in multiple files // "foo.x.cc" and "foo.y.cc" // File "foo.x.cc" // #include "foo.hh" // Include module's own header #include "foo.private.hh" // Include implementation // required declarations. ... definitions // End of "foo.x.cc" // File "foo.y.cc" // #include "foo.hh" #include "foo.private.hh" ... definitions // End of "foo.y.cc"
#include
pour avoir accès à la spécification d'un moduleUn module utilisant un autre module doit employer la directive de préprocesseur #include
pour pouvoir visualiser la spécification du module fournisseur.
De la même façon, les modules ne doivent jamais redéclarer d'élément de spécification d'un module fournisseur.
Lors de l'ajout de fichiers, utilisez uniquement la syntaxe #include <header>
pour les en-têtes "standards" et utilisez la syntaxe d'"en-tête" #include
pour les autres.
L'utilisation de la directive #include
s'applique également aux propres fichiers
d'implémentation d'un module : une implémentation de module doit inclure sa propre
spécification et ses en-têtes secondaires privés (voir "Placer les implémentations et les spécifications de module dans des fichiers distincts").
// The specification of module foo in its header file // "foo.hh" // class foo { ... declarations }; // End of "foo.hh" // The implementation of module foo in file "foo.cc" // #include "foo.hh" // The implementation includes its own // specification ... definitions for members of foo // End of "foo.cc"
Voici une exception à la règle #include
: lorsqu'un module utilise
ou contient uniquement les types d'un module fournisseur (classes) par référence (à l'aide des déclarations de type référence ou pointeur). Dans ce cas, le confinement ou l'utilisation par référence est spécifié à l'aide de la déclaration aval (voir également
"Réduire les dépendances de compilation") plutôt qu'une directive #include
.
Evitez d'inclure plus que ce qui est vraiment nécessaire : les en-têtes de module ne doivent donc pas inclure plus d'en-têtes que ce qui est requis par l'implémentation de module.
#include "a_supplier.hh" class needed_only_by_reference;// Use a forward declaration // for a class if we only need // a pointer or a reference // access to it. void operation_requiring_object(a_supplier required_supplier, ...); // // Operation requiring an actual supplier object; thus the // supplier specification has to be #included. void some_operation(needed_only_by_reference& a_reference, ...); // // Some operation needing only a reference to an object; thus // should use a forward declaration for the supplier.
Cette règle garantit que :
Lorsqu'un module a plusieurs fonctions inline, leurs définitions doivent être placées dans un fichier distinct réservé uniquement aux fonctions inline. Le fichier de fonction inline doit être inclus à la fin du fichier d'en-tête du module.
Voir également "Utiliser un symbole de compilation conditionnelle No_Inline
pour renverser la compilation inline".
Cette technique empêche les caractéristiques d'implémentation d'encombrer l'en-tête d'un module, ce qui permet de conserver une spécification saine. Elle aide également à réduire la réplication du code lorsqu'il n'y a pas de compilation inline : en utilisant la compilation conditionnelle, les fonctions inline peuvent être compilées dans un seul fichier objet, contrairement à la compilation statique dans chaque module d'utilisation. De la même manière, les définitions de fonction inline ne doivent pas être définies dans les définitions de classe, à moins qu'elles soient complètement évidentes.
Portionnez les grands modules en plusieurs unités de translation pour faciliter le retrait d'un code non référencé pendant la liaison du programme. Les fonctions membres qui sont rarement référencées doivent être séparées, en fichiers distincts, de celles qui sont communément utilisées. A la limite, des fonctions membres individuelles peuvent être placées dans leurs propres fichiers [Ellemtel, 1993].
Les éditeurs de liens ne sont pas tous capables d'éliminer un code non référencé dans un fichier objet de la même manière. Le portionnement des grands modules en de multiples fichiers permet aux éditeurs de liens de réduire les tailles des programmes exécutables en éliminant la liaison de fichiers objets entiers [Ellemtel, 1993].
Il est également intéressant de se demander d'abord si un module peut être portionné en plusieurs abstractions plus petites.
Séparez un code dépendant d'une plate-forme du code indépendant d'une plate-forme pour faciliter le portage. Les modules dépendants d'une plate-forme doivent avoir des noms de fichiers qualifiés par le nom de leur plate-forme, afin de souligner les dépendances de plates-formes.
SymaLowLevelStuff.hh // "LowLevelStuff" // specification SymaLowLevelStuff.SunOS54.cc // SunOS 5.4 implementation SymaLowLevelStuff.HPUX.cc // HP-UX implementation SymaLowLevelStuff.AIX.cc // AIX implementation
Du point de vue de l'architecture et de la maintenance, il est également approprié de rassembler les dépendances de plates-formes dans un nombre restreint de sous-systèmes de bas niveau.
Adoptez une structure de contenu de fichier standard et appliquez-la systématiquement
Une structure de contenu de fichier proposée est composée des éléments suivants, dans l'ordre spécifié :
1. Protection d'inclusion répétée (spécification uniquement).
2. Identification de gestion de version et de fichier (optionnelle).
3. Inclusions de fichiers requis par cette unité.
4. Documentation du module (spécification uniquement).
5. Déclarations (classe, type, constantes, objets et fonctions) et spécifications textuelles supplémentaires
(préconditions et postconditions, et invariants).
6. Inclusion des définitions de fonction inline de ce module.
7. Définitions (objets et fonctions) et déclarations propres à l'implémentation.
8. Notice de copyright.
9. Historique de contrôle des versions (optionnel).
L'ordre de contenu de fichier ci-dessus présente d'abord les informations pertinentes à propos du client, et est cohérent avec l'ordre des sections privées, protégées et publiques d'une classe.
Selon les règles de l'entreprise, les informations de copyright peuvent être placées au début du fichier.
L'inclusion et la compilation répétées de fichiers doivent être évitées en utilisant la construction suivante dans chaque fichier d'en-tête :
#if !defined(module_name) // Use preprocessor symbols to #define module_name // protect against repeated // inclusions... // Declarations go here #include "module_name.inlines.cc" // Optional inline // inclusion goes here. // No more declarations after inclusion of module's // inline functions. #endif // End of module_name.hh
Utilisez le nom de fichier de module pour le symbole de protection d'inclusion. Utilisez la même casse pour le symbole que pour le nom de module.
No_Inline
" pour renverser la compilation inlineUtilisez la construction de compilation conditionnelle suivante pour contrôler la compilation en ligne de la compilation hors ligne des fonctions inline :
// Au début de module_name.inlines.hh #if !defined(module_name_inlines) #define module_name_inlines #if defined(No_Inline) #define inline // Annule le mot clé inline #endif ... // Les définitions inline vont ici #endif // End of module_name.inlines.hh // At the end of module_name.hh // #if !defined(No_Inline) #include "module_name.inlines.hh" #endif // At the top of module_name.cc after inclusion of // module_name.hh // #if defined(No_Inline) #include "module_name.inlines.hh" #endif
La construction de compilation conditionnelle est similaire à la construction de protection d'inclusion multiple. Si le symbole No_Inline
n'est pas défini, alors les fonctions inline sont compilées avec la spécification du module et automatiquement exclues de l'implémentation du module. Si le symbole No_Inline
est défini, alors les définitions inline sont exclues de la spécification du module mais incluses dans l'implémentation du module avec le mot de passe inline
annulé.
La technique ci-dessus autorise la réplication d'un code réduit lorsque les fonctions inline sont compilées hors ligne. En utilisant la compilation conditionnelle, une seule copie des fonctions inline est compilée dans le module de définition. Par opposition au code répliqué, elles sont compilées comme fonctions "statiques" (lien interne) dans chaque module d'utilisation où la compilation hors ligne est spécifiée par un compilateur switch.
L'utilisation de la compilation conditionnelle accroît la difficulté de gestion des dépendances de construction. Cette complexité est gérée en traitant toujours les définition des fonctions inline et des en-têtes comme une seule unité logique : les fichiers d'implémentation sont alors dépendants des fichiers de définition des fonctions inline et des en-têtes.
Le retrait uniforme devrait être utilisé pour délimiter visuellement les instructions imbriquées. Il a été prouvé qu'un retrait de 2 à 4 espaces est plus efficace visuellement parlant, dans ce cas. Nous recommandons l'utilisation d'un retrait classique de 2 espaces.
Les délimiteurs d'instruction de bloc ou de composant ({}
) doivent être au même niveau que les instructions environnantes (par implication, cela signifie que les caractères {}
sont alignés verticalement). Les instructions au sein du bloc doivent être mises en retrait avec un nombre d'espaces choisi.
Les étiquettes cases d'une instruction switch
doivent se situer au même niveau d'indentation que l'instruction switch
. Les instructions dans l'instruction switch
peuvent alors être mises en retrait par un niveau 1 de retrait à partir de l'instruction switch
elle-même et des étiquettes cases.
if (true) { // New block foo(); // Statement(s) within block // indented by 2 spaces. } else { bar(); } while (expression) { statement(); } switch (i) { case 1: do_something();// Statements indented by // 1 indentation level from break; // the switch statement itself. case 2: //... default: //... }
Un retrait de 2 espaces est un compromis entre permettre une reconnaissance aisée des blocs, et permettre suffisamment de blocs imbriqués avant que le code ne se décale de trop par rapport au côté droit d'un écran d'affichage ou d'une page d'impression.
Si une déclaration de fonction ne tient pas sur une seule ligne, placez alors le premier paramètre sur la même ligne que le nom de fonction, et chaque paramètre suivant sur une nouvelle ligne, en retrait, au même niveau que le premier paramètre. Ce style de déclaration et de mise en retrait, présenté ci-dessous, laisse des blancs sous le nom et le type de retour de fonction, ce qui améliore alors leur visibilité.
void foo::function_decl( some_type first_parameter, some_other_type second_parameter, status_type and_subsequent);
Si cette instruction provoque un renvoi à la ligne, ou des paramètres trop mis en retrait, mettez alors tous les paramètres en retrait à partir du nom de fonction ou du nom de portée (classe, espace de nom), chacun sur une ligne distincte :
void foo::function_with_a_long_name( // function name is much less visible some_type first_parameter, some_other_type second_parameter, status_type and_subsequent);
Voir également les règles d'alignement ci-dessous.
La longueur maximale des lignes de programme doit être limitée pour éviter la perte d'informations lors des impressions sur une taille de papier standard (lettre) ou par défaut.
Si le niveau de retrait provoque un décalage trop important des instructions imbriquées vers la droite, et que les instructions s'étendent au-delà de la marge de droite, il est probablement temps de portionner le code en fonctions plus petites et plus gérables.
Lorsque les listes des paramètres des appels, des définitions, des énumérateurs ou des déclarations de fonctions dans les déclarations enum ne tiennent pas sur une seule ligne, faire un retour à la ligne après chaque élément de la liste et placez chaque élément sur une ligne distincte (voir également "Mettre en retrait les paramètres de fonction à partir du nom de fonction ou du nom de portée").
enum color { red, orange, yellow, green, //... violet };
Si une déclaration de canevas de fonction ou de classe est trop longue, compactez-la en lignes consécutives après la liste d'argument canevas. Par exemple (déclaration de la bibliothèque d'itérateurs standards, [X3J16, 95]) :
template <class InputIterator, class Distance> void advance(InputIterator& i, Distance n);
Ce chapitre vous conseille sur l'utilisation de commentaires dans le code.
Les commentaires doivent servir à compléter le code source, jamais à le paraphraser :
Pour chaque commentaire, le programmeur doit être capable de répondre facilement à la question suivante : "Quelle est l'information ajoutée par ce commentaire ?". Généralement, les noms choisis judicieusement ne nécessitent pas de commentaires. Les commentaires, à moins qu'ils ne participent à un langage de conception de programme, ne sont pas gérés par le compilateur. Aussi, conformément au principe du point de maintenance unique, les décisions de conception doivent être exprimées dans le code source plutôt que dans les commentaires, même si cela doit coûter quelques déclarations supplémentaires.
Il est préférable d'utiliser les délimiteurs de commentaires de style C++ "//
" que ceux de style C "/*...*/
".
Les commentaires de style C++ sont plus visibles et réduisent le risque de commenter par mégarde de grandes parties du code à cause de l'absence d'un délimiteur de fin de commentaire.
/* start of comment with missing end-of-comment delimiter do_something(); do_something_else(); /* Comment about do_something_else */ // End of comment is here ---> */ // Both do_something and // do_something_else // are accidentally commented out! Do_further();
Les commentaires doivent être situés à côté du code qu'ils commentent, avec le même niveau de mise en retrait, et ils doivent être liés au code par une ligne de commande vide.
Les commentaires s'appliquant à des instructions sources successives et multiples doivent être placés au-dessus des instructions. Ils servent ainsi d'introduction aux instructions. De même, les commentaires associés à des instructions individuelles doivent être placés en-dessous des instructions.
// A pre-statements comment applicable // to a number of following statements // ... void function(); // // A post-statement comment for // the preceding statement.
Evitez les commentaires sur la même ligne qu'une construction source : ils se décalent souvent. De tels commentaires sont toutefois tolérés pour les descriptions d'éléments dans de longues déclarations, telles que les expressions énumératives dans une déclaration enum.
Evitez les en-têtes contenant des informations du type auteur, numéros de téléphone, dates de création et de modification : auteur et numéros de téléphone deviennent rapidement des informations obsolètes ; les dates de création et de modification et les raisons des modifications sont par contre mieux conservées par un outil de gestion de configuration (ou quelque autre formulaire de fichier historique de version).
Evitez l'utilisation de barres verticales, de cadres ou de boîtes fermés, même pour les constructions principales (telles que les fonctions et les classes). Elles ne font qu'ajouter une nuisance visuelle et restent difficilement uniformes. Utilisez des interlignes plutôt que des lignes de commentaires lourdes pour séparer les blocs de code source associés. Utilisez un seul interligne pour séparer les constructions dans les fonctions ou les classes. Utilisez des interlignes doubles pour séparer les fonctions les unes des autres.
Les cadres et les formats peuvent sembler participer à l'uniformité, et rappeler aux programmeurs de documenter le code, mais ils conduisent souvent à l'utilisation de paraphrases [Kruchten, 94].
Utilisez des commentaires vides plutôt que des lignes vides au sein d'un bloc de commentaire pour séparer les paragraphes
// Some explanation here needs to be continued // in a subsequent paragraph. // // The empty comment line above makes it // clear that this is another // paragraph of the same comment block.
Evitez la répétition d'identificateurs de programme dans les commentaires, et la reproduction des informations trouvées ailleurs. Fournissez plutôt un pointeur désignant les informations. Sinon, toute modification de programme peut nécessiter de la maintenance à plusieurs endroits. Si les modifications de commentaire requises ne sont pas effectuées partout, cela va entraîner des commentaires faux ou trompeurs, ce qui est encore pire que de ne pas avoir de commentaires du tout.
Faites toujours en sorte d'écrire un code autodocumenté plutôt que de fournir des commentaires. Par exemple, choisissez de meilleurs noms, utilisez des variables temporaires supplémentaires ou restructurez le code. Attention au style, à la syntaxe et à l'orthographe des commentaires. Utilisez un langage normal, plutôt qu'un style télégraphique ou crypté.
do { ... } while (string_utility.locate(ch, str) != 0); // Exit search loop when found it.par :
do { ... found_it = (string_utility.locate(ch, str) == 0); } while (!found_it);
Même si un code autodocumenté est préférable aux commentaires, il est généralement nécessaire de fournir des informations en plus d'une explication des éléments compliqués du code. Il faut au moins documenter :
La documentation du code ainsi que les déclarations doivent suffire pour permettre à un client
d'utiliser le code. La documentation est requise puisque la sémantique complète des classes, des fonctions, des types et des objets ne peut pas être entièrement exprimée avec le C++ seul.
Ce chapitre fournit des conseils sur le choix des noms pour diverses entités C++.
Il n'est pas aisé de trouver de bons noms aux entités des programmes (classes, fonctions, types, objets, littéraux, exceptions, espaces de nom). Pour les applications de taille moyenne à grande, le problème est encore plus épineux : les incompatibilités de nom et le manque de synonymes pour désigner des concepts distincts mais similaires accroissent le degré de difficulté.
L'utilisation d'une convention de dénomination peut réduire l'effort requis pour inventer des noms appropriés. En plus de cet avantage, une convention de dénomination assure la cohérence du code. Pour être utile, une convention de dénomination doit fournir des conseils sur : le style typographique (ou comment écrire les noms), et la construction du nom (ou comment choisir les noms).
N'importe quelle convention de dénomination peut être utilisée, du moment qu'elle est appliquée systématiquement. L'uniformité des désignations est bien plus importante que la convention utilisée car elle appuie le principe de moindre surprise.
Etant donné que C++ est un langage sensible à la casse et que de nombreuses conventions de dénominations distinctes sont couramment utilisées par la communauté du C++, il est difficile de parvenir à une cohérence absolue des désignations. Afin d'optimiser la cohérence du code, nous vous recommandons de choisir, pour votre projet, une convention de dénomination basée sur l'environnement hôte (par exemple UNIX ou Windows) et les bibliothèques de principes utilisées par le projet :
Le lecteur avisé observera que les exemples de ce texte ne suivent pas toutes les instructions. Cela est en partie dû au fait que les exemples sont dérivés de plusieurs sources et également au souhait d'économiser du papier. Ainsi, les instructions de mise en forme n'ont pas été appliquées méticuleusement. Mais le message est "faites ce que je dis, pas ce que je fais".
Les noms avec un seul trait de soulignement à gauche ('_') sont souvent utilisés par les fonctions de bibliothèque ("_main
" et "_exit
").
Les noms avec un double trait de soulignement à gauche ("__"), ou un seul trait de soulignement à gauche suivi d'une majuscule sont réservés à l'utilisation interne du compilateur.
Evitez également les noms ayant des traits de soulignement continus car il est souvent difficile de discerner le nombre exacte de traits de soulignement.
Il est difficile de se souvenir des différences entre les noms de types qui diffèrent uniquement par leur casse, la confusion est courante.
Les abréviations peuvent être utilisées si elles sont couramment utilisées dans le domaine d'application (par exemple, FFT pour Fast Fourier Transform), ou si elles sont répertoriées dans une liste d'abréviations reconnues par le projet. Sinon, il est très probable que des abréviations similaires mais pas tout à fait identiques surviennent ici et là, entraînant de la confusion et des erreurs par la suite (par exemple, track_identification abrégé en trid, trck_id, tr_iden, tid, tr_ident et ainsi de suite).
L'utilisation de suffixes pour catégoriser les entités (comme type pour le type et erreur pour les exceptions) n'est généralement pas très efficace pour la compréhension du code. Les suffixes tels que tableau et struct impliquent également une implémentation spécifique qui, dans le cas d'une modification de l'implémentation (c'est-à-dire une modification de la représentation d'une struct ou d'un tableau) aurait un effet secondaire sur le code client, ou serait trompeuse.
Les suffixes peuvent toutefois être utiles dans quelques situations :
Choisissez des noms liés à l'utilisation, et utilisez des adjectifs avec des noms pour accentuer la signification locale (spécifique au contexte). En outre, assurez-vous que les noms s'accordent avec leurs types.
Choisissez des noms pour faire en sorte que les constructions telles que :
object_name.function_name(...); object_name->function_name(...);
soient faciles à lire et aient du sens.
La rapidité à taper n'est pas une justification valable pour utiliser des noms courts ou abrégés. Les identificateurs courts et composés d'une lettre sont souvent révélateurs d'un mauvais choix ou d'une paresse. Les exceptions sont les instances facilement reconnaissables comme l'utilisation du E pour la base des logarithmes naturels ou Pi.
Malheureusement, les compilateurs et les outils de support limitent parfois la longueur des noms. Aussi, assurez-vous que les mots longs ne diffèrent pas seulement par leurs caractères de fin : les caractères de différenciation peuvent être tronqués par ces outils.
void set_color(color new_color) { ... the_color = new_color; ... }est mieux que :
void set_foreground_color(color fg)et :
oid set_foreground_color(color foreground);{ ... the_foreground_color = foreground; ... }
La désignation du premier exemple est supérieure aux deux autres : new_color
est qualifié et s'accorde avec son type, ce qui renforce la sémantique de la fonction.
Dans le second cas, le lecteur intuitif pourrait déduire que fg
signifie avant-plan. Cependant, dans tout bon style de programmation, il ne faut laisser aucune place à l'intuition ou à la déduction.
Dans le troisième cas, lorsque le paramètre foreground
est utilisé (en dehors de
sa déclaration), le lecteur peut penser que foreground
signifie en fait
couleur d'avant-plan. Ce serait toutefois concevable s'il avait implicitement convertible à color
.
Former de noms à partir de substantifs et d'adjectifs, et s'assurer que les noms s'accordent avec leurs types correspond à un langage naturel, et accroît la sémantique et la lisibilité du code.
Les parties de noms qui sont des mots anglais doivent être orthographiés correctement et conformes à la forme requise du projet, c'est-à-dire en anglais ou en américain, mais pas les deux. C'est également vrai pour les commentaires.
Concernant les arguments de fonction, les fonctions et les objets booléens, utilisez une clause de prédicat à la forme positive ; par exemple, found_it
, is_available
, mais pas is_not_available
.
Lors de l'inversion des prédicats, les doubles négations sont plus difficiles à comprendre.
Si un système est décomposé de sous-systèmes, utilisez les noms des sous-systèmes comme noms d'espaces de noms pour le partitionnement et la réduction de l'espace de nom complet du système. Si le système est une bibliothèque, utilisez un seul espace de nom du plus haut niveau pour la bibliothèque entière.
Donnez à chaque espace de nom de la bibliothèque ou du sous-système, un nom ayant un sens, ainsi qu'un alias abrégé ou un acronyme. Choisissez des alias abrégés ou des acronymes peu enclins à entrer en conflit. Par exemple, la bibliothèque de projet de norme C++ ANSI [Plauger, 95] définit std
comme alias de iso_standard_library
.
Si le compilateur ne prend pas encore en charge la construction des espaces de nom, utilisez des préfixes de nom pour simuler les espaces de nom. Par exemple, les noms publiques dans l'interface d'un sous-système de gestion de système peuvent être préfixés par syma (raccourci de System Management).
L'utilisation d'espaces de nom pour entourer des noms globaux permet d'éviter les conflit de noms lorsque le code est développé de manière indépendante (par des vendeurs ou des équipes de sous-projet). Résultat, seuls les noms des espaces de nom sont globaux.
Utilisez un nom commun ou un syntagme nominal, au singulier, pour donner à la classe un nom exprimant son abstraction. Utilisez des noms plus généraux pour les classes de base et des noms plus spécialisés pour les classes dérivées.
typedef ... reference; // From the standard library typedef ... pointer; // From the standard library typedef ... iterator; // From the standard library class bank_account {...}; class savings_account : public bank_account {...}; class checking_account : public bank_account {...};
Lorsqu'il y a une incompatibilité ou une pénurie de noms appropriés pour les objets et
les types, utilisez le nom simple de l'objet, et ajoutez un suffixe tel que mode,
kind, code
et ainsi de suite pour le nom de type.
Utilisez le pluriel pour exprimer une abstraction représentant un ensemble d'objets.
typedef some_container<...> yellow_pages;
Lorsqu'une sémantique supplémentaire est nécessaire au-delà d'un simple ensemble d'objets, à partir de la bibliothèque standard et comme modèles de comportement et suffixes de noms, utilisez :
Utilisez des verbes ou des phrases actives pour les fonctions sans valeurs de retour (déclarations de fonction avec un type de retour vide) ou pour les fonctions qui retournent les valeurs par paramètres de pointeur ou de référence.
Utilisez des substantifs pour les fonctions qui retournent une seule valeur par type de retour de fonction non vide.
Pour les classes ayant des opérations communes (un modèle de comportement), utilisez des noms d'opération tirés d'une liste de choix sur le projet. Par exemple: begin, end, insert, erase (opérations de conteneur dans la bibliothèque standard).
Evitez de préfixer les fonctions avec "get" et "set", particulièrement pour les opérations publiques de chargement et de définition d'attributs d'objets. La désignation d'opération doit rester au niveau de l'abstraction de classe et de l'offre de service. Les attributs d'objet de modification et d'obtention sont des détails d'implémentation de bas niveau qui affaiblissent l'encapsulation si elle set définie sur publique.
Utilisez des adjectifs (ou des participes passés) pour les fonctions renvoyant un booléen (prédicats). Concernant les prédicats, il est souvent utile d'ajouter le préfixe is ou has avant un nom pour que le nom soit lu comme une assertion positive. C'est également utile lorsque le nom simple est déjà utilisé pour un objet, un nom de type ou un littéral d'énumération. Soyez précis et cohérent par rapport au temps.
void insert(...); void erase(...); Name first_name(); bool has_first_name(); bool is_found(); bool is_available();
N'utilisez pas de noms négatifs car cela peut conduire à des expressions contenant une double
négation (par exemple, !is_not_found
), ce qui rend le code encore plus difficile à comprendre. Dans certains cas, un prédicat négatif peut également devenir positif sans modifier sa sémantique, en utilisant un antonyme, tel que "is_invalid
" au lieu de "is_not_valid
".
bool is_not_valid(...); void find_client(name with_the_name, bool& not_found);Doit être redéfini par :
bool is_valid(...); void find_client(name with_the_name, bool& found);
Lorsque des opérations ont le même objectif, utilisez la surcharge plutôt que de rechercher des synonymes : cela réduit le nombre de concepts et de variations d'opérations dans le système, et décroît ainsi sa complexité générale.
Lors de la surcharge d'opérateurs, assurez-vous que les sémantiques de l'opérateur sont conservées. Si la signification conventionnelle d'un opérateur ne peut pas être conservée, choisissez un autre nom à la fonction plutôt que de surcharger l'opérateur.
Pour spécifier son caractère unique ou pour montrer que cette entité est le point principal de
l'action, ajoutez un préfixe au nom du paramètre ou de l'objet avec "the
" ou
"this
". Pour indiquer un objet secondaire, temporaire, auxiliaire, préfixez-le avec
"a
" ou "current
" :
void change_name( subscriber& the_subscriber, const subscriber::name new_name) { ... the_subscriber.name = new_name; ... } void update(subscriber_list& the_list, const subscriber::identification with_id, structure& on_structure, const value for_value); void change(object& the_object, const object using_object);
Puisque les exceptions doivent être utilisées uniquement pour traiter des situations d'erreurs, utilisez un substantif ou un syntagme nominal véhiculant clairement une idée négative :
overflow, threshold_exceeded, bad_initial_value
Utilisez l'un des mots tels que bad, incomplete, invalid, wrong, missing ou illegal, dans une liste convenue sur le projet, comme partie du nom plutôt que d'utiliser systématiquement error ou exception, qui ne véhiculent pas d'information précise.
La lettre 'E' dans les littéraux à virgule flottante et les chiffres hexadécimaux de 'A' à 'F' doivent toujours être en majuscule.
Ce chapitre donne des conseils sur l'utilisation et la forme de divers types de déclaration en langage C++.
Avant l'existence de la fonction espace de nom du langage C++, il n'y avait que des moyens limités pour gérer la portée d'un nom. Par conséquent, le nom d'espace global est plutôt devenu surpeuplé, ce qui a conduit à des conflits empêchant l'utilisation conjointe de certaines bibliothèques dans le même programme. La nouvelle fonction de langage d'espace de nom résout le problème de la pollution des espaces de nom globaux.
Cela signifie que seuls les noms des espaces de nom peuvent être globaux, toutes les autres déclarations doit se situer dans la portée de quelques espaces de nom.
Si vous ignorez cette règle, des conflits de noms peuvent se produirent.
Pour le regroupement logique de fonctionnalité sans classe (comme une catégorie de classe), ou pour une fonctionnalité avec une plus grande portée qu'une classe, telle qu'une bibliothèque ou un sous-système, utilisez un nom d'espace pour unifier les déclarations de manière logique (voir "Utiliser les espaces de nom pour diviser les noms complets potentiels par sous-systèmes ou par bibliothèques").
Exprimez le regroupement logique de fonctionnalité dans le nom.
namespace transport_layer_interface { /* ... */ }; namespace math_definitions { /* ... */ };
L'utilisation de données de portée globale ou d'espace de nom est contraire au principe d'encapsulation.
Les classes représentent l'unité d'implémentation et de conception fondamentales du C++. Elles doivent être utilisées pour enregistrer les abstractions de domaine et de conception, et, en tant que mécanisme d'encapsulation pour l'implémentation des types de données abstraits (ADT).
classe
plutôt que struct
pour l'implémentation de
types de données abstraitsUtilisez la classe de clé class
plutôt que struct
pour
l'implémentation d'une classe (un type de données abstrait).
Utilisez la classe de clé struct
pour la définition de structures de données ordinaires
(POD) comme dans C, particulièrement lors de l'interfaçage avec le code C.
Même si class
et struct
sont équivalentes et interchangeables,
class
a une mise en évidence de contrôle d'accès implicite
(privé) permettant une meilleure encapsulation.
Adopter une pratique cohérente pour distinguer class
de struct
entraîne une distinction sémantique au-delà des règles du langage : class
devient la construction la plus importante pour enregistrer les abstractions et l'encapsulation tandis que struct
représente une structure de données pure que l'on peut échanger dans les programmes combinés de langage de programmation.
Les spécificateurs d'accès dans une déclaration de classe doivent apparaître dans cet ordre : publique, protégé, privé.
L'ordre publique, protégé, privé des déclarations de membre garantit que l'information la plus intéressante pour l'utilisateur de classe est présentée en premier, ce qui réduit son besoin de naviguer dans des détails soit hors de propos, soit d'implémentation.
L'utilisation de membres de données protégés ou publiques réduit l'encapsulation d'une classe et affecte l'endurance du système face au changement : les membres de données publiques exposent l'implémentation d'une classe à ses utilisateurs ; les membres de données protégés exposent l'implémentation d'une classe à ses classes dérivées. Toute modification apportée aux membres de données publiques ou protégées des classes aura des conséquences sur les utilisateurs et les classes dérivées.
A première vue, cette instruction semble être contre les décisions intuitives : l'amitié expose des parties privées à des amis, alors comment peut-elle préserver l'encapsulation ? Dans les situations où les classes sont très interdépendantes et requièrent une gestion des connaissances internes de chacune, il vaut mieux octroyer l'amitié plutôt que d'exporter les caractéristiques internes via l'interface de classe.
L'exportation des caractéristiques internes en tant que membres publiques donne accès aux clients de classe, ce qui n'est pas souhaitable. L'exportation de membres protégés donne accès à des descendants potentiels, encourageant une conception hiérarchique, ce qui n'est pas souhaitable non plus. L'amitié octroie un accès privé sélectif sans imposer de contrainte de sous-classification, préservant ainsi l'encapsulation de tout ce qui ne requiert pas d'accès.
L'octroi d'une amitié à une classe de test amie est un bon exemple d'utilisation de l'amitié pour préserver l'encapsulation. La classe de test amie, en voyant les structures internes de la classe, peut implémenter le code test approprié, mais par la suite, la classe de test amie peut être supprimée du code distribué. Ainsi, aucune encapsulation n'est ni perdue, ni codée et incluse dans le code distribué.
Les déclarations de classes doivent contenir uniquement des déclarations de fonctions, jamais des définitions de fonctions (implémentations).
L'ajout de définitions de fonctions dans une déclaration de classe corrompt la spécification de classe avec des caractéristiques d'implémentation, ce qui rend l'interface de classe moins décelable, plus difficile à lire et accroît les dépendances de compilation.
Les définitions de fonction dans les déclarations de classe réduisent également le contrôle sur la fonction inline (voir également "Utiliser un symbole de compilation conditionnelle No_Inline
pour renverser la compilation inline").
Afin de permettre l'utilisation d'une classe dans un tableau, ou dans n'importe lequel des conteneurs STL, une classe doit fournir un constructeur publique par défaut, ou permettre au compilateur d'en générer un.
L'exception à la règle ci-dessus intervient lorsqu'une classe a un membre de données non statique de type référence. Dans ce cas, il est souvent impossible de créer un constructeur par défaut qui ait du sens. L'utilisation d'une référence pour un membre de donnée objet est donc discutable.
Le compilateur génère implicitement un constructeur de copie et un opérateur d'affectation à une classe si c'est nécessaire et que ce n'est pas déclaré de manière explicite. Le compilateur définit les constructeurs de copie et l'opérateur d'affectation implémente ce que l'on appelle couramment dans la terminologie Smalltalk ";copie superficielle", soit copie au niveau des membres avec copie au niveau du bit pour les pointeurs. L'utilisation d'opérateurs d'affectation par défaut et d'un constructeur de copie généré par le compilateur entraîne toujours des fuites de mémoire.
// Adapté à partir de [Meyers, 92]. void f() { String hello("Hello"); // Assume String is implemented // with a pointer to a char // array. { // Enter new scope (block) String world("World"); world = hello; // Assignment loses world's // original memory } // Destruct world upon exit from // block; // also indirectly hello String hello2 = hello; // Assign destructed hello to // hello2 }
Dans le code ci-dessus, la mémoire contenant la chaîne "World
"
est perdue après l'affectation. En sortant du bloc intérieur, world
est détruit. La mémoire référencée par hello
est alors également perdue. Le
hello
détruit est attribué à hello2
.
// Adapté de [Meyers, 1992]. void foo(String bar) {}; void f() { String lost = "String that will be lost!"; foo(lost); }
Dans le code ci-dessus, lorsque foo
est appelé avec l'argument lost
,
lost
est copié dans foo
à l'aide du constructeur de copie défini par le compilateur. Puisque lost
est copié avec une copie bit par bit du pointeur désignant
"String that will be lost!"
, à la sortie de
foo
, la copie de lost
sera détruite (en supposant
que le destructeur est implémenté correctement pour libérer de la mémoire) ainsi que la
mémoire contenant "String that will be lost!"
// Example from [X3J16, 95; section 12.8] class X { public: X(const X&, int); // int parameter is not // initialized // No user-declared copy constructor, thus // compiler implicitly declares one. }; // Deferred initialization of the int parameter mutates // constructor into a copy constructor. // X::X(const X& x, int i = 0) { ... }
Un compilateur ne voyant pas de signature de constructeur de copie "standard" dans une déclaration de classe va implicitement déclarer un constructeur de copie. Une initialisation différée des paramètres par défaut peut toutefois transformer un constructeur en constructeur de copie, conduisant à une ambiguïté lorsqu'un constructeur de copie est utilisé. Toute utilisation d'un constructeur de copie est donc gênée par cette ambiguïté [X3J16, 95; section 12.8].
A moins qu'une classe soit explicitement désignée comme non dérivable, son destructeur doit toujours être déclaré virtuel.
La suppression d'un objet d'une classe dérivée via un pointeur ou une référence à un type de classe de base va entraîner un comportement non défini, à moins que le destructeur de la classe de base soit déclaré virtuel.
// Bad style used for brevity class B { public: B(size_t size) { tp = new T[size]; } ~B() { delete [] tp; tp = 0; } //... private: T* tp; }; class D : public B { public: D(size_t size) : B(size) {} ~D() {} //... }; void f() { B* bp = new D(10); delete bp; // Undefined behavior due to // non-virtual base class // destructor }
Vous pouvez empêcher l'utilisation de constructeurs de paramètres uniques pour la conversion implicite en les déclarant avec le spécificateur explicit
.
Les fonction non virtuelles implémentent un comportement non variant et ne sont pas destinées à être spécialisées par des classes dérivées. Le non respect de cette instruction peut produire un comportement imprévu : le même objet peut déployer un comportement différent à des moments différents.
Les fonctions non virtuelles sont statiquement liées. Aussi, la fonction appelée sur un objet est dirigée par le type statique de la variable référençant le pointeur désignant A et le pointeur désignant B de l'objet, comme dans l'exemple ci dessous, et pas par le type actuel de l'objet.
// Adapté à partir de [Meyers, 92]. class A { public: oid f(); // Non-virtual: statically bound }; class B : public A { public: void f(); // Non-virtual: statically bound }; void g() { B x; A* pA = &x; // Static type: pointer-to-A B* pB = &x; // Static type: pointer-to-B pA->f(); // Calls A::f pB->f(); // Calls B::f }
Etant donné que les fonctions non virtuelles contraignent les sous-classes en restreignant la spécialisation et le polymorphisme, assurez-vous qu'une opération est vraiment non variante pour toutes les sous-classes avant de la déclarer non virtuelle.
L'initialisation de l'état d'un objet lors de la construction doit être exécutée par un constructeur-initialiseur (une liste d'initialiseurs membres) plutôt qu'avec des opérateurs d'affectation dans le corps du constructeur.
class X { public: X(); private Y the_y; }; X::X() : the_y(some_y_expression) { } // // "the_y" initialized by a constructor-initializerPlutôt que :
X::X() { the_y = some_y_expression; } // // "the_y" initialized by an assignment operator.
La construction d'un objet implique celle de toutes les classes de base et membres de données avant l'exécution du corps du constructeur. L'initialisation des membres de données requiert deux opérations (la construction et l'affectation), si elle est exécutée dans un corps de constructeur, au lieu d'une (construction avec une valeur initiale) lorsqu'elle est exécutée à l'aide d'un constructeur-initialiseur.
Pour les classes d'agrégation imbriquées (classes contenant des classes contenant des classes...), la surcharge de performance de nombreuses opérations (construction + affectation de membre) peut être considérable.
class A { public: A(int an_int); }; class B : public A { public: int f(); B(); }; B::B() : A(f()) {} // undefined: calls member function but A bas // not yet been initialized [X3J16, 95].
Le résultat d'une opération est non défini si une fonction membre est appelée directement ou indirectement par un constructeur initialiseur avant que tous les initialiseurs des membres des classes de base soient exécutés [X3J16, 95].
Faites attention lors de l'appel de fonctions membres dans les constructeurs. Sachez que même si une fonction virtuelle est appelée, celle qui est exécutée est celle qui est choisie dans la classe du constructeur ou du destructeur, ou l'une de ses classes de base.
static const
pour les constantes de classes entièresLors de la définition de constantes de classes entières (entier), utilisez les membres de données static const
plutôt que #define
ou les constantes globales. Si static
const
n'est pas pris en charge par le compilateur, utilisez plutôt enum
.
class X { static const buffer_size = 100; char buffer[buffer_size]; }; static const buffer_size;Ou :
class C { enum { buffer_size = 100 }; char buffer[buffer_size]; };Mais pas :
#define BUFFER_SIZE 100 class C { char buffer[BUFFER_SIZE]; };
Cela permet d'éviter les confusions lorsque le compilateur se plaint d'un type de retour manquant pour les fonctions déclarées sans type de retour explicite.
Utilisez également ces mêmes noms dans les définitions et les déclaration de fonction, cela réduit les surprises. Le fait de fournir des noms de paramètres améliore la lisibilité et la documentation du code.
Les instructions de retour dispersées dans un corps de fonction sont analogues aux instructions goto
,
rendant le code plus difficile à lire et à gérer.
Des retours multiples sont tolérés uniquement dans de très petites fonctions, lorsque tous les retour
s peuvent être vus simultanément et que la structure du code est très équilibrée :
type_t foo() { if (this_condition) return this_value; else return some_other_value; }
Les fonctions avec un type de retour void ne doivent pas avoir d'instruction de retour.
La création de fonctions produisant des effets secondaires communs (modification de données non recommandées autre que leur état d'objet interne : comme les données globales ou d'espace de nom) doit être réduite (voir également "Réduire l'utilisation de données de portée globale ou d'espace de nom"). Mais si c'est inévitable, tout effet secondaire doit alors être clairement documenté dans la spécification de fonction.
La transmission dans les objets requis en tant que paramètres rend le code moins dépendant au contexte, plus robuste et plus facile à comprendre.
L'ordre dans lequel les paramètres sont déclarés est important du point de vue du demandeur :
Ce classement permet de tirer parti des valeurs par défaut afin de réduire le nombre d'arguments dans les appels de fonction.
Le type des arguments de fonctions ayant un nombre variable de paramètres ne peut pas être vérifié.
Evitez d'ajouter des valeur par défaut aux fonctions dans les redéclarations suivantes de la fonction : à part les déclarations aval, une fonction ne doit être déclarée qu'une seule fois. Sinon, cela peut entraîner de la confusion chez les lecteurs qui ne connaissent pas les déclarations suivantes.
Vérifiez si les fonctions ont un comportement constant (retournent une valeur constante,
acceptent des arguments constants ou fonctionnent sans effet secondaire) et confirmez
le comportement à l'aide du spécificateur const
.
const T f(...); // Function returning a constant // object. T f(T* const arg); // Function taking a constant // pointer. // The pointed-to object can be // changed but not the pointer. T f(const T* arg); // Function taking a pointer to, and T f(const T& arg); // function taking a reference to a // constant object. The pointer can // change but not the pointed-to // object. T f(const T* const arg); // Function taking a constant // pointer to a constant object. // Neither the pointer nor pointed- // to object may change. T f(...) const; // Function without side-effect: // does not change its object state; // so can be applied to constant // objects.
La transmission et le retour d'objets par valeur peut conduire à une surcharge lourde de constructeur et de destructeur. Cette surcharge peut être évitée en transmettant et en retournant les objets par référence.
Les références const
peuvent être utilisées pour spécifier que les arguments transmis par référence ne peuvent pas être modifiés. Les constructeurs de copie et les opérateurs d'affectation sont des exemples typiques d'utilisation :
C::C(const C& aC); C& C::operator=(const C& aC);
Etudiez l'exemple suivant :
the_class the_class::return_by_value(the_class a_copy) { return a_copy; } the_class an_object; return_by_value(an_object);
Lorsque return_by_value
est appelé avec an_object
comme
argument, le constructeur de copie the_class
est appelé pour copier an_object
sur a_copy
. Le constructeur de copie the_class
est de nouveau appelé pour copier a_copy
sur l'objet temporaire de retour de fonction. Le destructeur the_class
est appelé pour détruire a_copy
sur le retour de la fonction. Un peu plus tard, le destructeur the_class
est de nouveau appelé pour détruire l'objet retourné par return_by_value
. Le coût total de l'appel de fonction inutile ci-dessus est de deux constructeurs et de deux destructeurs.
La situation est encore pire si the_class
est une classe dérivée
et contient des données de membres d'autres classes. Les constructeurs et les destructeurs des classes de bases et des classes contrôlées seraient également appelés, augmentant ainsi le nombre d'appels de constructeurs et de destructeurs engendrés par l'appel de fonction.
Les instructions ci-dessus semblent inviter les développeurs à transmettre et retourner toujours les objets par référence, mais il faut toutefois s'assurer de ne pas retourner de références objets locaux ou lorsque des objets sont requis. Le retour d'une référence à un objet local est une invitation au désastre puisque sur le retour de fonction, la référence renvoyée est liée à un objet détruit !
Les objets locaux sont détruits au moment du départ de la portée de fonction. L'utilisation d'objets détruits est une incitation au désastre.
Le non-respect de cette instruction conduit à des fuites de mémoire
class C { public: ... friend C& operator+( const C& left, const C& right); }; C& operator+(const C& left, const C& right) { C* new_c = new C(left..., right...); return *new_c; } C a, b, c, d; C sum; sum = a + b + c + d;
Puisque les résultats intermédiaires des opérateurs + ne sont pas stockés lors du calcul de la somme, les objets intermédiaires ne peuvent pas être annulés, ce qui entraîne des fuites de mémoire.
Le non respect de cette instruction viole l'encapsulation de données et peut conduire à de mauvaises surprises.
#define
pour l'expansion de macroMais utilisez la mise en ligne judicieusement : uniquement pour les fonctions très petites. La mise en ligne de fonctions importantes peut entraîner un gonflement du code.
Les fonctions inline augmentent également les dépendances de compilation entre les modules, puisque l'implémentation des fonctions inline doit être rendue disponible pour la compilation du code client.
[Meyers, 1992] explique clairement l'exemple suivant, plutôt extrême sur la mauvaise utilisation d'une macro :
A ne pas faire :
#define MAX(a, b) ((a) > (b) ? (a) : (b))
Mais plutôt :
inline int max(int a, int b) { return a > b ? a : b; }
La macro MAX
a plusieurs problèmes : son type n'est pas sûr et son comportement est non déterministe :
int a = 1, b = 0; MAX(a++, b); // a is incremented twice MAX(a++, b+10); // a is incremented once MAX(a, "Hello"); // comparing ints and pointers
Utilisez des paramètres par défaut plutôt que la surcharge de fonction lorsqu'un seul algorithme peut être exploité. Ce dernier peut être paramétré par un petit nombre de paramètres.
L'utilisation des paramètres par défaut aide à réduire le nombre de fonctions surchargées (ce qui améliore la maintenabilité) et réduit le nombre d'arguments requis dans les appels de fonction (ce qui améliore la lisibilité du code).
Utilisez la surcharge de fonction lorsque des implémentations multiples sont requise pour la même opération sémantique, mais avec différents types d'arguments.
Conservez la signification conventionnelle lors de la surcharge d'opérateurs. N'oubliez pas de définir les opérateurs associés, par exemple, operator==
et operator!=
.
Evitez de surcharger des fonctions avec un seul argument de pointeur par fonction avec un seul paramètre d'entier :
void f(char* p); void f(int i);
Les appels suivants peuvent provoquer des surprises :
f(NULL); f(0);
La résolution de la surcharge résout f(int)
et pas f(char*)
.
operator=
renvoie un référence à *this
Le C++ permet le chaînage des opérateurs d'affectation :
String x, y, z; x = y = z = "A string";
Puisque l'opérateur d'affectation est associatif à droite, la chaîne "A string
" est affectée à z, z à y, et y à x. operator=
est effectivement appelé une fois pour chaque expression, à droite de =, de droite à gauche. Cela signifie également que le résultat de chaque operator=
est un objet. Toutefois, le retour du côté gauche ou du côté droit est possible.
Puisque selon la méthode correcte, la signature de l'opérateur d'affectation doit toujours avoir ce format :
C& C::operator=(const C&);
seul l'objet le plus à gauche est possible (la partie droite est une référence const, et la partie gauche est une référence non const), aussi *this
doit être renvoyé. Reportez-vous à [Meyers,
1992] pour une explication détaillée.
operator=
pour l'auto-attributionVous avez deux bonnes raisons d'effectuer une vérification : tout d'abord, l'affectation d'un objet de classe dérivée implique l'appel d'un opérateur d'affectation de chaque classe de base sur la hiérarchie d'héritage, et l'abandon de ces opérations peut entraîner des sauvegardes d'exécution importantes. Deuxièmement, l'affectation entraîne la destruction de l'objet "lvalue" avant la copie de l'objet "rvalue". En cas d'affectation automatique, l'objet rvalue est détruit avant d'être affecté. Le résultat de l'affectation n'est donc pas défini.
N'écrivez pas de fonctions trop longues, par exemple, pas de plus de 60 lignes de code.
Réduisez le nombre d'instructions de retour, l'idéal est 1.
Recherchez une complexité cyclomatique inférieure à 10 (somme des instructions de décision + 1, pour les fonctions d'instruction de sortie simple).
Recherchez une complexité cyclomatique étendue inférieure à 15 (somme des instructions de décision + opérateurs logiques + 1, pour les fonctions d'instruction de sortie simple).
Réduisez la portée de référence maximale moyenne (distance en lignes entre la déclaration d'un objet local et la première instance de son utilisation).
Dans les grands projets, il y a généralement un ensemble de types fréquemment utilisés dans le système. Dans ce cas, il est pratique de rassembler ces types dans un ou plusieurs espaces de nom de fonctionnalité globale de bas niveau (voir l'exemple dans "Eviter l'utilisation de types fondamentaux").
Lorsque l'objectif est un fort degré de portabilité, ou lorsqu'un contrôle est nécessaire sur l'espace mémoire occupé par des objets numériques, ou encore lorsqu'une gamme spécifique de valeurs est requise, alors les types fondamentaux ne devraient pas être utilisés. Dans ces situations, il vaut mieux déclarer des noms de types explicites avec des contraintes de taille, à l'aide des types fondamentaux.
Assurez-vous que les types fondamentaux ne retournent pas dans le code par les compteurs de boucle, les indices de tableaux, et ainsi de suite.
namespace system_types { typedef unsigned char byte; typedef short int integer16; // 16-bit signed integer typedef int integer32; // 32-bit signed integer typedef unsigned short int natural16; // 16-bit unsigned integer typedef unsigned int natural32; // 32-bit unsigned integer ... }
La représentation des types fondamentaux est dépendante de l'implémentation.
typedef
pour créer des synonymes afin de renforcer la signification localeUtilisez typedef
pour créer des synonymes aux noms existants, afin de donner des noms locaux ayant plus de sens et d'améliorer la lisibilité (cela n'entraîne aucune pénalité d'exécution).
Vous pouvez également utiliser typedef
pour fournir des raccourcis aux noms qualifiés.
// vector declaration from standard library // namespace std { template <class T, class Alloc = allocator> class vector { public: typedef typename Alloc::types<T>reference reference; typedef typename Alloc::types<T>const_reference const_reference; typedef typename Alloc::types<T>pointer iterator; typedef typename Alloc::types<T>const_pointer const_iterator; ... } }
Lors de l'utilisation de noms typedef créés par typedef
, ne mélangez pas l'utilisation
du nom d'origine avec le synonyme dans le même élément de code.
Préférez l'utilisation de constantes nommées.
Utilisez plutôt const
ou enum
.
A ne pas faire :
#define LIGHT_SPEED 3E8Mais plutôt :
const int light_speed = 3E8;Ou ceci pour définir la taille des tableaux :
enum { small_buffer_size = 100, large_buffer_size = 1000 };
Le débogage est bien plus difficile car les noms introduits par #defines
sont remplacés lors du prétraitement de compilation, et ils n'apparaissent pas dans les tableaux de symboles.
Toujours initialiser les objets const au moment de la déclaration
Les objets const
qui ne sont pas déclarés extern
ont une liaison interne. L'initialisation de ces objets constants au moment de la déclaration permet d'utiliser des initialiseurs au moment de la compilation.
Les objets constants peuvent exister en mémoire morte.
Spécifiez les valeurs initiales dans les définitions d'objet, à moins que l'objet s'initialise automatiquement. S'il est impossible d'attribuer une valeur initiale qui ait du sens, attribuez alors une valeur "zéro" ou déclarez l'objet ultérieurement.
Pour les objets importants, il n'est généralement pas recommandé de construire des objets et de les initialiser ensuite à l'aide de l'affectation, car cela peut s'avérer très coûteux (voir également "Utiliser des constructeurs-initialiseurs plutôt que des affectations dans les constructeurs").
Si l'initialisation adéquate d'un objet est impossible au moment de la construction, initialisez alors l'objet à l'aide d'une valeur "zéro" signifiant "non initialisé". La valeur zéro ne peut être utilisé que pour l'initialisation pour déclarer une "valeur inutilisable mais connue", pouvant être rejetée d'une manière contrôlée par des algorithmes : pour indiquer une erreur de variable non initialisée lorsque l'objet est utilisé avant l'initialisation.
Il n'est pas toujours possible de déclarer une valeur zéro pour tous les types, particulièrement pour les types modulo, comme un angle. Dans ce cas, choisissez la valeur la moins probable.
Ce chapitre fournit des conseils sur l'utilisation et le format de diverses catégories d'expressions et d'instructions C++.
Eviter une imbrication trop importante des expressions
Le niveau d'imbrication d'une expression est défini comme le nombre d'ensembles de parenthèses imbriquées requis pour l'évaluation d'une expression de gauche à droite, si les règles de priorité des opérateurs ont été ignorées.
Une quantité trop importante de niveaux d'imbrications rend les expressions plus difficiles à comprendre.
A moins que l'ordre d'évaluation soit spécifié par un opérateur (opérateur virgule, expression ternaire, conjonctions et disjonctions), ne supposez aucun ordre d'évaluation particulier, car cela peut conduire à de mauvaises surprises et à la non transférabilité.
Par exemple, ne combinez pas l'utilisation d'une variable dans la même instruction comme un incrément ou un décrément de la variable.
foo(i, i++); array[i] = i--;
NULL
L'utilisation de 0 ou NULL
pour les pointeurs nuls est très sujet à controverse.
Le C et le C++ définissent que toute expression constante évalué à zéro peut être interprétée comme un pointeur nul. Puisque 0 est difficile à lire et que l'utilisation de littéraux n'est pas recommandée, les programmeurs utilisent traditionnellement la macro NULL
comme pointeur nul. Malheureusement, il n'existe aucune définition transférable pour NULL
.
Certains compilateurs C ANSI ont utilisé (void *)0, mais ce choix s'est révélé mauvais pour le C++ :
char* cp = (void*)0; /* C admis mais pas C++ */
Ainsi, toute définition de NULL
de la forme (T*)0, autre qu'un simple zéro, nécessite un transtypage dans C++. Historiquement, les instructions recommandant l'utilisation de 0 pour les pointeurs nuls, essayaient de faciliter l'exigence de transtypage et de rendre le code plus transférable. Plusieurs développeurs C++ se sentent toutefois plus à l'aise avec l'utilisation de NULL
plutôt que 0, et affirment que la plupart des compilateurs (plus précisément, la plupart des fichiers d'en-tête) implémentent désormais NULL
comme 0.
Cette instruction est en faveur de 0, puisqu'il est garanti que 0 travaille sans tenir compte de la valeur de
NULL
. Toutefois, ce point est rétrogradé comme conseil, à suivre ou à ignorer, selon les cas, et ce, en raison de la controverse.
Utilisez les nouveaux opérateurs de transtypage (dynamic_cast, static_cast,
reinterpret_cast, const_cast
) plutôt que les anciens.
Si vous n'avez pas les nouveaux opérateurs de transtypage, évitez le transtypage complet, particulièrement la conversion (conversion d'un objet de classe de base en un objet de classe dérivée).
Utilisez les opérateurs de transtypage comme suit :
dynamic_cast
: pour le transtypage entre les membres de la même hiérarchie de classe (sous-types) à l'aide d'informations d'exécution (les informations d'exécution sont disponibles pour les classes ayant des fonctions virtuelles). Le transtypage entre de telles classes est sûr.static_cast
: pour le transtypage entre les membres de la même hiérarchie de classe, sans utiliser d'informations de type d'exécution, la sécurité n'est donc pas garantie. Si le programmeur ne peut pas garantir la sécurité type, utilisez alors dynamic_cast
.reinterpret_cast
: pour le transtypage entre les types d'intégrale (entier) et de pointeur non associé. Il n'est pas sûre et doit seulement être utilisé entre les types mentionnés.const_cast
: pour le transtypage de la "constness" d'un argument de fonction spécifié en tant que paramètre const
. const_cast
n'a pas pour objectif de rejeter la "constness" d'un objet justement défini en tant qu'objet const (c'est possible en mémoire morte).N'utilisez pas typeid
pour implémenter une logique de commutation de type : laissez les opérateurs de transtypage exécuter la vérification de type et la conversion automatiquement. Voir[Stroustrup, 1994] pour une explication précise.
A ne pas faire :
void foo (const base& b) { if (typeid(b) == typeid(derived1)) { do_derived1_stuff(); else if (typeid(b) == typeid(derived2)) { do_derived2_stuff(); else if () { } }
L'ancien style de transtypage bloque le système de type et peut provoquer des bogues difficiles à détecter qui ne soient pas interceptés par le compilateur : le système de gestion de la mémoire peut être corrompu, les tables de fonctions virtuelles peuvent être ignorées, et les objets non associés peuvent être endommagés lors de l'accès à l'objet en tant qu'objet de classe dérivée. Notez que le dommage peut même être effectué par un accès en lecture, puisque les champs ou les pointeurs non existants peuvent être référencés.
Les nouveaux opérateurs de transtypage rendent la conversion de type plus sûre (dans la plupart des cas) et plus explicite.
N'utilisez pas les constantes ou les macros booléennes de l'ancien style : il n'existe aucune valeur standard booléenne true. Utilisez plutôt le nouveau type bool
.
Puisqu'il n'y avait traditionnellement aucune valeur standard pour true (1 ou ! 0), les comparaisons d'expressions différentes de zéro avec true pouvaient échouer.
Utilisez plutôt des expressions booléennes.
Evitez ceci :
if (someNonZeroExpression == true) // May not evaluate to trueFaites plutôt :
if (someNonZeroExpression) // Always evaluates as a true condition.
Le résultat de telles opérations est presque toujours sans signification.
Evitez un sinistre en définissant un pointeur désignant un objet supprimé comme indéfini : la suppression répétée d'un pointeur défini est néfaste, mais celle d'un pointeur indéfini est inoffensive.
Attribuez toujours une valeur de pointeur indéfinie après la suppression, même avant un retour de fonction, puisque le nouveau code peut être ajouté ultérieurement.
Utiliser une instruction switch lors de l'embranchement sur des valeurs discrètes
Utilisez une instruction switch plutôt qu'une série de "else if" lorsque la condition d'embranchement est une valeur discrète.
par défaut
aux instructions switch pour l'interception des erreursUne instruction switch doit toujours contenir une branche par défaut
qui doit servir à l'interception des erreurs.
Cette stratégie garantit que lorsque de nouvelles valeurs switch sont introduites, et que les branches servant à gérer les nouvelles valeurs sont oubliées, la branche par défaut existante interceptera l'erreur.
Utilisez une instruction for plutôt qu'une instruction while lorsque la résiliation de boucle ou d'itération est basée sur le compteur de boucle.
Eviter l'utilisation d'instructions de saut dans les boucles
Evitez de quitter les boucles (à l'aide de break, return
ou goto
) autrement que par la méthode de fin de boucle, et de passer prématurément à l'itération suivante avec continue
. Cela réduit le nombre de flux de chemins de contrôle, ce qui rend le code plus facile à comprendre.
goto
Il s'agit d'une recommandation universelle.
Cela entraîne des confusions pour les lecteurs et des risques potentiels de maintenance.
Ce chapitre conseille sur les sujets traitant de la gestion de mémoire et des relevés d'erreurs.
Les fonctions malloc
, calloc
et realloc
de la bibliothèque C ne doivent pas être utilisées pour l'attribution d'espace objet : le nouvel opérateur C++ doit être utilisé dans ce but.
La mémoire doit être attribuée à l'aide des fonctions C uniquement lorsqu'elle est sur le point d'être transmise à une fonction de bibliothèque C.
N'utilisez pas delete (supprimer) pour libérer de la mémoire allouée par les fonctions C, ou sur des objets créés de nouveau.
Si vous utilisez delete sur des objets de tableau sans insérer les crochets ("[]"), seul le premier élément du tableau sera supprimé, entraînant donc de la fuite de mémoire.
Le mécanisme des exceptions C++ ayant été peu utilisé, les instructions suivantes devront probablement être sérieusement revues.
Le projet de norme C++ définit deux larges catégories d'erreurs : les erreurs de logique et les erreurs d'exécution. Les erreurs de logique sont des erreurs de programmation qui peuvent être évitées. Les erreurs d'exécution sont définies comme des erreurs dues aux événements dans le cadre du programme.
La règle générale d'utilisation des exceptions est que le système en condition normale et en l'absence de surcharge ou d'incident matériel ne devrait émettre aucune exception.
Utilisez les préconditions de fonction et les assertions de postcondition pendant le développement pour fournir une détection des erreurs "fatales".
Les assertions fournissent un mécanisme simple et utile de détection des erreurs provisoires jusqu'à l'implémentation du code final de traitement d'erreur. En outre, les assertions peuvent être compilées à l'aide du symbole de préprocesseur "NDEBUG" (voir "Définir le symbole NDEBUG avec une valeur spécifique").
La macro assert est généralement utilisée dans ce but. Toutefois, la référence [Stroustrup, 1994] fournit une alternative de canevas, voir ci-dessous.
template<class T, class Exception> inline void assert ( T a_boolean_expression, Exception the_exception) { if (! NDEBUG) if (! a_boolean_expression) throw the_exception; }
N'utilisez pas d'exceptions pour les événements prévus et fréquents : les exceptions provoquent des perturbations dans le flux normal de contrôle du code, ce qui le rend plus difficile à comprendre et à gérer.
Les événements prévus doivent être gérés dans le flux normal de contrôle du code. Utilisez une valeur de retour de fonction ou le code d'état de paramètre "out" si nécessaire.
Les exceptions ne doivent pas non plus être utilisées pour implémenter des structures de contrôle : ce serait une autre forme d'instruction "goto".
Cela garantit que toutes les exceptions prennent en charge un ensemble minimal d'opérations communes et peuvent être gérées par un petit nombre de responsables de haut niveau.
Les erreurs de logique (erreur de domaine, erreur d'argument non valide, erreur de longueur et erreur hors échelle) devraient être utilisées pour indiquer les erreurs de domaine d'application, les arguments non valides transmis aux appels de fonction, la construction d'objets en dessous de leurs valeurs autorisées et les valeurs d'argument qui ne sont pas dans les intervalles autorisés.
Les erreurs d'exécution (erreur d'intervalle et erreur de dépassement de capacité) doivent être utilisées pour indiquer les erreurs arithmétiques et de configuration, les données altérées ou les erreurs d'épuisement des ressources qui ne sont détectables qu'à l'exécution.
Dans les grands systèmes, la gestion d'un grand nombre d'exceptions à chaque niveau rend le code difficile à lire et à gérer. Le traitement des exceptions peut éclipser le traitement normal.
Voici des manières de réduire le nombre d'exceptions :
Partagez les exceptions entre les abstractions à l'aide d'un faible nombre de catégories d'exception.
Emettez les exceptions spécialisées dérivées des exceptions standard mais gérez plus d'exceptions généralisées.
Ajoutez des états "exceptionnels" aux objets, et fournissez des primitives pour contrôler explicitement la validité des objets.
Les fonctions développant des exceptions (pas seulement les transmettant) doivent déclarer toutes les exceptions émises dans leur spécification d'exception : elles ne doivent pas générer des exceptions silencieusement sans prévenir leurs clients.
Lors du développement, répertoriez les exceptions à l'aide du mécanisme de journalisation approprié, le plus tôt possible ainsi qu'à leur "point d'émission".
Les gestionnaires d'exception doivent être définis de la classe dérivée à la classe de base, afin d'éviter de coder des gestionnaires inaccessibles. Voir l'exemple ci-dessous de ce qu'il ne faut pas faire. Cela assure également que la plupart des gestionnaires appropriés interceptent l'exception puisque les gestionnaires sont appariés dans un ordre de déclaration.
A ne pas faire :
class base { ... }; class derived : public base { ... }; ... try { ... throw derived(...); // // Throw a derived class exception } catch (base& a_base_failure) // // But base class handler "catches" because // it matches first! { ... } catch (derived& a_derived_failure) // // This handler is unreachable! { ... }
Evitez les gestionnaires d'exception interceptant tout (déclarations de gestionnaire utilisant ...), à moins que l'exception soit de nouveau émise.
Les gestionnaires interceptant tout ne doivent être utilisés que pour la gestion interne. L'exception doit donc être émise de nouveau pour éviter de masquer le fait que l'exception ne peut pas être gérée à ce niveau :
try { ... } catch (...) { if (io.is_open(local_file)) { io.close(local_file); } throw; }
Lors du renvoi de codes d'état comme paramètre de fonction, attribuez toujours une valeur au paramètre en tant que première instruction exécutable dans le corps de fonction. Faites systématiquement des états un succès par défaut ou un échec par défaut. Pensez à toutes les possibilités de sortie de fonction, y compris les gestionnaires d'exception.
Si une fonction risque de produire un résultat erroné à moins d'une saisie appropriée, installez le code dans la fonction pour détecter et signaler l'entrée non valide d'une manière contrôlée. Ne vous fiez pas à un commentaire disant au client de passer des valeurs appropriées. Il est virtuellement certain qu'un jour ou l'autre, ce commentaire sera ignoré, ce qui entraînera des erreurs difficiles à déboguer si les paramètres non valides ne sont pas détectés.
Ce chapitre traite des fonctions de langage qui sont a priori non portables.
Les noms des chemins d'accès ne sont pas représentés d'une façon standard dans les systèmes d'exploitation. Leur utilisation entraîne des dépendances de plates-formes.
#include "somePath/filename.hh" // Unix #include "somePath\filename.hh" // MSDOS
La représentation et l'alignement des types sont très dépendants de l'architecture de la machine. Les suppositions émises à propos de la représentation et de l'alignement peut entraîner des mauvaises surprises et réduire la portabilité.
En particulier, n'essayez jamais de stocker un pointeur dans un int
, un entier long ou tout autre type numérique, ce n'est absolument pas portable.
Ne pas dépendre d'un dépassement négatif particulier ou d'un comportement de dépassement négatif
Utiliser des constantes "élastiques" lorsque c'est possible
Les constantes élastiques évitent les problèmes de variation des tailles de mots.
const int all_ones = ~0; const int last_3_bits = ~0x7;
Les architectures des machines peuvent dicter l'alignement de certains types. La conversion de types avec des exigences d'alignement plus souples au niveau en types avec des exigences d'alignement plus rigoureuses peut entraîner des défaillances de programme.
Ce chapitre conseille sur la réutilisation du code C++.
Si les bibliothèques standards ne sont pas disponibles, créez alors des classes basées sur les interfaces des bibliothèques standards : cela facilite une migration ultérieure.
Utilisez des canevas pour réutiliser un comportement, lorsque ce dernier n'est pas dépendant d'un type de donnée spécifique.
Utilisez l'héritage public
pour exprimer la relation "isa"
et réutiliser les interfaces de classe de base, et leur implémentation (facultatif).
Evitez l'héritage privé lors de la réutilisation de l'implémentation ou la modélisation de "la totalité/une partie" des relations. La réutilisation d'implémentation sans redéfinition est mieux réussie par confinement que par l'héritage privé.
Utilisez l'héritage privé lorsque la redéfinition des opérations de classe de base est nécessaire.
L'héritage multiple doit être utilisé de manière sensée car il apporte une grande complexité. [Meyers, 1992] explique clairement les complexités dues aux ambiguïtés potentielles liées aux noms et à l'héritage répété. La complexité résulte de :
Les ambiguïtés : lorsque les mêmes noms sont utilisés par plusieurs classes, toute référence non qualifiée aux noms est ambiguë par nature. L'ambiguïté peut être résolue en qualifiant les noms de membre avec les noms de leurs classes. Cependant, cette méthode a pour effet regrettable de bloquer le polymorphisme et de transformer les fonctions virtuelles en fonctions liées statiquement.
L'héritage répété (héritage à de multiples reprises d'une classe de base par une classe dérivée via différents chemins dans la hiérarchie d'héritage) de plusieurs ensembles de membres de données à partir de la même base soulève le problème suivant : quels ensembles multiples de membres de données doivent être utilisés ?
La multiplication des membres de données hérités peut être évitée en utilisant l'héritage virtuel (héritage de classes de base virtuelles). Pourquoi ne pas toujours utiliser l'héritage virtuel, alors ? L'inconvénient de l'héritage virtuel est qu'il altère la représentation de l'objet sous-jacent et réduit l'efficacité d'accès.
Le fait de décréter une règle exigeant que tous les héritages soient virtuels, et d'imposer en même temps une pénalité de temps et d'espace serait trop autoritaire.
L'héritage multiple nécessite donc que les concepteurs de classe soient clairvoyants concernant les utilisations futures de leurs classes, et ce, afin d'être capable de décider de l'utilisation de l'héritage virtuel ou non virtuel.
Ce chapitre fournit des conseils sur les problèmes de compilation
N'incluez pas dans une spécification de module d'autres fichiers d'en-tête que ceux qui sont requis par l'implémentation du module.
Evitez d'incorporer des fichiers d'en-tête dans une spécification dans le but de gagner de la visibilité dans les autres classes, lorsque seule la visibilité de pointeur ou de la référence est nécessaire. A la place, utilisez des déclarations aval.
// Spécification du module A, contenue dans le fichier "A.hh" #include "B.hh" // Don't include when only required by // l'implémentation. #include "C.hh" // Don't include when only required by // référence; utiliser plutôt une déclaration aval. class C; class A { C* a_c_by_reference; // Has-a by reference. }; // End of "A.hh"
La réduction des dépendances de compilation est la raison de certains patterns ou schémas de mise en oeuvre de conception, diversement désignés : Handle ou Envelope [Meyers, 1992] ou Bridge [Gamma] classes. En divisant la responsabilité d'une abstraction de classe sur deux classes associées, l'une fournissant l'interface de classe et l'autre l'implémentation, les dépendances entre une classe et ses clients sont réduites puisque toute modification apportée à l'implémentation (la classe d'implémentation) n'entraîne plus la recompilation des clients.
// Module A specification, contained in file "A.hh" class A_implementation; class A { A_implementation* the_implementation; }; // End of "A.hh"
Cette approche permet également à la classe d'interface et à la classe d'implémentation d'être spécialisées en deux hiérarchies de classe distinctes.
NDEBUG
avec une valeur spécifiqueLe symbole NDEBUG
était traditionnellement utilisé pour compiler le code d'assertion implémenté à l'aide de la macro assert. Le paradigme servait à définir le symbole lorsqu'il fallait éliminer des assertions. Toutefois, les développeurs connaissaient rarement l'existence des assertions et n'ont donc jamais défini le symbole.
Nous préconisons l'utilisation de la version canevas de l'assert. Dans ce cas, il faut donner une valeur explicite au symbole NDEBUG
: 0 si le code d'assertion est souhaité, une valeur non nulle pour l'éliminer. Tout code d'assertion compilé par la suite sans fournir de valeur spécifique au symbole NDEBUG
générera des erreurs de compilation. L'attention du développeur sera alors attirée vers l'existence du code d'assertion.
Voici un récapitulatif de toutes les instructions présentées dans ce manuel.
Utiliser le sens commun
Toujours utiliser #include
pour avoir accès à la spécification d'un module
Ne jamais déclarer de noms commençant par un ou plusieurs
traits de soulignement ('_')
Restreindre les déclarations globales uniquement aux espaces de nom
Toujours fournir
un constructeur par défaut aux classes ayant des constructeurs déclarés explicitement
Toujours déclarer des constructeurs de copie et des opérateurs d'affectation aux classes avec des membres de données de type pointeur
Ne jamais redéclarer des paramètres de constructeurs avec des valeurs par défaut
Déclarer toujours les destructeurs comme étant virtuels
Ne jamais redéfinir des fonctions non virtuelles
Ne jamais appeler des fonctions membres à partir d'un initialiseur de construction
Ne jamais renvoyer une référence à un objet local
Ne jamais renvoyer de pointeur déréférencé initialisé de nouveau
Ne jamais retourner de pointeur ou de référence non const à des données de membre
Faire en sorte que operator=
renvoie un référence à *this
Effectuer un contrôle operator=
pour l'auto-attribution
Ne jamais rejeter la "constness" (utilisation de const) d'un objet constant
Ne pas supposer d'ordre d'évaluation particulier des expressions
Ne pas utiliser l'ancien style de transtypage
Utiliser le nouveau type bool
pour les expressions booléennes
Ne jamais comparer directement avec la valeur booléenne true
Ne jamais comparer des pointeurs à des objets qui ne sont pas dans le même rang
Toujours attribuer une valeur de pointeur indéfinie
à un pointeur d'objet supprimé
Toujours fournir une branche par défaut aux instructions switch pour l'interception des erreurs
Ne pas utiliser d'instruction goto
Eviter de mélanger les opérations de mémoire C et C++
Toujours utiliser delete[] pour supprimer des objets de tableau créés de nouveau
Ne jamais utiliser de noms de chemin d'accès aux fichiers définis dans le code
Ne pas supposer la représentation d'un type
Ne pas supposer l'alignement d'un type
Ne pas dépendre d'un dépassement négatif particulier ou d'un comportement de dépassement négatif
Ne pas convertir un type "plus court" en un type "plus long"
Définir le symbole NDEBUG
avec une valeur spécifique
Placer les implémentations et les spécifications de module dans des fichiers distincts
Extraire un seul ensemble d'extensions de nom de fichiers pour distinguer les en-têtes des fichiers d'implémentation
Eviter de définir plus d'une classe par spécification de module
Eviter de placer les déclarations propres à l'implémentation dans les spécifications d'un module
Placer les définitions de fonction inline de module dans un fichier distinct
Portionner les grands modules en plusieurs unités de translation si la taille du programme pose problème
Isoler les dépendances de plates-formes
Protection contre les inclusions de fichier répétées
Utiliser un symbole de compilation conditionnelle "No_Inline
" pour renverser la compilation inline
Utiliser un style de retrait cohérent et faible pour les instructions imbriquées
Mettre en retrait les paramètres de fonction à partir du nom de fonction ou du nom de portée
Utiliser une longueur de ligne maximale adaptée à la taille du papier d'impression standard
Utiliser un pliage de ligne cohérent
Utiliser des commentaires de style C++ au lieu de commentaires de style C
Optimiser la proximité du commentaire par rapport au code source
Eviter les commentaires en fin de ligne
Eviter les en-têtes de commentaires
Utiliser une ligne de commentaire vide pour séparer les paragraphes de commentaires
Eviter les redondances
Ecrire un code autodocumenté plutôt que des commentaires
Documenter les classes et les fonctions
Choisir une convention de dénomination et l'appliquer systématiquement
Eviter l'utilisation de noms de types qui ne soient différents que par la casse
Eviter l'utilisation d'abréviations
Eviter l'utilisation de suffixes pour dénoter les constructions de langage
Choisir des noms clairs, lisibles et ayant un sens
Orthographier les noms correctement
Utiliser des clauses de prédicat positif pour les booléens
Utiliser les espaces de nom pour diviser les noms complets potentiels par sous-systèmes ou par bibliothèques
Utiliser des noms ou des syntagmes nominaux pour les noms de classe
Utiliser des verbes pour les noms de fonction de type procédure
Utiliser la surcharge de fonction pour des objectifs identiques
Intensifier les noms par des éléments grammaticaux pour en accentuer le sens
Choisir des noms d'exception avec une signification négative
Utiliser des adjectifs sur le projet pour les noms d'exception
Utiliser des majuscules pour les exposants à virgule flottante et les chiffres hexadécimaux
Utiliser un espace de nom pour regrouper la fonctionnalité sans classe
Réduire l'utilisation de données de portée globale ou d'espace de nom
Utiliser classe plutôt que struct pour l'implémentation de types de données abstraits
Déclarer des membres de classe en ordre d'accessibilité décroissante
Eviter de déclarer des membres de données publiques ou protégées pour des types de données abstraits
Utiliser des amis pour préserver l'encapsulation
Eviter de fournir des définitions de fonction dans les déclarations de fonction
Eviter de déclarer trop d'opérateurs de conversion et des constructeurs de paramètres uniques
Utiliser les fonctions non virtuelles judicieusement
Utiliser des constructeurs-initialiseurs plutôt que des affectations dans les constructeurs
Attention lors de l'appel de fonctions membres dans les constructeurs et les destructeurs
Utiliser static const pour les constantes de classes entières
Toujours déclarer un type de retour de fonction explicite
Toujours fournir des noms de paramètres formels dans les déclarations de fonction
Rechercher des fonctions avec un seul point de retour
Eviter la création de fonction avec des effets secondaires globaux
Déclarer les paramètres de fonction en ordre décroissant d'importance et de volatilité
Eviter de déclarer des fonctions ayant un nombre variable de paramètres
Eviter de redéclarer des fonctions avec des paramètres par défaut
Maximiser l'utilisation de const dans les déclarations de fonction
Eviter de transmettre des objets par valeur
Utiliser de préférence les fonctions inline au lieu de #define
pour l'expansion de macro
Utiliser des paramètres par défaut plutôt que la surcharge de fonction
Utiliser la surcharge de fonction pour exprimer une sémantique commune
Eviter de surcharger les fonctions avec des pointeurs et des entiers
Limiter la complexité
Eviter l'utilisation de types fondamentaux
Eviter l'utilisation de valeurs littérales
Eviter l'utilisation de
la directive de préprocesseur #define
pour définir des constantes
Déclarer les objets près du point de leur première utilisation
Toujours initialiser les objets const au moment de la déclaration
Initialiser des objets lors de la définition
Utiliser une instruction if lors de l'embranchement des expressions booléennes
Utiliser une instruction switch lors de l'embranchement sur des valeurs discrètes
Utiliser une instruction for ou une instruction while lorsqu'un test de pré-itération est requis dans une boucle
Utiliser une instruction do-while lorsqu'un test de post-itération est requis dans une boucle
Eviter l'utilisation d'instructions de saut dans les boucles
Eviter le masquage des identificateurs dans des portées imbriquées
Utiliser abondamment les assertions lors du développement pour détecter les erreurs
Utiliser les exceptions uniquement pour les conditions vraiment exceptionnelles
Dériver les exceptions de projet des exceptions standards
Réduire le nombre d'exceptions utilisées par une abstraction donnée
Déclarer toutes les exceptions émises
Définir les gestionnaires d'exception dans l'ordre suivant : de la classe dérivée à la classe de base
Eviter les gestionnaires d'exception interceptant tout
S'assurer que les codes d'état des fonctions ont une valeur appropriée
Exécuter les contrôles de sécurité localement, ne pas attendre que le client les fasse
Utiliser des constantes "élastiques" lorsque c'est possible
Utiliser des composants des bibliothèques standards dès que c'est possible
Définir les types de systèmes globaux du projet
Utiliser typedef pour créer des synonymes afin de renforcer la signification locale
Utiliser des parenthèses redondantes pour rendre les expressions composées plus claires
Eviter une imbrication trop importante des expressions
Utiliser 0 pour les pointeurs nuls au lieu de NULL
Répertorier les exceptions dès la première occurrence