écrire du C++ clair, maintenable et robuste

Salut ce forum apprends les bases du c++

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

écrire du C++ clair, maintenable et robuste

Post by Hydraxx »

écrire du C++ clair, maintenable et robuste

Objectif du chapitre

Le style de programmation ne concerne pas seulement l'apparence du code. Un bon style doit permettre à un développeur de comprendre rapidement :
  • ce que fait le programme ;
  • pourquoi il le fait ;
  • quelles données sont manipulées ;
  • quelles fonctions ont quelles responsabilités ;
  • quelles ressources sont possédées ou empruntées ;
  • où se trouvent les opérations potentiellement dangereuses.
En C++ système ou bas niveau, ces principes sont particulièrement importants. Une erreur de compréhension peut concerner un pointeur, une taille de buffer, une ressource système ou une durée de vie et provoquer un crash, une corruption mémoire ou une fuite de ressources.

1. L'importance d'écrire du bon code

Du code correct n'est pas forcément du bon code.

Un programme peut compiler et produire le résultat attendu tout en étant difficile à comprendre, modifier ou déboguer. Le code professionnel doit donc viser plusieurs qualités :
  • correction ;
  • lisibilité ;
  • simplicité ;
  • cohérence ;
  • maintenabilité ;
  • facilité de débogage.
Le lecteur du code est important

Le code source est lu beaucoup plus souvent qu'il n'est écrit. Le lecteur peut être :
  • un collègue ;
  • un mainteneur plusieurs années plus tard ;
  • toi-même quelques mois après avoir écrit le programme ;
  • une personne chargée de rechercher un bug.
Il faut donc éviter de considérer qu'un code est bon simplement parce que son auteur le comprend actuellement.

Thinking Ahead

Lorsqu'on écrit une fonction, il faut penser à son utilisation future. Une fonction claire, correctement nommée et ayant une responsabilité précise sera beaucoup plus facile à réutiliser et à modifier.

2. Documenter son code

Les commentaires sont utiles lorsqu'ils apportent une information que le code ne communique pas suffisamment clairement.

Un commentaire ne doit normalement pas traduire mécaniquement une instruction C++ en français.

Mauvais principe :

Code: Select all

// Incrémente x
++x;
Le commentaire ne fournit aucune information supplémentaire.

Un commentaire beaucoup plus utile explique une intention :

Code: Select all

// Ignore l'entrée réservée afin qu'elle ne soit pas traitée
// comme une entrée utilisateur normale.
La règle fondamentale est donc :

Le code explique généralement COMMENT quelque chose est fait. Le commentaire doit surtout expliquer POURQUOI.

3. Raisons d'écrire des commentaires

Expliquer l'utilisation d'une fonction

Une fonction publique ou complexe peut nécessiter une documentation indiquant :
  • son objectif ;
  • le sens de ses paramètres ;
  • ses préconditions ;
  • la signification de sa valeur de retour ;
  • les erreurs ou exceptions possibles ;
  • les effets de bord importants.
Cela permet à l'utilisateur de la fonction de comprendre son contrat sans avoir à analyser toute son implémentation.

Préconditions et postconditions

Une précondition décrit ce qui doit être vrai avant l'appel.

Exemples conceptuels :
  • un pointeur doit être valide ;
  • une taille doit être supérieure à zéro ;
  • une ressource doit déjà avoir été initialisée.
Une postcondition décrit ce que la fonction garantit après son exécution correcte.

Dans du code système, ces informations peuvent être très importantes lorsque plusieurs composants se partagent des ressources ou des buffers.

Expliquer les choix non évidents

Un algorithme peut sembler inutilement compliqué alors qu'il répond à une contrainte précise. Le commentaire doit alors expliquer cette contrainte.

Par exemple :

Code: Select all

// Cette copie intermédiaire est nécessaire car la zone source
// peut être invalidée par l'opération suivante.
Sans cette explication, un futur développeur pourrait « simplifier » le code et introduire un bug.

4. Commenter du code compliqué

Lorsqu'un algorithme est complexe, des commentaires peuvent servir de repères.

Il est préférable de commenter les grandes étapes :

Code: Select all

// 1. Recherche de la position d'insertion.

// 2. Décalage des éléments.

