Pourquoi les outils de développement C++ sont ils moins aboutis que pour Java ou C# ?

Tout programme informatique est composé de séquence d’opérations logiques et arithmétiques, même les plus complexes (google, siri, elbotwatson…). Il est humainement impossible d’écrire ces séquences d’instructions à la main : on utilise des langages de programmations pour expliquer aux machines ce qu’il faut faire.

Ces langages sont à la fois compréhensibles par l’homme et la machine. Mais leur manipulation est plus ou moins difficile par chaque interlocuteur. Le langage de programmation C++ est sans aucun doutes l’un des plus complexes à comprendre par un ordinateur (et par le développeur!). Néanmoins, il est considéré par beaucoup comme l’un des langages permettant d’exploiter le plus efficacement la puissance de calcul de la machine. Ce qui explique entre autres que malgré son age (développé dès 1979 par Bjarne Stroustrup) et sa complexité, il reste largement utilisé en entreprise et se positionne dans le top 5 des langages les plus populaires.

L’enjeu : développer plus efficacement

La complexité du C++ du point de vue du développeur est avérée mais parfois contre balancée par sa flexibilité et sa performance. C’est un compromis à faire par rapport aux objectifs ou une contrainte par rapport à l’historique du projet.

Une question que l’on a moins l’habitude de se poser est la complexité du langage du point de vue de la machine. En effet, d’un point de vue très égoïste, il peut sembler que cette question ne nous concerne pas. C’est malheureusement faux!

Il est peut-être nécessaire de définir précisément ce que l’on entends par « compliqué pour la machine ». A minima, pour rendre un langage de programmation utile, il faut un compilateur, c’est-à-dire un logiciel spécial qui traduit le texte du programme écrit ce langage vers du code machine exécutable par un microprocesseur. Dans un environnement de développement moderne, de nombreux outils s’ajoutent au compilateur, afin d’améliorer la productivité des développeurs : coloration syntaxique, auto-complétion, débogage, rétro-architecture, génération de documentation, mesure de métriques, refactoring, analyse statique, etc.

En plus des outils standards cités ci-dessus, il peut y avoir des besoins d’outils spécialisés. Par exemple, lors de nos audits de performance de code, nous mesurons des métriques de code spécifiques et croisons ces informations avec le profil du logiciel. Ou bien, lors de nos missions d’optimisation logicielle, nous effectuons des transformations de code partiellement automatisables, par exemple transformer des tableaux de structures en structures de tableaux peut améliorer la localité des données et donc l’efficacité du cache et au final le temps d’exécution.

Tous ces outils contribuent de manière très significative à améliorer la productivité du développeur, et doivent tous d’une manière ou d’une autre « comprendre » le code. Force est de constater qu’il y a en C++ beaucoup moins d’outils que pour d’autres langages (Java par exemple), et que ces outils sont moins complets et moins performants. De ce fait, la plupart des développeurs C++ n’utilisent par exemple pas d’outils de refactoring. L’explication est simple: il est plus difficile pour l’ordinateur de comprendre du code C++ que du code Java ou C#. Pour comprendre pourquoi certains langages sont plus problématique que d’autres, le paragraphe suivant donne des notions de théorie des langages informatiques.

La théorie des langages pour les nuls

La plupart des outils de compilation, d’analyse de code ou de développement commencent par les trois étapes suivantes:

  1. Analyse lexicale / lexing : séquence de caractères => séquence de mots du langage
  2. Analyse syntaxique / grammaticale / parsing : séquence de mots => arbre syntaxique
  3. Analyse sémantique : vérification et annotation de l’arbre syntaxique, par exemple par rapport aux types (type-checking)

Illustration: considérons le code suivant:

int x = rand();
if(x > 2) x = 0;

L’analyse lexicale donne la séquence de symboles suivante:

KEYWORD_INT  SYM("x")  ASSIGN  SYM("rand")
PAR_OPEN  PAR_CLOSE  SEMICOLON KEYWORD_IF OPEN_PAR
SYM("x")  GT  INT_LITERAL(2) CLOSE_PAR SYM("x")
ASSIGN  INT_LITERAL(0)

