Blog 8 min de lecture

Écrire du code C efficace : la leçon de Linus Torvalds

Share this article
Écrire du code C efficace : la leçon de Linus Torvalds

Chaque projet a son guide de style : un ensemble de conventions sur la façon dont le code doit être écrit. Certains responsables choisissent des règles de codage basiques, tandis que d'autres préfèrent des règles plus avancées. Dans de nombreux projets, cependant, aucune règle de codage n'est définie, et chaque développeur utilise son propre style.

Il est beaucoup plus facile de comprendre une grande base de code lorsque tout le code qu'elle contient est écrit dans un style cohérent.

Il existe de nombreuses ressources sur les bonnes pratiques de codage. On peut apprendre de bonnes règles de codage en :

  • Lisant un livre ou un magazine.
  • Utilisant des ressources en ligne.
  • Apprenant auprès d'un collègue.
  • Suivant une formation.

On peut aussi travailler avec un expert pendant quelques mois pour améliorer les compétences de codage de l'équipe. Cependant, trouver la bonne personne n'est pas facile, et cela peut coûter cher à l'entreprise. Mais pourquoi chercher un expert quand on peut apprendre auprès de développeurs d'exception comme Linus Torvalds ? La simple exploration du code source qu'il a développé ou maintenu peut fournir des enseignements précieux sur la façon d'écrire du code C efficace.

Linus Torvalds est largement reconnu pour avoir créé le noyau Linux — et avoir été pendant de nombreuses années son développeur principal — ainsi que pour avoir créé le très populaire système de gestion de versions distribué Git. Rien que cela rend son code digne d'être étudié :)

Dans le code source de Git

Jetons un œil à un extrait de code de Git :

Voici quelques observations sur ce code :

  • Les fonctions sont déclarées static.
  • Les fonctions retournent un code d'erreur.
  • Les fonctions ont peu de paramètres.
  • La fonction se termine le plus tôt possible.
  • Les variables sont déclarées static.
  • Les noms des variables sont faciles à comprendre.
  • Les fonctions sont très courtes.
  • Le code est bien indenté.
  • Pas de commentaires superflus dans le corps : le code parle de lui-même.
  • Les corps de fonctions sont bien indentés.
  • Les gardes d'inclusion (define guards) sont claires.

En parcourant le code source de Git, on constate à quel point il est implémenté de manière cohérente : les mêmes bonnes pratiques sont appliquées partout. Pour le vérifier, recherchons les fonctions statiques :

from m in Methods where m.IsStatic select m

Le treemap est très utile pour avoir une vue d'ensemble des éléments de code correspondant à une requête CQLinq ; les rectangles bleus représentent les résultats.

Presque toutes les fonctions sont déclarées static, elles ne sont donc visibles que dans l'unité de traduction où elles sont déclarées.

Dans le code source du noyau Linux

Passons au code source de Linux et examinons l'implémentation de la fonction suivante :

linux11

Le code semble très propre. En particulier, la fonction

  • n'a que quelques lignes de code.
  • Sa signature est bien définie.
  • Elle est bien commentée.
  • Le code est bien indenté.
  • Les noms des variables sont très clairs.
  • La const-correctness est respectée.
  • Elle vérifie les paramètres d'entrée et émet un avertissement s'ils ne remplissent pas certaines conditions.

Un autre développeur pourrait implémenter la même fonction comme ceci :

linux12

Le style de codage a un impact majeur sur la lisibilité du code source. Investir quelques heures dans la formation des développeurs et mener des revues de code périodiques peut rendre le code beaucoup plus facile à maintenir et à faire évoluer.

Explorons le code source du noyau Linux avec CppDepend et découvrons quelques règles de codage de base adoptées par ses développeurs.

Modularité

La modularité est une technique de conception logicielle qui consiste à composer un logiciel à partir de parties distinctes ; le code modulaire est plus facile à gérer et à maintenir.

Pour un langage procédural comme le C, qui ne dispose pas de constructions logiques telles que les espaces de noms, les composants ou les classes, la modularité peut être obtenue grâce aux répertoires et aux fichiers.

Voici quelques scénarios possibles :

  • Mettre tous les fichiers sources dans un seul répertoire.
  • Isoler les fichiers liés à un module ou un sous-module dans un répertoire spécifique.

Dans le cas du noyau Linux, des répertoires et sous-répertoires sont utilisés pour modulariser le code source du noyau.

linux15Encapsulation

L'encapsulation consiste à masquer les fonctions et les données internes à une implémentation. En C, l'encapsulation s'effectue à l'aide du mot-clé static. Ces entités sont appelées fonctions et variables à portée de fichier.

Recherchons toutes les fonctions statiques en exécutant la requête CQLinq suivante :

linux17

Nous pouvons utiliser la vue Metric pour avoir une bonne idée du nombre de fonctions concernées. Dans la vue Metric, la base de code est représentée par un treemap. Le treemapping est une méthode d'affichage de données arborescentes à l'aide de rectangles imbriqués. L'arborescence utilisée dans un treemap CppDepend est la hiérarchie de code habituelle :

  • Les projets contiennent des répertoires.
  • Les répertoires contiennent des fichiers.
  • Les fichiers contiennent des structs, des fonctions et des variables.