// 3. Insertion du nouvel élément.
Cela permet au lecteur de comprendre la structure générale avant d'étudier les instructions.

Mais il existe une meilleure solution lorsque le code devient vraiment difficile à suivre : le décomposer.

Une énorme fonction accompagnée de dizaines de commentaires est souvent le signe que plusieurs responsabilités devraient être séparées.

5. Les commentaires doivent rester corrects

Un commentaire faux peut être plus dangereux qu'une absence de commentaire.

Exemple :

Code: Select all

// Buffer de 256 octets.
std::byte buffer[512];
Le commentaire est devenu incorrect après une modification.

Lorsqu'on modifie du code, il faut donc vérifier les commentaires associés.

Un bon commentaire :
  • apporte une information utile ;
  • reste proche du code concerné ;
  • est concis ;
  • décrit l'intention plutôt que la syntaxe ;
  • est mis à jour lorsque le comportement change.
6. Éviter les commentaires qui compensent un mauvais code

Avant d'ajouter un commentaire pour expliquer une instruction incompréhensible, il faut se demander si le code pourrait simplement être rendu plus clair.

Au lieu d'une expression opaque :

Code: Select all

if (s && n > 0 && f)
des noms significatifs rendent l'intention beaucoup plus évidente.

Le meilleur commentaire est parfois un meilleur nom de variable, une constante correctement nommée ou une fonction extraite.

Le code auto-documenté

Un code est dit auto-documenté lorsque ses noms et sa structure communiquent naturellement son intention.

Cela ne signifie pas qu'il ne faut jamais commenter. Cela signifie qu'on ne doit pas utiliser les commentaires comme remplacement d'un code lisible.

7. Commentaires ligne par ligne

Commenter chaque ligne est généralement une mauvaise pratique.

Exemple :

Code: Select all

// Déclare le compteur.
int count = 0;

// Incrémente le compteur.
++count;

// Retourne le compteur.
return count;
Ces commentaires créent du bruit visuel.

Ils augmentent également la quantité d'informations qu'il faudra maintenir.

Un commentaire ligne par ligne est justifié lorsque l'instruction possède une signification qui n'est réellement pas évidente.

8. Commentaires de bloc et documentation

Un commentaire placé avant une fonction ou un bloc peut documenter son rôle général.

Pour une fonction importante, la documentation peut contenir :
  • description ;
  • paramètres ;
  • retour ;
  • exceptions ;
  • conditions particulières.
Des outils peuvent ensuite transformer certains formats de commentaires en documentation externe.

Il faut néanmoins éviter de produire une documentation gigantesque pour des fonctions dont l'utilisation est évidente.

9. Commentaires ad hoc

Les commentaires temporaires servent parfois de marqueurs pendant le développement.

Exemples courants :

Code: Select all

// TODO: gérer le cas où la taille est nulle.

// FIXME: cette logique doit être revue.
Ils peuvent être utiles, mais ne doivent pas devenir un cimetière de tâches abandonnées.

Un commentaire temporaire doit idéalement être :
  • précis ;
  • actionnable ;
  • facile à retrouver.
Dans un projet professionnel, le système de suivi des problèmes peut être préférable pour les tâches importantes.

10. Commentaires de copyright

Certains projets placent une notice de copyright ou de licence en tête des fichiers.

Il s'agit d'une information juridique ou organisationnelle, pas d'une explication technique du code.

La convention utilisée dépend du projet et de l'organisation.

11. Décomposition

La décomposition consiste à transformer un gros problème en plusieurs problèmes plus petits.

Supposons qu'un programme doive :
  • ouvrir un fichier ;
  • lire son contenu ;
  • analyser les données ;
  • calculer des statistiques ;
  • afficher le résultat.
Tout mettre dans une fonction gigantesque fonctionnerait éventuellement, mais rendrait le programme difficile à maintenir.

Une meilleure conception sépare les responsabilités conceptuellement :

Code: Select all

ouvrir le fichier
lire les données
analyser les données
calculer les statistiques
afficher le résultat
Chaque partie devient plus simple à comprendre et à tester.

12. Décomposition par fonctions

Une fonction devrait généralement représenter une opération cohérente.

