Vue d’ensemble des modules en C++

C++20 introduit des modules. Un module est un ensemble de fichiers de code source compilés indépendamment des fichiers sources (ou plus précisément, les unités de traduction) qui les importent.

Les modules éliminent ou réduisent la plupart des problèmes associés à l’utilisation de fichiers d’en-tête. Ils réduisent souvent les temps de compilation, parfois de manière significative. Les macros, les directives de préprocesseur et les noms non exportés déclarés dans un module ne sont pas visibles en dehors du module. Ils n’ont aucun effet sur la compilation de l’unité de traduction qui importe le module. Vous pouvez importer des modules dans n’importe quel ordre sans souci de redéfinition des macros. Les déclarations dans l’unité de traduction d’importation ne participent pas à la résolution de surcharge ou à la recherche de noms dans le module importé. Une fois qu’un module est compilé une fois, les résultats sont stockés dans un fichier binaire qui décrit tous les types, fonctions et modèles exportés. Le compilateur peut traiter ce fichier beaucoup plus rapidement qu’un fichier d’en-tête. Et le compilateur peut le réutiliser à chaque endroit où le module est importé dans un projet.

Vous pouvez utiliser des modules côte à côte avec des fichiers d’en-tête. Un fichier source C++ peut inclure des modules import et également des fichiers d’en-tête #include. Dans certains cas, vous pouvez importer un fichier d’en-tête en tant que module, ce qui est plus rapide que d’utiliser #include pour le traiter avec le préprocesseur. Nous vous recommandons d’utiliser des modules dans de nouveaux projets plutôt que des fichiers d’en-tête autant que possible. Pour les projets existants plus volumineux actuellement en cours de développement, essayez de convertir les en-têtes hérités en modules. Fondez votre adoption sur le fait que vous obteniez une réduction significative des temps de compilation.

Pour comparer les modules avec d’autres façons d’importer la bibliothèque standard, consultez Comparer les unités d’en-tête, les modules et les en-têtes précompilés.

À compter de Visual Studio 2022 version 17.5, l’importation de la bibliothèque standard en tant que module est standardisée et entièrement implémentée dans le compilateur Microsoft C++. Pour savoir comment importer la bibliothèque standard à l’aide de modules, consultez Importer la bibliothèque standard C++ à l’aide de modules.

Modules à partition unique

Un module à partition unique est un module qui se compose d’un fichier source unique. L’interface de module et l’implémentation se trouvent dans le même fichier.

L’exemple de module à partition unique suivant montre une définition de module simple dans un fichier source appelé Example.ixx. L’extension .ixx est l’extension par défaut pour les fichiers d’interface de module dans Visual Studio. Si vous souhaitez utiliser une autre extension, utilisez le commutateur /interface pour le compiler en tant qu’interface de module. Dans cet exemple, le fichier d’interface contient à la fois la définition de fonction et la déclaration. Vous pouvez également placer les définitions dans un ou plusieurs fichiers d’implémentation de module distincts, comme illustré dans un exemple ultérieur, mais il s’agit d’un exemple de module à partition unique.

L’instruction export module Example; indique que ce fichier est l’interface principale d’un module appelé Example. Le modificateur export avant int f() indique que cette fonction est visible lorsqu’un autre programme ou module importe Example:

// Example.ixx
export module Example;

#define ANSWER 42

namespace Example_NS
{
   int f_internal()
   {
     return ANSWER;
   }

   export int f()
   {
     return f_internal();
   }
}

Le fichier MyProgram.cpp utilise import pour accéder au nom exporté par Example. Le nom de l’espace de noms Example_NS est visible ici, mais pas tous ses membres, car ils ne sont pas exportés. En outre, la macro ANSWER n’est pas visible, car les macros ne sont pas exportées.

// MyProgram.cpp
import std;
import Example;

using namespace std;

int main()
{
   cout << "The result of f() is " << Example_NS::f() << endl; // 42
   // int i = Example_NS::f_internal(); // C2039
   // int j = ANSWER; //C2065
}

La déclaration import ne peut apparaître qu’à l’étendue globale. Un module et le code qui l’utilise doivent être compilés avec les mêmes options de compilateur.

Grammaire du module

module-name :
module-name-qualifier-seq optezidentifier

module-name-qualifier-seq :
identifier .
module-name-qualifier-seq identifier .

module-partition :
: module-name

module-declaration :
export optermodulemodule-namemodule-partitionopterattribute-specifier-seqopter;

module-import-declaration :
export opterimportmodule-nameattribute-specifier-seqopter;
export opterimportmodule-partitionattribute-specifier-seqopter;
export opterimportheader-nameattribute-specifier-seqopter;

Implémentation de modules

Une interface de module exporte le nom du module et tous les espaces de noms, types, fonctions, et ainsi de suite, qui composent l’interface publique du module.
Une implémentation de module définit les éléments exportés par le module.
Dans sa forme la plus simple, un module peut être un seul fichier qui combine l’interface de module et l’implémentation. Vous pouvez également placer l’implémentation dans un ou plusieurs fichiers d’implémentation de module distincts, comme .h et les fichiers .cpp le faire.

