Le C++ a stagné pendant de nombreuses années, et beaucoup de développeurs étaient convaincus que le langage connaîtrait le même sort que COBOL, Fortran et VB6 : plus aucun nouveau projet ne serait développé avec, et les développeurs C++ se contenteraient de maintenir les projets existants. Mais contre toute attente, le C++ est renaît de ses cendres, et les nouvelles normes ont profondément changé la façon d'utiliser le langage.
Cependant, l'héritage du mécanisme #include est toujours là. Après la modernisation du langage, il est devenu le maillon faible suivant à traiter. Il présente en effet de nombreux inconvénients ; en voici quelques-uns, tirés de cet intéressant document.
- La montée en charge au moment de la compilation: chaque fois qu'un en-tête est inclus, le compilateur doit prétraiter et analyser le texte de cet en-tête et de tous les en-têtes qu'il inclut, de manière transitive. Ce processus doit être répété pour chaque unité de traduction de l'application, ce qui représente une énorme quantité de travail redondant. Dans un projet comportant N unités de traduction et M en-têtes inclus dans chaque unité de traduction, le compilateur effectue M x N travail, alors que la plupart des M en-têtes sont partagés entre plusieurs unités de traduction. Le C++ est particulièrement touché, car le modèle de compilation des templates oblige à placer une énorme quantité de code dans les en-têtes.
- La fragilité:
#includesont traitées comme des inclusions textuelles par le préprocesseur, et sont donc soumises à toutes les définitions de macros actives au moment de l'inclusion. Si une définition de macro active entre en collision avec un nom de la bibliothèque, elle peut casser l'API de la bibliothèque ou provoquer des échecs de compilation dans l'en-tête de la bibliothèque elle-même. Pour un exemple extrême,#define std "The C++ Standard"puis incluez un en-tête de la bibliothèque standard : le résultat est une horrible cascade d'échecs dans l'implémentation de la bibliothèque standard C++. Des problèmes réels plus subtils surviennent lorsque les en-têtes de deux bibliothèques différentes interagissent à cause de collisions de macros, et les utilisateurs sont contraints de réordonner les directives#includeou d'introduire des directives#undefpour briser la dépendance (involontaire). - Les palliatifs conventionnels: les programmeurs C ont adopté un certain nombre de conventions pour contourner la fragilité du modèle de préprocesseur du C. Les gardes d'inclusion, par exemple, sont nécessaires pour la grande majorité des en-têtes afin de garantir qu'une inclusion multiple ne casse pas la compilation. Les noms de macros sont écrits avec
LONG_PREFIXED_UPPERCASE_IDENTIFIERSpour éviter les collisions, et certains développeurs de bibliothèques/frameworks utilisent même des noms__underscoreddans les en-têtes pour éviter les collisions avec les noms « normaux » qui (par convention) ne devraient même pas être des macros. Ces conventions constituent une barrière à l'entrée pour les développeurs venant de langages autres que le C, représentent du code répétitif pour les développeurs plus expérimentés, et rendent nos en-têtes bien plus laids qu'ils ne devraient l'être. - La confusion des outils: dans un langage fondé sur le C, il est difficile de construire des outils qui fonctionnent bien avec les bibliothèques logicielles, car les frontières des bibliothèques ne sont pas claires. Quels en-têtes appartiennent à une bibliothèque donnée, et dans quel ordre ces en-têtes doivent-ils être inclus pour garantir qu'ils compilent correctement ? Ces en-têtes sont-ils du C, du C++, de l'Objective-C++ ou l'une des variantes de ces langages ? Quelles déclarations de ces en-têtes font réellement partie de l'API, et lesquelles ne sont présentes que parce qu'elles devaient être écrites dans le fichier d'en-tête ?
Une solution à ces problèmes avait déjà été introduite dans les premières ébauches de C++0x: la fonctionnalité des modules. Cependant, elle a été reportée à chaque nouvelle spécification de la norme.
Les modules : un modèle sémantique plus robuste
Les modules améliorent l'accès aux bibliothèques grâce à un modèle sémantique plus robuste et plus efficace. Du point de vue de l'utilisateur, le code ne diffère que légèrement, car on utilise une déclaration import plutôt qu'une directive de préprocesseur #include. Voici un exemple tiré de l'ébauche de C++0x :
import std; // Module import directive.
int main() {
std::cout << "Hello World\n";
}
Plus besoin d'inclure plusieurs fichiers de la STL — un seul import suffit, ce qui rend le code plus propre. L'import de module charge une représentation binaire du module std et rend son API directement disponible pour l'application. Les définitions du préprocesseur qui précèdent la déclaration d'import n'ont aucun impact sur l'API fournie par std, car le module lui-même a été compilé comme un module autonome séparé.
Voici un article intéressant montrant comment développer un module C++ avec VS2017 : Les modules C++ dans Visual Studio 2017.
Conclusion
Les modules C++ élimineront les problèmes associés au mécanisme d'inclusion et rendront l'utilisation des autres bibliothèques beaucoup plus simple, comme en C# ou en Java.
Essayez de créer votre premier module et d'explorer cette puissante fonctionnalité.