Une bonne fonction possède :
  • un objectif clair ;
  • un nom précis ;
  • des entrées compréhensibles ;
  • une sortie identifiable ;
  • une quantité raisonnable de logique.
Il ne faut cependant pas découper artificiellement chaque instruction en fonction séparée.

L'objectif n'est pas d'obtenir le plus grand nombre de fonctions possible. L'objectif est de créer des unités logiques.

Signe qu'une fonction devrait être décomposée

Une fonction qui peut être décrite par :

« elle ouvre le fichier, vérifie son format, analyse les données, calcule des statistiques et affiche les erreurs »

contient probablement plusieurs responsabilités.

À l'inverse :

« elle vérifie la validité de l'en-tête »

correspond à une responsabilité beaucoup plus précise.

13. Avantages de la décomposition

La décomposition apporte plusieurs bénéfices :
  • lecture plus rapide ;
  • tests plus simples ;
  • réutilisation ;
  • débogage plus localisé ;
  • modifications moins risquées ;
  • réduction de la complexité mentale.
Elle permet également de cacher les détails.

Une fonction appelante peut simplement demander une opération sans connaître toutes les étapes internes nécessaires pour la réaliser.

14. Décomposition dans du code système

Cette technique devient particulièrement intéressante en programmation système.

Imagine une opération qui nécessite conceptuellement :
  • obtenir une ressource ;
  • vérifier son état ;
  • allouer un buffer ;
  • effectuer une opération ;
  • valider le résultat ;
  • libérer les ressources.
Séparer correctement ces responsabilités rend le chemin d'erreur beaucoup plus facile à analyser.

C'est particulièrement utile avec :
  • descripteurs de fichiers ;
  • handles Windows ;
  • sockets ;
  • allocations mémoire ;
  • mappings ;
  • threads ;
  • objets de synchronisation.
15. Nommer correctement

Le nom d'une variable ou d'une fonction constitue une forme de documentation.

Un bon nom permet de comprendre le rôle de l'élément sans rechercher immédiatement son origine.

Mauvais :

Code: Select all

int x;
int tmp;
int a2;
Meilleur :

Code: Select all

int retryCount;
std::size_t bufferSize;
bool connectionClosed;
Le nom doit être suffisamment précis sans devenir absurdement long.

16. Choisir un bon nom

Un bon nom décrit l'intention et non simplement le type.

Par exemple :

Code: Select all

std::size_t fileSize;
est généralement plus informatif que :

Code: Select all

std::size_t number;
Pour les fonctions, un verbe décrivant l'action est souvent naturel :

Code: Select all

readHeader()
validateBuffer()
calculateSize()
Pour une valeur booléenne, un nom qui se lit comme une condition améliore la compréhension :

Code: Select all

isValid
hasData
shouldRetry
17. Éviter les noms trop courts

Les noms d'une lettre ne sont pas toujours mauvais.

Dans une petite boucle :

Code: Select all

for (int i = 0; i < count; ++i)
`i` est immédiatement compréhensible.

Mais une variable utilisée pendant 80 lignes et appelée `x` oblige le lecteur à mémoriser sa signification.

Plus la portée et la durée de vie d'une variable sont grandes, plus son nom devrait être explicite.

18. Éviter les noms trompeurs

Le pire nom n'est pas forcément un nom court : c'est un nom qui fait croire quelque chose de faux.

Une variable appelée :

Code: Select all

bufferSize
ne devrait pas contenir parfois une taille et parfois le nombre d'éléments.

De même, une fonction appelée :

Code: Select all

checkFile()
ne devrait pas modifier silencieusement le fichier.

Le nom doit correspondre au comportement réel.

19. Conventions de nommage

Plusieurs conventions existent.

Exemples :

Code: Select all

bufferSize
BufferSize
buffer_size
Aucune convention universelle ne rend automatiquement le code meilleur.

Le point essentiel est la cohérence.

Dans un projet existant, il est généralement préférable de suivre la convention du projet plutôt que d'imposer sa préférence personnelle.

Cela concerne :
  • variables ;
  • fonctions ;
  • types ;
  • constantes ;
  • membres ;
  • namespaces.
20. Préfixes

Certaines conventions utilisent des préfixes pour communiquer une information supplémentaire.