Pour les modules plus volumineux, vous pouvez fractionner des parties du module en sous-modules appelés partitions. Chaque partition se compose d’un fichier d’interface de module qui exporte le nom de la partition de module. Une partition peut également avoir un ou plusieurs fichiers d’implémentation de partition. Le module dans son ensemble a une interface de module principale , qui est l’interface publique du module. Il peut exporter les interfaces de partition, si vous le souhaitez.

Un module se compose d’une ou plusieurs unités de module . Une unité de module est une unité de traduction (fichier source) qui contient une déclaration de module. Il existe plusieurs types d’unités de module :

  • Une unité d’interface de module exporte un nom de module ou un nom de partition de module. Une unité d’interface de module a export module dans sa déclaration de module.
  • Une unité d’implémentation de module n’exporte pas de nom de module ou de partition de module. Comme le nom l’indique, il implémente un module.
  • Une unité d’interface de module principal exporte le nom du module. Il doit y avoir une seule unité d’interface de module principal dans un module.
  • Une unité d’interface de partition de module exporte un nom de partition de module.
  • Une unité d’implémentation de partition de module a un nom de partition de module dans sa déclaration de module, mais aucun mot-clé export.

Le mot clé export est utilisé uniquement dans les fichiers d’interface. Un fichier d’implémentation peut import un autre module, mais ne peut export aucun nom. Les fichiers d’implémentation peuvent avoir n’importe quelle extension.

Modules, espaces de noms et recherche dépendante des arguments

Les règles des espaces de noms dans les modules sont identiques à tout autre code. Si une déclaration dans un espace de noms est exportée, l’espace de noms englobant (à l’exception des membres qui ne sont pas explicitement exportés dans cet espace de noms) est également implicitement exporté. Si un espace de noms est explicitement exporté, toutes les déclarations de cette définition d’espace de noms sont exportées.

Lorsque le compilateur effectue une recherche dépendante des arguments pour la résolution de surcharge dans l’unité de traduction d’importation, il considère les fonctions déclarées dans la même unité de traduction (y compris les interfaces de module) comme où le type des arguments de la fonction est défini.

Partitions de module

Une partition de module est similaire à un module, sauf :

  • Il partage la propriété de toutes les déclarations dans l’ensemble du module.
  • Tous les noms exportés par les fichiers d’interface de partition sont importés et exportés par le fichier d’interface primaire.
  • Le nom d’une partition doit commencer par le nom du module suivi d’un signe deux-points (:).
  • Les déclarations dans l’une des partitions sont visibles dans l’ensemble du module.
  • Aucune précaution particulière n’est nécessaire pour éviter les erreurs de règle à définition unique (ODR). Vous pouvez déclarer un nom (fonction, classe, et ainsi de suite) dans une partition et le définir dans une autre.

Un fichier d’implémentation de partition commence comme suit et est une partition interne du point de vue des normes C++ :

module Example:part1;

Un fichier d’interface de partition commence comme suit :

export module Example:part1;

Pour accéder aux déclarations dans une autre partition, une partition doit l’importer. Mais il ne peut utiliser que le nom de la partition, et non le nom du module :

module Example:part2;
import :part1;

L’unité d’interface principale doit importer et réexporter tous les fichiers de partition d’interface du module, comme suit :

export import :part1;
export import :part2;

L’unité d’interface principale peut importer des fichiers d’implémentation de partition, mais ne peut pas les exporter. Ces fichiers ne sont pas autorisés à exporter des noms. Cette restriction permet à un module de conserver les détails d’implémentation internes au module.

Modules et fichiers d’en-tête

Vous pouvez inclure des fichiers d’en-tête dans un fichier source de module en plaçant une directive #include avant la déclaration du module. Ces fichiers sont considérés comme étant dans le fragment de module global . Un module ne peut voir que les noms dans le fragment de module global qui se trouvent dans les en-têtes qu’il inclut explicitement. Le fragment de module global contient uniquement des symboles utilisés.

// MyModuleA.cpp

#include "customlib.h"
#include "anotherlib.h"

import std;
import MyModuleB;

//... rest of file

Vous pouvez utiliser un fichier d’en-tête traditionnel pour contrôler les modules importés :

// MyProgram.h
#ifdef C_RUNTIME_GLOBALS
import std.compat;
#else
import std;
#endif

Fichiers d’en-tête importés

Certains en-têtes sont suffisamment autonomes qu’ils peuvent être introduits à l’aide du mot clé import. La principale différence entre un en-tête importé et un module importé est que toutes les définitions de préprocesseur dans l’en-tête sont visibles dans le programme d’importation immédiatement après l’instruction import.

import <vector>;
import "myheader.h";

Voir aussi

Importer la bibliothèque standard C++ à l’aide de modules
module, , importexport
Didacticiel sur les modules nommés
Comparer les unités d’en-tête, les modules et les en-têtes précompilés