La complexité cyclomatique d’une fonction est le nombre de régions visibles dans son logigramme. Cet indicateur a été introduit en 1976 par Thomas McCabe. Il sert notamment à évaluer le nombre de tests à effectuer pour tester unitairement une fonction. Et plus une fonction a une complexité cyclomatique élevée, moins elle est facile à maintenir.
Cas trivial
Prenons le cas trivial d’une fonction qui ne fait rien. Sa complexité cyclomatique est de 1. En effet, si on dessine son logigramme, le nombre de régions délimitées est de 1.
void fonctionTriviale() {
// je ne fais rien
}
Autre exemple
La fonction suivante insère une chaine de caractères dans une liste ordonnée par les nombres d’occurences. Par exemple si l’on veut insérer la chaine « Guyancourt » dans la liste suivante:
{ "Saclay", 2 }, { "Guyancourt", 2}, { "Chevreuse", 1 }
on va obtenir le résultat suivant:
{ "Guyancourt", 3 }, { "Saclay", 2}, { "Chevreuse", 1 }
void ajouteChaineDansListe(
std::list<std::pair<std::string, int>>& liste,
const std::string& chaine )
{
// Tout d'abord, on cherche la chaine dans la liste
auto iterator = std::find_if(liste.begin(),
liste.end(),
[chaine](auto a)
{ return (a.first == chaine); });
if (liste.end() == iterator /* A */) {
/* A OUI */
// Si on ne l'a pas trouvée on l'ajoute en
// fin de liste
liste.push_back(std::pair<std::string, int>
(chaine, 1));
}
else {
/* A NON */
// Si on l'a trouvée on augmente son compteur
iterator->second ++;
while (liste.begin() != iterator /* B */)
{
/* B OUI */
// Si on est pas en début de liste,
// on compare avec la valeur du
// compteur précédent
auto previous = --iterator++;
if (previous->second < iterator->second /* C */) {
/* C OUI */
// Si notre compteur est
// supérieur au compteur
// précédent, alors on
// intervertit les deux
// éléments de la liste
auto tempPair = *iterator;
*previous = tempPair;
*iterator = *previous;
--iterator;
}
else {
/* C NON */
// si le compteur précédent
// est supérieur, alors
// notre élément est à la
// bonne place. Nous pouvons
// sortir de la fonction.
iterator = liste.begin();
}
}
/* B NON */
}
}
Comme on le voit sur le logigramme, il y a quatre régions délimitées. La complexité cyclomatique de la fonction est donc de 4
Problèmes en cas de grande complexité cyclomatique
Si une fonction a une grande complexité cyclomatique, on peut s’attendre aux problèmes suivants:
- par construction, il va falloir de nombreux tests pour la valider intégralement. C’est pourquoi d’ailleurs de telles fonctions sont rarement testées unitairement par les développeurs.
- la fonction est un nid à bug. En effet, il y a tellement de cas, de branchements de code, qu’un développeur devant apporter une modification dans la fonction verra difficilement tous les endroits où il doit agir. D’autre part, il devra anticiper les conséquences de ses changements dans toutes les branches conditionnelles de la fonction. Cela fait de nombreux cas à traiter
- le risque accru de fuites mémoire ou plus généralement, de non libération des ressources. En effet plus la complexité cyclomatique est grande, plus il y de branches dans une fonction. Donc si une ressource locale (mémoire, fichier, etc. ) est allouée dans une branche, il faut penser à la libérer dans toutes les branches suivantes menant à la sortie de la fonction
La norme en C++ est de dire qu’au delà d’une complexité cyclomatique de 10, la fonction est trop compliquée, il faut la simplifier.
Faiblesse de l’indice
La limite de la complexité cyclomatique et qu’elle ne permet pas de dire si une fonction va être facilement compréhensible par un développeur. Parce que après tout, si une fonction donne de bons résultats, le fait qu’elle ait de nombreuses branches conditionnelles ne pose pas de problème au processeur, qui l’exécute.
Par contre, quand il faut la modifier, la chose importante est avant tout la capacité du développeur à la comprendre. Or l’éventuelle difficulté à comprendre ne dépend pas forcément du nombre de branches. Pour s’en assurer prennons l’exemple du switch case
.
Exemple du switch
Considérons le code suivant:
switch (valeurEntiere) {
case 1:
// fais ceci
break;
case 2:
// fais cela
break;
case 3:
default:
// fais ce qu'il te plait
break;
}
Ce bout de code a une complexité cyclomatique de 3, et pourtant il est plus simple à comprendre qu’un autre qui aurait une CC de 2.
C’est pourquoi notre partenaire SONAR a développé un autre indice, appelé complexité cognitive, pour évaluer la difficulté de compréhension d’une fonction.