Historiquement, différents projets C/C++ ont utilisé des préfixes indiquant :
  • un membre ;
  • une variable statique ;
  • une constante ;
  • un type particulier.
Ces conventions peuvent être utiles si toute l'équipe les comprend.

Il ne faut cependant pas accumuler des préfixes uniquement par habitude. Les outils modernes et le système de types fournissent déjà beaucoup d'informations.

21. Utiliser les fonctionnalités du langage pour exprimer l'intention

Le C++ possède des mécanismes qui permettent au compilateur et au lecteur de comprendre certaines intentions.

Au lieu d'écrire une intention uniquement dans un commentaire, il est souvent préférable de l'exprimer directement dans le langage lorsque c'est possible.

C'est une idée fondamentale :

Une contrainte vérifiée par le compilateur est généralement plus fiable qu'une contrainte écrite uniquement dans un commentaire.

22. Utiliser const

`const` indique qu'une valeur ne doit pas être modifiée par l'intermédiaire considéré.

Exemple :

Code: Select all

const int maxRetries = 5;
Le lecteur sait immédiatement que cette valeur n'est pas destinée à changer.

Le compilateur peut également empêcher certaines modifications accidentelles.

`const` sert donc à la fois :
  • à documenter l'intention ;
  • à renforcer les contraintes du programme.
23. Const-correctness

La const-correctness consiste à utiliser `const` de manière cohérente lorsque les données ne doivent pas être modifiées.

Pour un pointeur :

Code: Select all

const std::byte* data;
cela exprime que les octets pointés sont considérés en lecture seule par cette interface.

Dans du code manipulant des buffers, cette information est très précieuse.

Le lecteur peut immédiatement distinguer conceptuellement :

Code: Select all

const std::byte* input;
std::byte* output;
Le premier représente naturellement une source en lecture, le second une zone susceptible d'être modifiée.

24. Les constantes nommées

Les valeurs littérales dispersées dans le code peuvent rendre le programme difficile à comprendre.

Exemple :

Code: Select all

if (retryCount >= 5)
Pourquoi `5` ?

Une constante nommée communique l'intention :

Code: Select all

constexpr int maxRetryCount = 5;
puis :

Code: Select all

if (retryCount >= maxRetryCount)
Le lecteur comprend immédiatement le rôle de la valeur.

25. Les nombres magiques

Un nombre magique est une valeur apparaissant dans le code sans que sa signification soit évidente.

Exemple :

Code: Select all

buffer.resize(4096);
4096 peut représenter :
  • une taille de page ;
  • une taille de bloc ;
  • une limite arbitraire ;
  • une taille imposée par un format.
Si cette signification est importante, elle doit être exprimée.

Par exemple :

Code: Select all

constexpr std::size_t blockSize = 4096;
Cela devient encore plus important avec les flags et formats binaires.

Un code bas niveau rempli de :

Code: Select all

0x20
0x40
0x1000
0x2000
devient rapidement très difficile à auditer si aucune signification n'est attachée à ces valeurs.

26. constexpr et les constantes modernes

Lorsque la valeur peut être connue à la compilation, `constexpr` permet d'exprimer cette propriété.

Exemple :

Code: Select all

constexpr std::size_t headerSize = 64;
Cela indique explicitement que `headerSize` est une constante de compilation.

Toutes les constantes n'ont pas besoin d'être globales. Une constante utilisée uniquement dans une fonction peut rester locale afin de limiter sa portée.

27. Références et pointeurs

Les références et pointeurs communiquent des intentions différentes.

Une référence :

Code: Select all

void process(Buffer& buffer);
exprime généralement qu'un objet existant est utilisé directement.

Un pointeur :

Code: Select all

void process(Buffer* buffer);
peut suggérer notamment :
  • qu'une valeur nulle est possible ;
  • qu'une sémantique particulière est nécessaire ;
  • qu'on travaille naturellement avec une adresse.
Il ne faut pas remplacer mécaniquement tous les pointeurs par des références.

En programmation système, les pointeurs restent indispensables.

La question est plutôt :

Quelle représentation exprime le mieux le contrat réel de l'interface ?

28. Références const

Une référence constante est particulièrement utile lorsqu'on veut recevoir un objet sans le copier et sans le modifier :

