Partager via


Vue d'ensemble des modules dans 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 avoir de problèmes par rapport à la 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é, 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. De plus, 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 import des modules et également #include des fichiers d’en-tête. 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 actifs existants plus volumineux en cours de développement, expérimentez la conversion d’en-têtes hérités en modules. L'adoption d'un tel système dépend de l'obtention d'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.

Activer les modules dans le compilateur Microsoft C++

À compter de Visual Studio 2022 version 17.1, les modules standard C++20 sont entièrement implémentés dans le compilateur Microsoft C++.

Avant d’être spécifié par la norme C++20, Microsoft avait une prise en charge expérimentale des modules. Le compilateur a également pris en charge l’importation de modules de bibliothèque standard prédéfinis, décrits ci-dessous.

À 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++. Cette section décrit l’ancienne méthode expérimentale, qui est toujours prise en charge. Pour plus d’informations sur la nouvelle méthode standardisée d’importation de la bibliothèque standard à l’aide de modules, consultez Importer la bibliothèque standard C++ à l’aide de modules.

Vous pouvez utiliser la fonctionnalité modules pour créer des modules à partition unique et importer les modules de bibliothèque standard fournis par Microsoft. Pour activer la prise en charge des modules de bibliothèque standard, compilez avec /experimental:module et /std:c++latest. Dans un projet Visual Studio, cliquez avec le bouton droit sur le nœud du projet dans l’Explorateur de solutions et choisissez Propriétés. Définissez la liste déroulante Configuration sur Toutes les configurations, puis choisissez Propriétés>de configuration C/C++>Language>Activer C++ Modules (expérimental).

Un module et le code qui l’utilise doivent être compilés avec les mêmes options de compilateur.

Utiliser la bibliothèque standard C++ en tant que modules (expérimental)

Cette section décrit l’implémentation expérimentale, qui est toujours prise en charge. La nouvelle méthode standardisée d’utilisation de la bibliothèque standard C++ en tant que modules est décrite dans Importer la bibliothèque standard C++ à l’aide de modules.

En important la bibliothèque standard C++ en tant que modules plutôt que de l’inclure via des fichiers d’en-tête, vous pouvez potentiellement accélérer les temps de compilation en fonction de la taille de votre projet. La bibliothèque expérimentale est divisée en modules nommés suivants :

  • std.regex fournit le contenu de l’en-tête <regex>
  • std.filesystem fournit le contenu de l’en-tête <filesystem>
  • std.memory fournit le contenu de l’en-tête <memory>
  • std.threading fournit le contenu des en-têtes <atomic>, <condition_variable>, <future>, <mutex>, <shared_mutex> et <thread>
  • std.core fournit tout le reste dans la bibliothèque C++ Standard

Pour consommer ces modules, ajoutez une déclaration d’importation en haut du fichier de code source. Par exemple :

import std.core;
import std.regex;

Pour utiliser les modules Microsoft Standard Library, compilez votre programme avec les options et les options /EHsc et /MD.

Exemple

L’exemple suivant montre une définition de module simple dans un fichier source appelé Example.ixx. L’extension .ixx est requise pour les fichiers d’interface de module dans Visual Studio. Dans cet exemple, le fichier d’interface contient à la fois la définition de fonction et la déclaration. Toutefois, 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.

L’instruction export module Example; indique que ce fichier est l’interface principale d’un module appelé Example. Le modificateur export sur 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 Example;
import std.core;

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.

Grammaire du module

module-name:
module-name-qualifier-seqoptidentifier

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

module-partition :
: module-name

module-declaration :
exportoptmodulemodule-namemodule-partitionoptattribute-specifier-seqopt;

module-import-declaration:
exportoptimportmodule-nameattribute-specifier-seqopt;
exportoptimportmodule-partitionattribute-specifier-seqopt;
exportoptimportheader-nameattribute-specifier-seqopt;

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, de la même façon que des fichiers .h et .cpp.

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 possède 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 un nom de module ou un nom de partition de module. Comme le nom l’indique, il implémente un module.
  • Une unité d’interface de module principale 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 dispose d’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 il ne peut pas export de noms. 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 les résolutions 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 sont définis.

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 faisant partie du 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.core;
import MyModuleB;

//... rest of file

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

// MyProgram.h
import std.core;
#ifdef DEBUG_LOGGING
import std.filesystem;
#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

module, import, export
Tutoriel sur les modules nommés
Comparer les unités d’en-tête, les modules et les en-têtes précompilés