cpp
Convention de code en C++
Posted by Jean-Michel Frouin on .Introduction
A partir du moment où l'on développe du code, que ce soit seul ou en équipe, il faut quelques conventions pour rendre le code "compréhensible". Un premier réflexe à avoir est de bien commenter le code. Soit pour les autres développeurs, soit pour les autres personnes intervenant dans le projet et qui seront amenées à lire le code. Pour ce faire, il faut dès le départ avoir une convention de nommage. Une convention de nommage est un ensemble de règles de mise en forme du code. Un très bon ouvrage (High-integrity C++ coding standard manual) traite des lignes de conduites conseillées pour coder en C++. Dans le cas de ce document, elles ne s'appliquent pas toutes. Toutes les conventions, ou lignes de conduites, ne sont fournies qu'à titre informatif.
Nommage
Une convention de nommage est essentielle, si on veut gagner du temps, aussi bien pour intégrer de nouveaux développeurs dans le projet, que pour débugger son code. Une chose est importante lorsque l'on trouve un nom pour une donnée membre (ou une variable) : il faut trouver un nom qui convient. Pour cela, il faut suivre la convention de nommage et en plus trouver un choix judicieux. Un nom ne doit pas être trop long ni trop court. Voici ma convention (mixte entre celle de Microsoft et celle du noyau linux).
Pour définir :
Variable globale : bool gVar
Donnée membre d'une classe : bool mVar.
Variable à portée locale : bool Var.
Paramètre d'une méthode : void CClass::Function(bool var).
Type : typedef char tVarType.
Méthode privée d'une classe : bool CClass::__Register();
Nom d'une classe : class CName.
Interface de classe : class IName.
Patrons (template) d'une classe : class TName.
Enumération : enum eVarName.
Code
Introduction
D'une manière générale, il est important de bien commenter les fichiers d'en-têtes des classes. Une autre chose importante est de bien organiser le répertoire du projet dès le début. Il faudra veiller à prévoir un répertoire pour la compilation séparée du répertoire contenant les sources (voir cmake) Isoler les codes sources dans un répertoire dédié.
+-build (Répertoire de compilation totalement) +-doc (Contient la documentation doxygen) +-gfx (Images pour doxygen) +-src (Contient tous les codes sources du projet) +---- leaks +---- plugins +---- outils +---- xml
Pour le développement de logiciels libres, certains fichiers doivent être présents. C'est une bonne méthode de travail de les intégrer par défaut dans n'importe quel projet.
Notamment :
COPYING : Contient la licence du projet.
CHANGELOG : Contient la liste des modifications. (Très pratique pour garder une trace des évolutions du projet)
COPYRIGHT : Contient la liste des auteurs qui ont participé au projet.
INSTALL : Contient une description complète sur la façon de compiler et d'installer l'application.
README : Contient des informations générales sur le projet.
TODO : Liste des tâches à faire.
Bien entendu, le contenu de ces fichiers peut facilement être intégré dans la documentation, doxygen par exemple, du projet.
Accesseurs et mutateurs
Plutôt que de rendre une donnée membre public ou protected, il vaut mieux utiliser les accesseurs / mutateurs pour y accéder.
Un accesseur n'est rien d'autre qu'une méthode de la classe permettant de récupérer la valeur de la donnée membre. Le type de retour de l'accesseur est donc celui de la donnée membre à encapsuler.
Un mutateur est une méthode de la classe permettant de modifier la valeur de la donnée membre. Le type de retour du mutateur est donc void. Par contre, le mutateur prend au moins une valeur en paramètre, la nouvelle valeur.
code/acc_mut.cpp
Lorsque toutes les données membres d'une classe sont totalement encapsulées par un couple accesseur / mutateur, on parle de programmation objet pure. (C++ n'oblige pas la POO pure)
Accolades
On a vite tendance à écrire ce genre de choses :
bool Classe::Verif() { if(condition) return true; else return false; }
Pour que le code soit plus lisible (et plus fonctionnel), j'utilise les accolades autant que possible.
Le code devient donc :
bool Classe::Verif() { if(condition) { return true; }else { return false; } }
L'utilisation des accolades peut sembler inutile ici, mais en fait si l'on vient à commenter un des return, les accolades nous évitent les erreurs de compilation.
Ainsi le code suivant compilera toujours (ce qui n'est pas le cas du même code sans accolades) :
bool Classe::Verif() { if(condition) { //return true; } else { return false; } }
Classe
Écriture
Lors de l'écriture d'une classe, déclarer les méthodes et les données membres séparément pour plus de clarté. Utiliser l'opérateur de portée pour les séparer. Ainsi, au lieu de :
class CClasse { public: void Methode1(); int mNb; void Methode2(); int mLong; int mLarg; };
On préférera :
class CClasse { //Méthodes. public: void Methode1(); void Methode2(); //Données membres. public: int mNb; int mLong; int mLarg; };
De plus, il est important (pour une efficacité plus grande lors d'une recherche) de définir les méthodes dans l'ordre décroissant de leur portée. (public -> protected -> private).
classe CClasse { public: CClasse(); protected: Methode(); private: ~CClasse(); };
Implémentation
Forme de Coplien sécurisée :
Lors de l'écriture de classe complexe (notamment gérant des pointeurs), on peut réduire les risques de problèmes en définissant :
Un constructeur par défaut.
Un constructeur par copie.
Un opérateur de copie par affectation (T& operator=(const T&;)).
Un destructeur (Si ces 4 méthodes sont correctement implémentées, on parle alors de forme de Coplien (sécurisée)).
Limiter le nombre de paramètres d'une méthode / fonction :
Pour améliorer la lisibilité et la performance du code, ne pas définir plus de 6 à 8 paramètres pour une fonction/méthode. (Si on a besoin de plus de 6 paramètres, il faut se demander si la méthode est bien conçue)
Définir les tests explicitement :
Une autre petite habitude à prendre, qui simplifie la vie (et évite de revenir sur des dizaines de fichiers plus tard) est de toujours définir les tests explicitement :
if(mVar != 0)
plutôt que
if(mVar)
En effet, Si mVar est un pointeur, cela fonctionnera. Mais si, du jour au lendemain, le pointeur devient un scope pointeur, alors il faudra revenir sur le code.
\textit{Initialiser ses pointeurs} :
Encore une bonne habitude à prendre (qui découle de la remarque précédente) : toujours initialiser les pointeurs à 0 dans les constructeurs par défaut d'une classe.
Cela permet de détecter rapidement les problèmes (en supposant que l'on utilise des tests explicites).
Les deux remarques précédentes évitent le problème illustré par le code suivant :
#include int main() { int* Ptr; if(Ptr) { std::cout << "Que ce passe t'il si j'utilise Ptr ici ?\n"; } return EXIT_SUCCESS; }
Mots clefs à éviter :
Ne JAMAIS utiliser goto. Éviter au maximum l'utilisation de const qui peut être trop facilement annulé grâce à mutable.
Constantes et Variables globales
Éviter au maximum d'utiliser des nombres importants (pour le projet) sans explication. Par exemple, pour l'utilisation d'un masque de bit : 0xFF00, il vaut mieux passer par une constante : #define masque 0xFF00;. De plus suffixer les constantes par un F pour un flot, un L pour un long: #define max = 122.0L simplifie la lecture du code.
L'utilisation de variables globales est déconseillée, car cela rend le code non portable (cf C++ Gotchas: Avoiding Common Problems in Coding and Design, Gotchas \#3).
L'utilisation de NULL pour initialiser un pointeur à 0 n'est pas pertinente car NULL est en fait une définition dépendante de la machine sur laquelle on compile le code. L'utilisation de NULL rend donc le code plus difficile à porter, mais est surtout source d'erreur.
Exemple de différentes définitions de NULL :
#define NULL ((char *)0) #define NULL ((void *)0) #define NULL 0
Fonctions inline
Les fonctions inline doivent être des fonctions courtes (Les accesseurs et les mutateurs sont de bons candidats aux fonctions inline) sans quoi cela peut affecter les performances.
Il vaut mieux définir une fonction inline de façon implicite. Ainsi vaut-il mieux écrire cela :
classe CClasse { public: //Déclaration, inline, implicite car définie dans la déclaration de la classe. int getNb() { return mNb; } private: int mNb; };
au lieu de :
classe CClasse { public: inline int getNb(); //Déclaration explicite (mot clef inline) public: int mNb; }; CClasse::getNb() { return mNb; // Définition hors de la déclaration de la classe. }
Initialisation
Il est préférable d'initialiser directement une variable lors de sa création plutôt que de lui affecter une valeur après sa création. Ainsi :
std::string Tmp("Test");
est préférable à :
std::string Tmp; Tmp = "Test";
Dans le premier cas, seul le constructeur (Attention, le constructeur de std::string (comme beaucoup de classes) ne se limite pas à l'appel du constructeur de std::string, mais par exemple à l'appel des constructeurs de ses classes parentes} de std::string est appelé.
Dans le second on ajoute une opération d'affectation via l'opérateur = des std::string.
Instruction de branchement
Pour éviter les calculs inutiles, il vaut mieux ne pas faire appel à une méthode dans le test d'arrêt d'une instruction de branchement.
Ce code fera appel à la méthode getMax, de l'objet a, autant de fois qu'il y aura d'itération :
for(int i = 0; i < a.getMax(); ++i) ...
Ce code n'appellera getMax qu'une fois :
const inst Max = a.getMax(); for(int i=0; i < Max; ++i) ...
Retour
L'utilisation classique des return ne permet pas de garder la maîtrise d'une méthode. En effet on ne sait jamais à l'avance où sortira (dans le code) notre fonction:
bool Classe::Verif() { if(condition) { return true; //On sort ici ? } else { return false; //Ou la ? } }
Cela peut être amélioré avec :
bool Classe::Verif() { bool Ret = false; if(condition) { Ret = true; } return Ret; //On sort toujours ici ! }
Pfffff voila !