Code: Select all

void inspect(const Header& header);
Elle communique plusieurs informations :
  • l'objet doit exister ;
  • la fonction travaille sur l'objet original ;
  • elle n'est pas censée le modifier par cette référence.
Cela rend l'interface plus expressive.

29. Pointeurs en programmation bas niveau

Dans le code système, les pointeurs sont naturels lorsqu'on manipule :
  • buffers ;
  • mémoire brute ;
  • structures fournies par une API ;
  • zones mappées ;
  • adresses ;
  • interfaces C ;
  • structures de données internes.
Le style doit alors rendre leur utilisation aussi explicite que possible.

Il faut pouvoir répondre rapidement à :
  • le pointeur peut-il être nul ?
  • qui possède la mémoire ?
  • quelle est la taille accessible ?
  • la mémoire est-elle modifiable ?
  • combien de temps reste-t-elle valide ?
Ces questions sont beaucoup plus importantes que la simple esthétique du code.

30. Ownership et durée de vie

L'ownership désigne la responsabilité de gérer la durée de vie d'une ressource.

Lorsqu'une fonction reçoit une adresse, le lecteur doit idéalement comprendre si elle :
  • emprunte simplement la ressource ;
  • prend possession de la ressource ;
  • peut conserver l'adresse après son retour ;
  • doit libérer quelque chose.
Une grande partie des bugs C++ bas niveau provient d'une mauvaise compréhension des durées de vie.

Le style, les types, les noms et la structure du programme doivent donc rendre ces relations aussi évidentes que possible.

31. Exceptions personnalisées

Le C++ permet de créer des types d'exceptions représentant des erreurs spécifiques.

Conceptuellement, une erreur précise est plus informative qu'une erreur générique.

Le lecteur peut comprendre immédiatement la catégorie de problème rencontrée.

Cependant, en programmation système, l'utilisation des exceptions dépend fortement du projet.

Certains environnements C++ les utilisent largement. D'autres, notamment certains composants très bas niveau, préfèrent :
  • codes d'erreur ;
  • valeurs de retour ;
  • types représentant explicitement succès ou erreur.
Le principe du chapitre reste valable : utiliser le langage pour communiquer précisément la nature de l'erreur.

32. Formatage

Le formatage n'affecte généralement pas le comportement du programme, mais influence fortement sa lisibilité.

Exemple compact :

Code: Select all

if(valid){process(data);}
Version plus lisible :

Code: Select all

if (valid) {
    process(data);
}
Le but est de permettre au lecteur d'identifier immédiatement la structure du programme.

33. Indentation

L'indentation montre visuellement l'imbrication.

Exemple :

Code: Select all

if (connected) {
    if (hasData) {
        process();
    }
}
La structure est immédiatement visible.

Une indentation incohérente peut faire croire qu'une instruction appartient à un bloc alors que ce n'est pas le cas.

34. Accolades

Plusieurs styles d'accolades existent.

Exemple :

Code: Select all

if (condition) {
    action();
}
ou :

Code: Select all

if (condition)
{
    action();
}
Les deux sont valides.

Le choix du style est beaucoup moins important que sa cohérence dans le projet.

Pour les structures de contrôle, utiliser des accolades même lorsqu'une seule instruction est présente peut éviter certaines erreurs lors de modifications ultérieures.

Exemple :

Code: Select all

if (condition) {
    action();
}
Si une deuxième instruction est ajoutée, le bloc reste explicite.

35. Espaces et parenthèses

Les espaces doivent faciliter la lecture.

Exemple :

Code: Select all

result = a + b * c;
est plus facile à lire qu'une succession compacte de caractères.

Les parenthèses peuvent également être utilisées pour rendre l'ordre logique plus évident, même lorsque les règles de priorité des opérateurs suffiraient techniquement.

Le but n'est pas de montrer qu'on connaît par cœur toute la table de précédence : le but est que le lecteur comprenne immédiatement l'expression.

36. Tabs et espaces

Tabs contre espaces est principalement une convention de projet.

Le véritable problème apparaît lorsque plusieurs développeurs utilisent des configurations incompatibles et produisent un code dont l'alignement change selon l'éditeur.