La vue treemap offre un moyen pratique de représenter le résultat d'une requête CQLinq, afin de voir visuellement les types concernés par la requête.

linux2

Comme on peut le constater, de nombreuses fonctions sont déclarées static.

Recherchons maintenant les champs statiques :

linux3

Comme pour les fonctions, de nombreuses variables sont déclarées static.

Dans le code source du noyau Linux, l'encapsulation est utilisée chaque fois que des fonctions et des variables doivent rester privées à la portée du fichier.

Utilisez des structs pour stocker votre modèle de données

En programmation C, les fonctions utilisent des variables pour effectuer leurs traitements ; ces variables peuvent être :

  • Des variables statiques.
  • Des variables globales.
  • Des variables locales.
  • Des variables issues de structs.

Chaque projet a son modèle de données, qui peut être utilisé par de nombreux fichiers sources. Utiliser des variables globales est une option, mais il est généralement préférable de regrouper les données liées dans des structs.

Recherchons les variables globales de type primitif :

linux4

Seules quelques variables sont concernées, et certaines d'entre elles pourraient peut-être être regroupées dans des structs, comme (elfcorehdr_addr et elfcorehdr_size) ou (pm_freezing et pm_nosig_freezing).

Gardez des fonctions courtes et efficaces

Voici quelques conseils sur la longueur des fonctions, issus de la page web Linux coding style:

Functions should be short and sweet, and do just one thing.  They should
fit on one or two screenfuls of text (the ISO/ANSI screen size is 80x24,
as we all know), and do one thing and do that well.

The maximum length of a function is inversely proportional to the
complexity and indentation level of that function.  So, if you have a
conceptually simple function that is just one long (but simple)
case-statement, where you have to do lots of small things for a lot of
different cases, it's OK to have a longer function.

Recherchons les fonctions de plus de 30 lignes de code.

linux14

Seules quelques méthodes dépassent 30 lignes de code.

Nombre de paramètres des fonctions

Les fonctions où NbParameters > 8 peuvent être pénibles à appeler et dégrader les performances. Une autre solution consiste à fournir une structure dédiée au passage des arguments.

linux7

Seules deux fonctions ont plus de huit paramètres.

Nombre de variables locales

Les méthodes où NbVariables est supérieur à 8 peuvent être difficiles à comprendre et à maintenir. Celles où NbVariables est supérieur à 15 sont extrêmement complexes et devraient être scindées en méthodes plus petites, sauf si elles sont générées automatiquement par un outil.

linux9

Seules cinq fonctions ont plus de 15 variables locales.

Évitez de définir des fonctions complexes

De nombreuses métriques peuvent être utilisées pour identifier les fonctions complexes ; NbLinesOfCode, le nombre de paramètres et le nombre de variables locales figurent parmi les plus basiques.

Il existe aussi d'autres métriques utiles pour identifier les fonctions complexes :

  • La complexité cyclomatique est une métrique logicielle procédurale populaire, égale au nombre de décisions qui peuvent être prises dans une procédure.
  • La profondeur d'imbrication (Nesting Depth) est une métrique définie sur les méthodes, correspondant à la profondeur maximale de la portée la plus imbriquée dans le corps d'une méthode.
  • Max Nested Loops correspond au niveau maximal d'imbrication des boucles dans une fonction.

Les valeurs maximales acceptables pour ces métriques dépendent largement des préférences de l'équipe ; il n'existe pas de seuils universels.

Recherchons les fonctions candidates à une refactorisation :

linux8

Très peu de fonctions peuvent être considérées comme complexes.

Conventions de nommage

Il n'existe pas de convention de nommage universelle ; chaque projet peut adopter celle qui convient le mieux à ses besoins. L'essentiel est d'appliquer la convention choisie de manière cohérente.

Dans le cas de Linux, les structs doivent commencer par une lettre minuscule, et nous pouvons vérifier si c'est le cas pour l'ensemble du code source du noyau — exécutons la requête suivante :

linux5

Seuls quatre structs commencent par « _ » au lieu d'une lettre minuscule.

Indentation

L'indentation est très utile pour rendre le code facile à lire ; voici les motivations qui la sous-tendent, tirées de la page web Linux coding style:

Rationale: The whole idea behind indentation is to clearly define where
a block of control starts and ends.  Especially when you've been looking
at your screen for 20 straight hours, you'll find it a lot easier to see
how the indentation works if you have large indentations.

Now, some people will claim that having 8-character indentations makes
the code move too far to the right, and makes it hard to read on a
80-character terminal screen.  The answer to that is that if you need
more than 3 levels of indentation, you're screwed anyway, and should fix
your program.
 Conclusion

Explorer des projets open source bien connus est un excellent moyen d'améliorer ses compétences en programmation, surtout lorsque ces projets sont développés et maintenus par des experts. Inutile de télécharger et de compiler le projet — vous pouvez simplement parcourir le code sur GitHub.

Share this article