Réponse à la question.
Historique de la question
Avant C++20, le modèle de compilation de C++ reposait sur l'inclusion textuelle via des directives du préprocesseur. Lorsqu'un fichier d'en-tête était inclus, le préprocesseur copiait littéralement le texte de cet en-tête dans le fichier incluant. Ce mécanisme faisait que les macros définies dans les en-têtes fuyaient dans l'espace de noms global de chaque unité de traduction qui les incluait, conduisant à des bogues subtils et des collisions de noms difficiles à diagnostiquer.
Le problème
La fuite de macros créait des cauchemars de maintenance dans de grandes bases de code. Une macro définie dans une bibliothèque tierce pouvait silencieusement redéfinir des mots-clés ou des identifiants courants dans le code consommateur, provoquant des échecs de compilation ou des erreurs d'exécution qui semblaient sans rapport avec la cause réelle. Les solutions traditionnelles comme les gardes #undef étaient manuelles, sujettes aux erreurs et ne s'échelonnaient pas à travers des graphes de dépendance complexes. Le problème fondamental était que le préprocesseur n'avait aucun concept de portée ou de limites d'interface.
La solution
Les modules C++20 introduisent un mécanisme d'importation sémantique qui opère au niveau du langage plutôt qu'au niveau du préprocesseur. Lors de l'importation d'un module avec import module_name;, le compilateur traite l'interface exportée du module sans exécuter les directives du préprocesseur de l'unité de traduction importante. Les macros définies dans le module restent privées à l'implémentation de ce module, sauf si elles sont explicitement exportées. Cette propriété garantit que les macros ne fuient pas à travers les frontières des unités de traduction, offrant une véritable encapsulation et empêchant la pollution des noms.
// mathlib.cpp (Implémentation du module) module; #define INTERNAL_CALC_FACTOR 3.14 // Macro privée, non fuyante export module mathlib; export double compute(double x) { return x * INTERNAL_CALC_FACTOR; } // main.cpp (Consommateur) import mathlib; // INTERNAL_CALC_FACTOR n'est PAS visible ici // #ifdef INTERNAL_CALC_FACTOR serait faux int main() { double result = compute(10.0); // Fonctionne bien }
Situation de la vie réelle
Une société de trading financier maintenait une grande base de code avec des millions de lignes de code à travers des centaines de modules. Ils s'appuyaient sur une bibliothèque mathématique héritée qui définissait des macros comme MIN et MAX dans ses en-têtes publics. Ces macros entraient fréquemment en collision avec des fonctions de la bibliothèque standard et des bibliothèques de parsing JSON tierces qui utilisaient min et max comme noms de variables ou de modèles de fonction.
La première approche envisagée était de protéger tous les en-têtes tiers avec des gardes de style #pragma once et de #undef manuellement les macros problématiques après chaque inclusion. Cela nécessitait que les développeurs se souviennent des en-têtes définissant quelles macros et de nettoyer après chaque inclusion. L'approche était fragile car manquer un seul #undef pouvait causer des échecs dans des parties non liées de la base de code. Elle augmentait également considérablement les temps de compilation en raison du traitement répété du même texte d'en-tête par le préprocesseur à travers les unités de traduction.
La deuxième approche envisagée était de convertir la bibliothèque mathématique pour utiliser des fonctions en ligne et des modèles au lieu de macros. Bien que cela résolvait le problème de fuite, cela nécessitait des modifications étendues de la bibliothèque héritée. La bibliothèque mathématique était utilisée par plusieurs équipes et y apporter des changements risquait de rompre les calculs existants qui s'appuyaient sur des sémantiques ou des effets secondaires spécifiques à l'évaluation des macros. L'effort de refactoring était estimé à six mois et était jugé trop risqué pour la plateforme de trading.
La solution choisie était de migrer vers les modules C++20. L'équipe a converti la bibliothèque mathématique en un module qui exportait des fonctions mathématiques tout en gardant les macros internes à l'implémentation du module. En utilisant import mathlib; au lieu de #include <mathlib.h>, les unités de traduction consommatrices ne voyaient plus les macros MIN et MAX. Cette approche nécessitait des changements minimes à l'implémentation de la bibliothèque - seulement ajouter des déclarations d'exportation et convertir les en-têtes en unités d'interface de module. La migration a pris deux semaines au lieu de six mois. Le résultat a été l'élimination des collisions de noms liées aux macros dans l'ensemble de la base de code et une réduction de 15 % des temps de compilation grâce à l'interface compilée du module.
Ce que les candidats oublient souvent
Comment le format binaire compilé de l'unité d'interface du module prévient-il la fuite de macros par rapport à l'inclusion d'en-têtes textuels ?
Les candidats oublient souvent que les modules C++20 produisent des unités d'interface de module compilées (CMI) qui sont des représentations binaires de l'interface exportée du module. Contrairement aux en-têtes textuels qui sont traités par le préprocesseur et contiennent des définitions de macros sous forme de texte, les CMI stockent des informations sémantiques sur les fonctions exportées, les types et les modèles.
Le préprocesseur ne traite pas le contenu d'un module importé ; il ne voit que la déclaration d'importation. Par conséquent, les macros définies dans l'implémentation du module ou même dans son unité d'interface ne sont pas visibles pour l'importateur. Cela est fondamentalement différent de #include, qui copie littéralement du texte y compris les directives #define.
Comprendre cela nécessite de reconnaître que les modules passent d'un modèle d'inclusion textuelle à un modèle d'importation sémantique. Le format binaire garantit que seules les entités explicitement exportées sont visibles, et les macros ne font pas partie de l'interface exportée sauf si elles sont spécifiquement exportées à l'aide de directives de macros.
Pourquoi les macros exportées à partir d'un module utilisant export import se comportent-elles différemment des macros des directives #include ?
Les candidats confondent fréquemment export import de macros avec le comportement normal des macros. Bien que C++20 permette d'exporter des macros à l'aide de export import, ces macros n'affectent que le code qui importe le module et ne fuient pas au-delà de cette portée d'importation.
Contrairement à #include où les macros persistent dans l'unité de traduction jusqu'à ce qu'elles soient explicitement annulées ou à la fin du fichier, les macros exportées des modules sont limitées à l'exposition de l'unité de traduction importante à ce module. Le préprocesseur traite les macros importées comme si elles étaient définies au moment de l'importation, mais elles n'affectent pas les importations subséquentes ni l'état global du préprocesseur de la même manière que l'inclusion textuelle.
De plus, si plusieurs modules exportent des macros conflictuelles, le conflit est détecté au moment de l'importation plutôt que de causer des erreurs de redéfinition silencieuses plus tard dans la compilation. Ce comportement de portée fournit l'hygiène qui manque à l'inclusion textuelle, garantissant que les macros se comportent plus comme de véritables entités avec une portée de nom appropriée.
Comment l'indépendance du module vis-à-vis du préprocesseur affecte-t-elle l'intégration du système de build et l'analyse des dépendances ?
Les candidats oublient souvent que les modules C++20 nécessitent que les systèmes de build comprennent les dépendances des modules avant le début de la compilation, contrairement aux en-têtes où les dépendances sont découvertes pendant la compilation. Étant donné que les modules sont des unités compilées plutôt que des fichiers texte, le système de build doit analyser les unités d'interface des modules pour déterminer ce qu'elles exportent et ce qu'elles importent.
Cela nécessite un processus de build en deux phases : d'abord, l'analyse des unités d'interface des modules pour construire un graphe de dépendances, puis la compilation dans l'ordre des dépendances. L'indépendance vis-à-vis du préprocesseur signifie que les gardes traditionnels #ifdef pour l'inclusion des en-têtes sont sans pertinence, et la configuration des interfaces de module basée sur des macros est limitée. Les systèmes de build doivent suivre les artefacts de module compilés (BMI - Interface de Module Binaire) plutôt que de se limiter à des fichiers sources.
Cela change fondamentalement la manière dont le suivi des dépendances et les builds incrémentiels fonctionnent. Le système de build doit maintenant gérer les fichiers BMI comme des artefacts intermédiaires avec leurs propres chaînes de dépendance, nécessitant des mises à jour des outils de build comme CMake ou Bazel pour prendre en charge des graphes de compilation conscients des modules.