Une équipe doit donc définir une règle commune ou utiliser un outil de formatage automatique.

Le choix précis est secondaire.

37. Retours à la ligne

Les systèmes utilisent historiquement différentes représentations des fins de ligne.

Cela peut provoquer des différences inutiles dans les outils de versionnement lorsqu'un fichier entier semble avoir changé uniquement à cause de son format de fin de ligne.

Les éditeurs et systèmes de contrôle de version modernes permettent généralement de gérer cette situation.

Pour un projet multiplateforme, il faut éviter de modifier accidentellement toutes les fins de ligne d'un fichier.

38. Formatage automatique

Les outils de formatage permettent d'appliquer automatiquement une convention.

C'est souvent préférable à des discussions interminables sur :
  • l'emplacement des accolades ;
  • le nombre d'espaces ;
  • l'indentation ;
  • les retours à la ligne.
Une convention automatisée permet aux développeurs de se concentrer sur les problèmes techniques.

39. Les défis stylistiques dans les vrais projets

Dans un projet réel, il est rarement possible d'obtenir un code parfaitement uniforme immédiatement.

On peut rencontrer :
  • du vieux code ;
  • plusieurs générations de développeurs ;
  • des bibliothèques tierces ;
  • des conventions historiques ;
  • différents compilateurs ;
  • différents systèmes d'exploitation.
Il faut donc distinguer ce qui mérite réellement d'être corrigé de ce qui constitue simplement une préférence personnelle.

40. Ne pas réécrire inutilement du code uniquement pour le style

Modifier énormément de lignes uniquement pour changer une convention peut :
  • rendre les revues de code difficiles ;
  • polluer l'historique du projet ;
  • créer des conflits ;
  • masquer les véritables changements fonctionnels.
Si une fonction doit être modifiée pour une raison technique, il peut être raisonnable d'améliorer localement sa lisibilité.

Mais reformater arbitrairement une énorme codebase sans raison doit être évité.

41. Respecter le style d'une codebase existante

Lorsqu'on contribue à un projet, la cohérence collective est généralement plus importante que sa préférence personnelle.

Si un projet utilise une convention donnée, il est préférable de la suivre.

Un développeur professionnel doit pouvoir travailler dans plusieurs styles.

Le style est un outil au service de la compréhension, pas une identité personnelle.

42. Application directe au C++ système et bas niveau

Dans le développement système, la lisibilité est une mesure de sécurité et de fiabilité.

Un code peut manipuler simultanément :
  • adresses mémoire ;
  • tailles ;
  • offsets ;
  • flags ;
  • handles ;
  • buffers ;
  • threads ;
  • états de ressources.
Chaque information ambiguë augmente le risque d'erreur.

43. Toujours associer un buffer à sa taille

Lorsqu'on manipule de la mémoire brute, l'adresse seule ne suffit souvent pas.

Conceptuellement :

Code: Select all

data + dataSize
est beaucoup plus informatif qu'une adresse dont la taille accessible est inconnue.

Dans une fonction travaillant sur un buffer, il faut toujours savoir :
  • où commence le buffer ;
  • combien d'octets sont valides ;
  • combien d'octets sont alloués ;
  • si le buffer peut être modifié.
La taille logique et la capacité physique ne sont pas nécessairement identiques.

44. Distinguer taille en octets et nombre d'éléments

C'est une source classique de bugs.

Une variable appelée :

Code: Select all

size
peut être ambiguë.

Des noms comme :

Code: Select all

byteCount
elementCount
bufferCapacity
bytesRead
communiquent beaucoup mieux l'unité représentée.

Dans du code bas niveau, l'unité d'une valeur fait partie de sa signification.

45. Les offsets doivent être explicites

Lorsqu'on analyse un format binaire, plusieurs valeurs numériques peuvent représenter :
  • une adresse ;
  • un offset depuis le début du fichier ;
  • un offset depuis une structure ;
  • une taille ;
  • un index.
Utiliser des noms génériques augmente considérablement le risque de mélanger ces concepts.

Exemple :

Code: Select all

headerOffset
sectionOffset
fileOffset
virtualAddress
rawSize
Le nom aide à prévenir des erreurs logiques.

46. Isoler les opérations dangereuses