L’analyse syntaxique donne ensuite l’arbre suivant

ast

Enfin, l’analyse sémantique va faire le lien entre les différentes variables « x », s’assurer qu’elle a bien été déclarée avant d’être utilisée, que la condition du « if » est bien convertible en « bool », etc. Ce n’est pas l’objet de cet article, mais l’analyse sémantique du C++ est également plus complexe que dans beaucoup de langages.

L’analyse lexicale (découpage en mots) est relativement simple pour tous les langages, l’analyse syntaxique (création de l’arbre) est plus compliquée. Afin de maîtriser cette complexité, toute une théorie des langages, de leur grammaire, et des parseurs (ou analyseur syntaxique) associés a été développée. La plupart des langages de programmations ont des grammaires non-contextuelles et non-ambiguës, c’est-à-dire que l’interprétation d’une séquence de mots peut être comprise de manière unique sans regarder les mots précédents ou suivants. Ces langages sont compréhensibles par un automate, ce qui permet de générer le parseur automatiquement – c’est ce que font bison ou yacc.

Pour fabriquer un parseur Java, il suffit de recopier la grammaire dans un générateur de parseur. Extrait grammaire java (page 599 dans la référence):

Statement:
    Block
    StatementExpression ;
    if ParExpression Statement [ else Statement ]
    assert Expression [ : Expression ] ;
    switch ParExpression { SwitchBlockStatementGroups }
    while ParExpression Statement do Statement while ParExpression ;
    ...

Le cas épineux du C++

Pour le C++, les choses sont loin d’être aussi simples. En effet, c’est un langage contextuel, c’est-à-à-dire que l’arbre correspondant à une séquence de mots dépends du contexte. Exemples:

  • x * y
    • La multiplication des variables x et y : int x; x * y;
    • La déclaration de la variable y de type pointeur sur le type y : typedef int x; x * y;
  • x(x)
    • L’initialisation d’un membre : struct C { int x; C(int x) : x(x) {} };
    • L’appel fonction sur elle-même : template<typename T> void x(T arg) {}; x(x);
  • class
    • Déclare une classe : class C { };
    • Spécifie un type paramétrique (pas forcément une classe) : template<class T> void f();
  • a < b > c( );
    • Un calcul booléen : bool a,b; bool c() { return true; }; a < b > c( );
    • La déclaration d’un objet paramétrique : template<class T> class a { }; typedef int b; a < b > c( );

Ces ambiguïtés rendent bien plus ardue l’analyse d’un code C++, et ne sont pas présentes en Java ou C#. C’est l’une des raisons pour laquelle bien que la demande d’outillage soit similaire pour ces langages, les outils sont moins aboutis car plus coûteux pour le C++. Illustration en chiffres:

  • Longueur de la description standard du langage
    • Java 195 958 mots
    • C++ 454 531 mots
  • Coûts estimé (ohloh) du développement de compilateurs
    • Java Flex 3.7M$
    • Java JKit 1.2M$
    • C++ GNU 108M$
    • C++ LLVM 20M$
  • Résultat d’un recherche google « refactoring » (requête le 20/02/2014 en anglais et sans cookies google)

Conclusion

Toujours à la recherche de plus d’efficacité, dans la practice HPC d’ANEO nous nous sommes penché sur la question de l’outillage du C++. En effet, nous menons régulièrement des missions d’audit ou d’optimisation de logiciels de traitement d’information ou de simulation. Nous intervenons parfois sur des codes C++ de plusieurs millions de lignes de code, et souvent dans des situations d’urgence et d’attentes fortes sur les résultats. Dans ces conditions, nos consultants usent de leur expertise pour cibler le travail et obtenir un effet de levier maximal (rapport gain/effort). Cependant, la complexité des logiciels a tendance à grandir à un rythme effréné qui rend de plus en plus important l’utilisation d’outils plus haut-niveau que l’éditeur de texte pour analyser et transformer  les codes sources. L’offre du marché n’est pas adaptée à la demande, principalement à cause de difficultés techniques propres au C++.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *