Modules (since C++20)
La plupart des projets C++ utilisent plusieurs unités de traduction, et ont donc besoin de partager les déclarations et les définitions entre ces unités. L'utilisation des en-têtes est prédominante à cette fin, un exemple étant la bibliothèque standard dont les déclarations peuvent être fournies en incluant l'en-tête correspondant .
Les modules sont une fonctionnalité du langage permettant de partager des déclarations et des définitions entre les unités de traduction. Ils constituent une alternative à certains cas d'utilisation des en-têtes.
Les modules sont orthogonaux aux namespaces .
// helloworld.cpp export module helloworld; // déclaration de module import <iostream>; // déclaration d'importation export void hello() // déclaration d'exportation { std::cout << "Hello world!\n"; }
// main.cpp import helloworld; // déclaration d'importation int main() { hello(); }
Table des matières |
Syntaxe
export
(facultatif)
module
nom-module partition-module
(facultatif)
attr
(facultatif)
;
|
(1) | ||||||||
export
déclaration
|
(2) | ||||||||
export {
séquence-déclarations
(facultatif)
}
|
(3) | ||||||||
export
(facultatif)
import
nom-module attr
(facultatif)
;
|
(4) | ||||||||
export
(facultatif)
import
partition-module attr
(facultatif)
;
|
(5) | ||||||||
export
(facultatif)
import
nom-en-tête attr
(facultatif)
;
|
(6) | ||||||||
module;
|
(7) | ||||||||
module : private;
|
(8) | ||||||||
Déclarations de module
Une unité de traduction peut avoir une déclaration de module, auquel cas elle est considérée comme une unité de module . La déclaration de module , si elle est fournie, doit être la première déclaration de l'unité de traduction (à l'exception du fragment de module global , qui sera abordé ultérieurement). Chaque unité de module est associée à un nom de module (et optionnellement une partition), fourni dans la déclaration de module.
export
(facultatif)
module
nom-module partition-module
(facultatif)
attr
(facultatif)
;
|
|||||||||
Le nom du module est constitué d'un ou plusieurs identifiants séparés par des points (par exemple :
mymodule
,
mymodule.mysubmodule
,
mymodule2
...). Les points n'ont pas de signification intrinsèque, cependant ils sont utilisés informellement pour représenter une hiérarchie.
Si un identifiant dans le nom du module ou la partition de module est défini comme une macro de type objet , le programme est mal formé.
Un module nommé est l'ensemble des unités de module portant le même nom de module.
Les unités de module dont la déclaration comporte le mot-clé export sont appelées unités d'interface de module ; toutes les autres unités de module sont appelées unités d'implémentation de module .
Pour chaque module nommé, il doit y avoir exactement une unité d'interface de module qui ne spécifie aucune partition de module ; cette unité de module est appelée unité d'interface de module primaire . Son contenu exporté sera disponible lors de l'importation du module nommé correspondant.
// (chaque ligne représente une unité de traduction séparée) export module A; // déclare l'unité d'interface de module primaire pour le module nommé 'A' module A; // déclare une unité d'implémentation de module pour le module nommé 'A' module A; // déclare une autre unité d'implémentation de module pour le module nommé 'A' export module A.B; // déclare l'unité d'interface de module primaire pour le module nommé 'A.B' module A.B; // déclare une unité d'implémentation de module pour le module nommé 'A.B'
Exportation des déclarations et définitions
Les unités d'interface de module peuvent exporter des déclarations (y compris des définitions), qui peuvent être importées par d'autres unités de traduction. Pour exporter une déclaration, soit préfixez-la avec le mot-clé export , soit placez-la à l'intérieur d'un bloc export .
export
déclaration
|
|||||||||
export {
séquence-de-déclarations
(optionnel)
}
|
|||||||||
export module A; // déclare l'unité d'interface de module primaire pour le module nommé 'A' // hello() sera visible par les unités de traduction important 'A' export char const* hello() { return "hello"; } // world() ne sera PAS visible. char const* world() { return "world"; } // one() et zero() seront tous deux visibles. export { int one() { return 1; } int zero() { return 0; } } // L'exportation des espaces de noms fonctionne également : hi::english() et hi::french() seront visibles. export namespace hi { char const* english() { return "Hi!"; } char const* french() { return "Salut!"; } }
Importation de modules et d'unités d'en-tête
Les modules sont importés via une déclaration d'importation :
export
(facultatif)
import
nom-du-module attr
(facultatif)
;
|
|||||||||
Toutes les déclarations et définitions exportées dans les unités d'interface de module du module nommé donné seront disponibles dans l'unité de traduction utilisant la déclaration d'importation.
Les déclarations d'importation peuvent être exportées dans une unité d'interface de module. C'est-à-dire que si le module
B
exporte-importe
A
, alors l'importation de
B
rendra également visibles toutes les exportations de
A
.
Dans les unités de module, toutes les déclarations d'importation (y compris les export-imports) doivent être regroupées après la déclaration du module et avant toutes les autres déclarations.
/////// A.cpp (unité d'interface de module primaire de 'A') export module A; export char const* hello() { return "hello"; } /////// B.cpp (unité d'interface de module primaire de 'B') export module B; export import A; export char const* world() { return "world"; } /////// main.cpp (pas une unité de module) #include <iostream> import B; int main() { std::cout << hello() << ' ' << world() << '\n'; }
#include ne doit pas être utilisé dans une unité de module (en dehors de la fragment de module global ), car toutes les déclarations et définitions incluses seraient considérées comme faisant partie du module. À la place, les en-têtes peuvent également être importés comme unités d'en-tête avec une déclaration d'importation :
export
(optionnel)
import
header-name attr
(optionnel)
;
|
|||||||||
Un module d'en-tête est une unité de traduction distincte synthétisée à partir d'un en-tête. L'importation d'un module d'en-tête rendra accessibles toutes ses définitions et déclarations. Les macros du préprocesseur sont également accessibles (car les déclarations d'importation sont reconnues par le préprocesseur).
Cependant, contrairement à #include , les macros de prétraitement déjà définies au point de la déclaration d'importation n'affecteront pas le traitement de l'en-tête. Cela peut être gênant dans certains cas (certains en-têtes utilisent des macros de prétraitement comme forme de configuration), auquel cas l'utilisation du fragment de module global est nécessaire.
/////// A.cpp (unité d'interface de module primaire de 'A') export module A; import <iostream>; export import <string_view>; export void print(std::string_view message) { std::cout << message << std::endl; } /////// main.cpp (pas une unité de module) import A; int main() { std::string_view message = "Hello, world!"; print(message); }
Fragment de module global
Les modules peuvent être précédés d'un fragment de module global , qui peut être utilisé pour inclure des en-têtes lorsque l'importation des en-têtes n'est pas possible (notamment lorsque l'en-tête utilise des macros de préprocesseur comme configuration).
module;
directives-de-prétraitement (optionnel) déclaration-de-module |
|||||||||
Si une unité de module possède un fragment de module global, alors sa première déclaration doit être
module;
. Ensuite, seules les
directives de préprocesseur
peuvent apparaître dans le fragment de module global. Enfin, une déclaration de module standard marque la fin du fragment de module global et le début du contenu du module.
/////// A.cpp (unité d'interface de module primaire de 'A') module; // La définition de _POSIX_C_SOURCE ajoute des fonctions aux en-têtes standard, // conformément à la norme POSIX. #define _POSIX_C_SOURCE 200809L #include <stdlib.h> export module A; import <ctime>; // Uniquement pour la démonstration (mauvaise source d'aléatoire). // Utilisez <random> de C++ à la place. export double weak_random() { std::timespec ts; std::timespec_get(&ts, TIME_UTC); // de <ctime> // Fourni dans <stdlib.h> selon la norme POSIX. srand48(ts.tv_nsec); // drand48() retourne un nombre aléatoire entre 0 et 1. return drand48(); } /////// main.cpp (pas une unité de module) import <iostream>; import A; int main() { std::cout << "Valeur aléatoire entre 0 et 1 : " << weak_random() << '\n'; }
Fragment de module privé
L'unité d'interface du module primaire peut être suffixée par un fragment de module privé , ce qui permet à un module d'être représenté comme une seule unité de traduction sans rendre tous les contenus du module accessibles aux importateurs.
module : private;
séquence-de-déclarations (optionnel) |
|||||||||
Fragment de module privé termine la partie de l'unité d'interface du module qui peut affecter le comportement d'autres unités de traduction. Si une unité de module contient un fragment de module privé , elle sera la seule unité de module de son module.
export module foo; export int f(); module : private; // termine la partie de l'unité d'interface du module qui // peut affecter le comportement d'autres unités de traduction // démarre un fragment de module privé int f() // définition non accessible depuis les importateurs de foo { return 42; }
Partitions de module
Un module peut avoir
des unités de partition de module
. Ce sont des unités de module dont les déclarations de module incluent une partition de module, qui commence par un deux-points
:
et est placée après le nom du module.
export module A:B; // Déclare une unité d'interface de module pour le module 'A', partition ':B'.
Une partition de module représente exactement une unité de module (deux unités de module ne peuvent pas désigner la même partition de module). Elles sont visibles uniquement depuis l'intérieur du module nommé (les unités de traduction en dehors du module nommé ne peuvent pas importer directement une partition de module).
Une partition de module peut être importée par des unités de module du même module nommé.
export
(optionnel)
import
module-partition attr
(optionnel)
;
|
|||||||||
/////// A-B.cpp export module A:B; ... /////// A-C.cpp module A:C; ... /////// A.cpp export module A; import :C; export import :B; ...
` et contient des termes spécifiques au C++. Seuls les commentaires dans le code source auraient pu être traduits, mais ils sont purement informatifs (noms de fichiers) et n'ont donc pas été modifiés pour préserver la cohérence du code.
Toutes les définitions et déclarations dans une partition de module sont visibles par l'unité de module importatrice, qu'elles soient exportées ou non.
Les partitions de module peuvent être des unités d'interface de module (lorsque leurs déclarations de module contiennent
export
). Elles doivent être export-importées par l'unité d'interface de module principale, et leurs déclarations exportées seront visibles lorsque le module est importé.
export
(optionnel)
import
module-partition attr
(optionnel)
;
|
|||||||||
/////// A.cpp export module A; // unité d'interface de module primaire export import :B; // Hello() est visible lors de l'importation de 'A'. import :C; // WorldImpl() est maintenant visible uniquement pour 'A.cpp'. // export import :C; // ERREUR : Impossible d'exporter une unité d'implémentation de module. // World() est visible par toute unité de traduction important 'A'. export char const* World() { return WorldImpl(); }
/////// A-B.cpp export module A:B; // unité d'interface de module partition // Hello() est visible par toute unité de traduction important 'A'. export char const* Hello() { return "Hello"; }
/////// A-C.cpp module A:C; // unité d'implémentation de module partition // WorldImpl() est visible par toute unité de module de 'A' important ':C'. char const* WorldImpl() { return "World"; }
/////// main.cpp import A; import <iostream>; int main() { std::cout << Hello() << ' ' << World() << '\n'; // WorldImpl(); // ERREUR : WorldImpl() n'est pas visible. }
Propriété du module
En général, si une déclaration apparaît après la déclaration de module dans une unité de module, elle est attachée à ce module.
Si une déclaration d'une entité est attachée à un module nommé, cette entité ne peut être définie que dans ce module. Toutes les déclarations d'une telle entité doivent être attachées au même module.
Si une déclaration est attachée à un module nommé, et qu'elle n'est pas exportée, le nom déclaré possède module linkage .
export module lib_A; int f() { return 0; } // f a une liaison de module export int x = f(); // x est égal à 0
export module lib_B; int f() { return 1; } // OK, f dans lib_A et f dans lib_B font référence à des entités différentes export int y = f(); // y est égal à 1
Si deux déclarations d'une entité sont attachées à différents modules, le programme est mal formé ; aucun diagnostic n'est requis si aucune n'est accessible depuis l'autre.
/////// decls.h int f(); // #1, attaché au module global int g(); // #2, attaché au module global
/////// Interface du module M module; #include "decls.h" export module M; export using ::f; // OK, ne déclare pas une entité, exporte #1 int g(); // Erreur : correspond à #2, mais attaché à M export int h(); // #3 export int k(); // #4
/////// Autre unité de traduction import M; static int h(); // Erreur : correspond à #3 int k(); // Erreur : correspond à #4
Les déclarations suivantes ne sont attachées à aucun module nommé (et ainsi l'entité déclarée peut être définie en dehors du module) :
- namespace définitions avec liaison externe ;
- déclarations dans une spécification de liaison de langage .
export module lib_A; namespace ns // ns n'est pas attaché à lib_A. { export extern "C++" int f(); // f n'est pas attaché à lib_A. extern "C++" int g(); // g n'est pas attaché à lib_A. export int h(); // h est attaché à lib_A. } // ns::h doit être défini dans lib_A, mais ns::f et ns::g peuvent être définis ailleurs (par exemple // dans un fichier source traditionnel).
Notes
| Macro de test de fonctionnalité | Valeur | Norme | Fonctionnalité |
|---|---|---|---|
__cpp_modules
|
201907L
|
(C++20) | Modules — prise en charge du langage de base |
__cpp_lib_modules
|
202207L
|
(C++23) | Modules de la bibliothèque standard std et std. compat |
Mots-clés
private , module , import , export
Rapports de défauts
Les rapports de défauts modifiant le comportement suivants ont été appliqués rétroactivement aux normes C++ précédemment publiées.
| DR | Applicable à | Comportement publié | Comportement corrigé |
|---|---|---|---|
| CWG 2732 | C++20 |
il n'était pas clair si les en-têtes importables peuvent
réagir à l'état du préprocesseur au point d'importation |
aucune réaction |
| P3034R1 | C++20 |
les noms de modules et les partitions de modules pouvaient
contenir des identifiants définis comme macros objet |
interdit |