Une opération utilisant :
  • casts bas niveau ;
  • arithmétique de pointeurs ;
  • mémoire brute ;
  • API système complexe ;
  • conversion de représentation ;
devrait idéalement être isolée dans une zone claire du programme.

Cela permet de concentrer l'audit et les vérifications.

Au lieu de répandre des conversions partout, une petite interface bien définie peut centraliser les hypothèses nécessaires.

47. Vérifier les limites avant l'accès

Lorsqu'un programme analyse des données externes, il ne faut jamais supposer qu'elles sont valides.

Avant de lire une structure ou un champ, il faut conceptuellement vérifier que la zone correspondante appartient bien au buffer disponible.

Par exemple, avant de lire N octets à partir d'un offset, il faut s'assurer que cette plage est contenue dans les données disponibles.

Cette règle est fondamentale pour :
  • parseurs binaires ;
  • protocoles réseau ;
  • formats de fichiers ;
  • IPC ;
  • données provenant d'une API non fiable.
48. Nommer les flags

Les flags numériques doivent être représentés par des noms significatifs lorsque cela est possible.

Mauvais :

Code: Select all

if (flags & 0x20)
Le lecteur doit connaître la signification de `0x20`.

Meilleur conceptuellement :

Code: Select all

if (flags & executableFlag)
Le sens devient immédiatement visible.

49. Gérer clairement les ressources

Une fonction système peut acquérir plusieurs ressources.

Le lecteur doit pouvoir déterminer rapidement :
  • où chaque ressource est acquise ;
  • qui en est responsable ;
  • quand elle est libérée ;
  • ce qui arrive en cas d'erreur.
Un code où une ressource est créée à la ligne 20 et libérée à la ligne 400 est beaucoup plus difficile à auditer.

Réduire les portées et utiliser des abstractions de durée de vie appropriées simplifie énormément le raisonnement.

50. Limiter la portée des variables

Une variable devrait généralement être déclarée dans la portée la plus petite qui permet son utilisation.

Cela réduit :
  • le nombre d'endroits pouvant la modifier ;
  • la durée pendant laquelle son état doit être mémorisé ;
  • les risques de réutilisation accidentelle.
Cette règle est particulièrement utile pour les pointeurs temporaires et les ressources.

51. Éviter les états implicites

Un programme devient difficile à comprendre lorsque le comportement d'une fonction dépend de nombreuses variables globales ou états cachés.

Plus les dépendances sont explicites, plus le code est facile à raisonner.

Une fonction dont les données nécessaires sont visibles dans son interface est généralement plus facile à tester et déboguer.

Dans le code système, certains états globaux sont parfois nécessaires, mais ils doivent rester contrôlés.

52. Commenter les hypothèses bas niveau

Certains commentaires sont extrêmement utiles en programmation système.

Exemples conceptuels :

Code: Select all

// La structure est garantie valide jusqu'au retour de cette fonction.

// L'offset est relatif au début du mapping.

// Cette opération doit être exécutée sous le verrou.

// La mémoire est fournie par l'appelant et ne doit pas être libérée ici.
Ces commentaires décrivent des contraintes invisibles dans les instructions elles-mêmes.

C'est exactement le type de documentation qui possède une forte valeur.

53. Commenter le pourquoi des contraintes système

Lorsqu'une opération inhabituelle est nécessaire à cause d'une API, d'un ABI ou d'un format, documenter la raison évite qu'un futur développeur ne la supprime.

Exemple conceptuel :

Code: Select all

// Conserve cet alignement : le format binaire impose une frontière
// de 8 octets pour cette structure.
Le commentaire protège ici une connaissance importante.

54. Le style et le débogage

Un programme correctement décomposé facilite énormément le débogage.

Si une opération est divisée en étapes clairement nommées, il devient plus facile de déterminer :
  • quelle étape échoue ;
  • quelles données étaient valides avant l'erreur ;
  • quelle ressource était détenue ;
  • quel invariant a été violé.
Les fonctions gigantesques rendent les breakpoints, traces et analyses beaucoup plus difficiles.

55. Le style et la performance

Écrire du code lisible ne signifie pas abandonner les performances.

Il faut d'abord exprimer clairement l'algorithme et les invariants.

