Modules (since C++20)
La mayoría de los proyectos en C++ utilizan múltiples unidades de traducción, por lo que necesitan compartir declaraciones y definiciones entre esas unidades. El uso de headers es prominente para este propósito, un ejemplo siendo la biblioteca estándar cuyas declaraciones pueden ser proporcionadas incluyendo el header correspondiente .
Los módulos son una característica del lenguaje para compartir declaraciones y definiciones entre unidades de traducción. Son una alternativa para algunos casos de uso de los encabezados.
Los módulos son ortogonales a namespaces .
// helloworld.cpp export module helloworld; // declaración del módulo import <iostream>; // declaración de importación export void hello() // declaración de exportación { std::cout << "Hello world!\n"; }
// main.cpp import helloworld; // declaración de importación int main() { hello(); }
Contenidos |
Sintaxis
export
(opcional)
module
module-name module-partition
(opcional)
attr
(opcional)
;
|
(1) | ||||||||
export
declaration
|
(2) | ||||||||
export {
declaration-seq
(opcional)
}
|
(3) | ||||||||
export
(opcional)
import
module-name attr
(opcional)
;
|
(4) | ||||||||
export
(opcional)
import
module-partition attr
(opcional)
;
|
(5) | ||||||||
export
(opcional)
import
header-name attr
(opcional)
;
|
(6) | ||||||||
module;
|
(7) | ||||||||
module : private;
|
(8) | ||||||||
Declaraciones de módulos
Una unidad de traducción puede tener una declaración de módulo, en cuyo caso se considera una unidad de módulo . La declaración de módulo , si se proporciona, debe ser la primera declaración de la unidad de traducción (exceptuando el fragmento de módulo global , que se cubrirá más adelante). Cada unidad de módulo está asociada a un nombre de módulo (y opcionalmente una partición), proporcionado en la declaración de módulo.
export
(opcional)
module
nombre-módulo partición-módulo
(opcional)
attr
(opcional)
;
|
|||||||||
El nombre del módulo consiste en uno o más identificadores separados por puntos (por ejemplo:
mymodule
,
mymodule.mysubmodule
,
mymodule2
...). Los puntos no tienen un significado intrínseco, sin embargo se utilizan informalmente para representar jerarquía.
Si cualquier identificador en el nombre del módulo o partición del módulo está definido como una macro de tipo objeto , el programa está mal formado.
Un módulo nombrado es la colección de unidades de módulo con el mismo nombre de módulo.
Las unidades de módulo cuya declaración tiene la palabra clave export se denominan unidades de interfaz de módulo ; todas las demás unidades de módulo se denominan unidades de implementación de módulo .
Para cada módulo nombrado, debe existir exactamente una unidad de interfaz de módulo que no especifique ninguna partición de módulo; esta unidad de módulo se denomina unidad de interfaz de módulo primaria . Su contenido exportado estará disponible al importar el módulo nombrado correspondiente.
// (cada línea representa una unidad de traducción separada) export module A; // declara la unidad de interfaz del módulo primario para el módulo nombrado 'A' module A; // declara una unidad de implementación del módulo para el módulo nombrado 'A' module A; // declara otra unidad de implementación del módulo para el módulo nombrado 'A' export module A.B; // declara la unidad de interfaz del módulo primario para el módulo nombrado 'A.B' module A.B; // declara una unidad de implementación del módulo para el módulo nombrado 'A.B'
Exportación de declaraciones y definiciones
Las unidades de interfaz de módulo pueden exportar declaraciones (incluyendo definiciones), que pueden ser importadas por otras unidades de traducción. Para exportar una declaración, ya sea prefíjela con la palabra clave export , o colóquela dentro de un bloque export .
export
declaración
|
|||||||||
export {
secuencia-de-declaraciones
(opcional)
}
|
|||||||||
export module A; // declara la unidad de interfaz del módulo primario para el módulo nombrado 'A' // hello() será visible para las unidades de traducción que importen 'A' export char const* hello() { return "hello"; } // world() NO será visible. char const* world() { return "world"; } // Tanto one() como zero() serán visibles. export { int one() { return 1; } int zero() { return 0; } } // Exportar espacios de nombres también funciona: hi::english() y hi::french() serán visibles. export namespace hi { char const* english() { return "Hi!"; } char const* french() { return "Salut!"; } }
Importación de módulos y unidades de cabecera
Los módulos se importan mediante una declaración de importación :
export
(opcional)
import
module-name attr
(opcional)
;
|
|||||||||
Todas las declaraciones y definiciones exportadas en las unidades de interfaz del módulo del módulo nombrado dado estarán disponibles en la unidad de traducción que utilice la declaración de importación.
Las declaraciones de importación pueden ser exportadas en una unidad de interfaz de módulo. Es decir, si el módulo
B
exporta-importa
A
, entonces importar
B
también hará visibles todas las exportaciones de
A
.
En unidades de módulo, todas las declaraciones de importación (incluyendo export-imports) deben agruparse después de la declaración del módulo y antes de todas las demás declaraciones.
/////// A.cpp (unidad de interfaz de módulo primario de 'A') export module A; export char const* hello() { return "hello"; } /////// B.cpp (unidad de interfaz de módulo primario de 'B') export module B; export import A; export char const* world() { return "world"; } /////// main.cpp (no es una unidad de módulo) #include <iostream> import B; int main() { std::cout << hello() << ' ' << world() << '\n'; }
#include no debe usarse en una unidad de módulo (fuera del global module fragment ), porque todas las declaraciones y definiciones incluidas serían consideradas parte del módulo. En su lugar, los encabezados también pueden importarse como header units con una import declaration :
export
(opcional)
import
header-name attr
(opcional)
;
|
|||||||||
Una unidad de cabecera es una unidad de traducción separada sintetizada a partir de un encabezado. Importar una unidad de cabecera hará accesibles todas sus definiciones y declaraciones. Las macros del preprocesador también son accesibles (porque las declaraciones de importación son reconocidas por el preprocesador).
Sin embargo, a diferencia de #include , las macros de preprocesamiento ya definidas en el punto de la declaración de importación no afectarán el procesamiento del encabezado. Esto puede ser inconveniente en algunos casos (algunos encabezados utilizan macros de preprocesamiento como forma de configuración), en cuyo caso se necesita el uso del fragmento de módulo global .
/////// A.cpp (unidad de interfaz de módulo primario 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 (no es una unidad de módulo) import A; int main() { std::string_view message = "Hello, world!"; print(message); }
Fragmento de módulo global
Las unidades de módulo pueden tener como prefijo un fragmento de módulo global , que puede utilizarse para incluir cabeceras cuando importar las cabeceras no es posible (especialmente cuando la cabecera utiliza macros de preprocesamiento como configuración).
module;
preprocessing-directives (opcional) module-declaration |
|||||||||
Si un módulo-unidad tiene un fragmento de módulo global, entonces su primera declaración debe ser
module;
. Luego, solo
directivas de preprocesamiento
pueden aparecer en el fragmento de módulo global. Después, una declaración de módulo estándar marca el final del fragmento de módulo global y el inicio del contenido del módulo.
/////// A.cpp (unidad de interfaz de módulo primario de 'A') module; // Definir _POSIX_C_SOURCE añade funciones a los encabezados estándar, // según el estándar POSIX. #define _POSIX_C_SOURCE 200809L #include <stdlib.h> export module A; import <ctime>; // Solo para demostración (fuente débil de aleatoriedad). // Usar C++ <random> en su lugar. export double weak_random() { std::timespec ts; std::timespec_get(&ts, TIME_UTC); // desde <ctime> // Proporcionado en <stdlib.h> según el estándar POSIX. srand48(ts.tv_nsec); // drand48() retorna un número aleatorio entre 0 y 1. return drand48(); } /////// main.cpp (no es una unidad de módulo) import <iostream>; import A; int main() { std::cout << "Valor aleatorio entre 0 y 1: " << weak_random() << '\n'; }
Fragmento de módulo privado
La unidad de interfaz del módulo principal puede tener como sufijo un fragmento privado del módulo , lo que permite que un módulo se represente como una sola unidad de traducción sin hacer que todos los contenidos del módulo sean accesibles para los importadores.
module : private;
declaration-seq (opcional) |
|||||||||
Fragmento privado del módulo finaliza la porción de la unidad de interfaz del módulo que puede afectar el comportamiento de otras unidades de traducción. Si una unidad de módulo contiene un fragmento privado del módulo , será la única unidad de módulo de su módulo.
export module foo; export int f(); module : private; // finaliza la porción de la unidad de interfaz del módulo que // puede afectar el comportamiento de otras unidades de traducción // inicia un fragmento privado del módulo int f() // definición no accesible desde los importadores de foo { return 42; }
Particiones de módulos
Un módulo puede tener
unidades de partición de módulo
. Son unidades de módulo cuyas declaraciones de módulo incluyen una partición de módulo, que comienza con dos puntos
:
y se coloca después del nombre del módulo.
export module A:B; // Declara una unidad de interfaz de módulo para el módulo 'A', partición ':B'.
Una partición de módulo representa exactamente una unidad de módulo (dos unidades de módulo no pueden designar la misma partición de módulo). Son visibles únicamente desde dentro del módulo nombrado (las unidades de traducción fuera del módulo nombrado no pueden importar una partición de módulo directamente).
Una partición de módulo puede ser importada por unidades de módulo del mismo módulo nombrado.
export
(opcional)
import
module-partition attr
(opcional)
;
|
|||||||||
/////// A-B.cpp export module A:B; ... /////// A-C.cpp module A:C; ... /////// A.cpp export module A; import :C; export import :B; ...
Todas las definiciones y declaraciones en una partición de módulo son visibles para la unidad de módulo importadora, ya sean exportadas o no.
Las particiones de módulo pueden ser unidades de interfaz de módulo (cuando sus declaraciones de módulo tienen
export
). Deben ser export-importadas por la unidad de interfaz de módulo principal, y sus declaraciones exportadas serán visibles cuando el módulo sea importado.
export
(opcional)
import
module-partition attr
(opcional)
;
|
|||||||||
/////// A.cpp export module A; // unidad de interfaz primaria del módulo export import :B; // Hello() es visible al importar 'A'. import :C; // WorldImpl() ahora es visible solo para 'A.cpp'. // export import :C; // ERROR: No se puede exportar una unidad de implementación de módulo. // World() es visible para cualquier unidad de traducción que importe 'A'. export char const* World() { return WorldImpl(); }
/////// A-B.cpp export module A:B; // unidad de interfaz de módulo particionado // Hello() es visible para cualquier unidad de traducción que importe 'A'. export char const* Hello() { return "Hello"; }
/////// A-C.cpp module A:C; // unidad de implementación del módulo particionado // WorldImpl() es visible por cualquier unidad de módulo de 'A' que importe ':C'. char const* WorldImpl() { return "World"; }
/////// main.cpp import A; import <iostream>; int main() { std::cout << Hello() << ' ' << World() << '\n'; // ERROR: WorldImpl() no es visible. }
Propiedad del módulo
En general, si una declaración aparece después de la declaración del módulo en una unidad de módulo, se adjunta a ese módulo.
Si una declaración de una entidad está adjunta a un módulo con nombre, esa entidad solo puede definirse en ese módulo. Todas las declaraciones de dicha entidad deben estar adjuntas al mismo módulo.
Si una declaración está adjunta a un módulo con nombre, y no es exportada, el nombre declarado tiene module linkage .
export module lib_A; int f() { return 0; } // f tiene enlace de módulo export int x = f(); // x es igual a 0
export module lib_B; int f() { return 1; } // OK, f en lib_A y f en lib_B se refieren a entidades diferentes export int y = f(); // y es igual a 1
Si dos declaraciones de una entidad están adjuntas a módulos diferentes, el programa está mal formado; no se requiere diagnóstico si ninguna es accesible desde la otra.
/////// decls.h int f(); // #1, adjunta al módulo global int g(); // #2, adjunta al módulo global
/////// Interfaz del módulo M module; #include "decls.h" export module M; export using ::f; // OK, no declara una entidad, exporta #1 int g(); // Error: coincide con #2, pero está adjunto a M export int h(); // #3 export int k(); // #4
/////// Otra unidad de traducción import M; static int h(); // Error: coincide con #3 int k(); // Error: coincide con #4
Las siguientes declaraciones no están adjuntas a ningún módulo con nombre (y por lo tanto la entidad declarada puede definirse fuera del módulo):
- namespace definiciones con enlace externo;
- declaraciones dentro de una language linkage especificación.
export module lib_A; namespace ns // ns no está adjunto a lib_A. { export extern "C++" int f(); // f no está adjunto a lib_A. extern "C++" int g(); // g no está adjunto a lib_A. export int h(); // h está adjunto a lib_A. } // ns::h debe definirse en lib_A, pero ns::f y ns::g pueden definirse en otro lugar (por ejemplo, // en un archivo fuente tradicional).
Notas
| Macro de prueba de características | Valor | Estándar | Característica |
|---|---|---|---|
__cpp_modules
|
201907L
|
(C++20) | Módulos — soporte del lenguaje central |
__cpp_lib_modules
|
202207L
|
(C++23) | Módulos de la biblioteca estándar std y std. compat |
Palabras clave
private , module , import , export
Informes de defectos
Los siguientes informes de defectos que modifican el comportamiento se aplicaron retroactivamente a los estándares publicados anteriormente de C++.
| DR | Aplicado a | Comportamiento publicado | Comportamiento correcto |
|---|---|---|---|
| CWG 2732 | C++20 |
no estaba claro si los headers importables pueden
reaccionar al estado del preprocesador desde el punto de importación |
no reacciona |
| P3034R1 | C++20 |
los nombres de módulo y las particiones de módulo podían
contener identificadores definidos como macros tipo objeto |
prohibido |