Lorsqu'une optimisation est réellement nécessaire, elle peut rendre le code moins évident. Dans ce cas, un commentaire expliquant :
  • pourquoi l'optimisation existe ;
  • quelle hypothèse elle utilise ;
  • quelle mesure a justifié son introduction ;
peut être extrêmement utile.

Une optimisation obscure sans justification risque d'être supprimée ou modifiée incorrectement.

56. Style C++ et interfaces C / système

Le C++ système communique fréquemment avec des interfaces C.

Cela implique souvent :
  • pointeurs ;
  • structures ;
  • buffers ;
  • codes d'erreur ;
  • ressources représentées par des handles.
Le rôle du code C++ peut être de construire une couche plus claire autour de ces primitives tout en conservant leur contrôle bas niveau.

L'objectif n'est pas de cacher aveuglément le système, mais de rendre explicites :
  • les durées de vie ;
  • les erreurs ;
  • les invariants ;
  • les responsabilités.
57. Ce qu'il faut retenir absolument

1. Le code est écrit pour être lu.

Un programme qui fonctionne mais que personne ne comprend devient rapidement difficile à maintenir.

2. Les commentaires expliquent surtout le pourquoi.

Ne répète pas simplement l'instruction.

3. Un commentaire faux est dangereux.

La documentation doit évoluer avec le code.

4. Préfère rendre le code clair avant de le commenter.

Un bon nom ou une meilleure décomposition peut supprimer le besoin d'un commentaire.

5. Décompose les gros problèmes.

Chaque fonction devrait représenter une responsabilité compréhensible.

6. Les noms portent de l'information.

`byteCount` est plus précis que `n`.

7. Utilise const lorsque l'intention est la lecture seule.

Le compilateur peut ainsi faire respecter une partie du contrat.

8. Évite les nombres magiques.

Donne un nom aux valeurs possédant une signification métier ou technique.

9. Référence et pointeur ne communiquent pas exactement la même chose.

Choisis la représentation correspondant au contrat réel.

10. En bas niveau, ownership et durée de vie doivent être évidents.

Il faut savoir qui possède une ressource et combien de temps elle reste valide.

11. Pour chaque buffer, connais l'adresse et la taille.

Une adresse sans limites connues est dangereuse.

12. Précise les unités.

Octets, éléments, offsets et adresses ne doivent pas être confondus.

13. La cohérence du projet est plus importante que tes préférences de formatage.

Tabs contre espaces ou emplacement des accolades sont secondaires.

14. Isole les opérations dangereuses.

Les casts, calculs d'adresses et manipulations de mémoire brute doivent rester faciles à identifier et auditer.

15. En programmation système, le bon style contribue directement à la robustesse.

Quand le programme manipule directement mémoire et ressources système, rendre les hypothèses explicites réduit les erreurs humaines.

Résumé simplifié

Si tu ne devais retenir que la logique générale de ce chapitre :

Code: Select all

Code clair
    |
    +-- noms précis
    |
    +-- fonctions avec une responsabilité claire
    |
    +-- commentaires = pourquoi
    |
    +-- const pour exprimer l'immuabilité
    |
    +-- constantes nommées
    |
    +-- ownership et durée de vie explicites
    |
    +-- buffers accompagnés de leurs tailles
    |
    +-- formatage cohérent
    |
    +-- opérations dangereuses isolées
En C++ bas niveau, pose-toi régulièrement ces questions :
  • Est-ce que je comprends immédiatement ce que représente cette variable ?
  • Cette taille est-elle en octets ou en éléments ?
  • Ce pointeur peut-il être nul ?
  • Qui possède cette mémoire ?
  • Combien de temps cette adresse reste-t-elle valide ?
  • Cette fonction modifie-t-elle les données ?
  • Cette valeur numérique possède-t-elle un nom ?
  • Cette fonction fait-elle trop de choses ?
  • Ce commentaire explique-t-il réellement quelque chose ?
  • Un autre développeur pourrait-il modifier ce code sans casser une hypothèse invisible ?
Si les réponses sont évidentes en lisant le programme, le code est déjà beaucoup plus proche d'un C++ professionnel, maintenable et adapté au développement système.

Return to “Bases du C++”

Who is online

Users browsing this forum: No registered users and 0 guests