Última actualización:2026-10-12
Módulos Modules de C++20¶
Cuando usamos archivos de cabecera propios o bibliotecas de terceros, solemos recurrir a la directiva #include para introducir la biblioteca; es una forma que todos usan con frecuencia. Este método, en realidad, copia todo el código de un archivo fuente (la cabecera) dentro de otro archivo.
Esto plantea los problemas siguientes:
- El archivo fuente puede incluirse varias veces en el mismo objetivo, por eso solemos usar
#pragma onceo#ifndefpara evitar que el archivo fuente se incluya varias veces en la misma unidad de traducción. - La copia de código alarga los tiempos de compilación: en cuanto se modifica una cabecera, todos los archivos que la incluyen indirectamente deben recompilarse.
- Los problemas de orden del
#include, que a veces provocan errores de compilación inexplicables.
C++20 introdujo una nueva forma de sustituir la #include 指令: los módulos.
Veamos a fondo los módulos.
1.Unidades de módulo¶
Un módulo de C++ se compone de una o más unidades de traducción (tu) que contienen palabras clave específicas para la declaración del módulo. A esas unidades de traducción se las llama unidades de módulo.
Las unidades de traducción que no son unidades de módulo se consideran parte del módulo global; el módulo global es anónimo, no tiene interfaz y contiene el código normal que no es de módulo.
1.1 Global Module Fragment¶
Una unidad de módulo puede llevar como prefijo un fragmento de módulo global; cuando no es posible importar un archivo de cabecera (en especial cuando la cabecera usa macros del preprocesador como configuración), este fragmento de módulo global puede usar directamente el código original.
Por ejemplo: en el código siguiente, lo que hay entre module; y export module Foo; es el global module fragment.
module;
#include <iostream>
#ifdef Say
void hello();
#endif
export module Foo;
// purivew
void world();
Un detalle importante: si el módulo tiene un fragmento de módulo global, la primera declaración debe ser module;; es decir, colocar esta declaración en otra posición da error.
1.2 purview¶
El purview puede entenderse como todo el ámbito del módulo: empieza en la declaración del módulo y se extiende hasta el final de la unidad de traducción.
Por ejemplo: hello no está, mientras que world y GetData sí están en el purview.
module;
void hello();
// <- 这里不在Foo模块的purview内
export module Foo;
// <- 在Foo模块的purview内
void world();
export void GetData();
1.3 Private module fragment¶
La unidad de interfaz del módulo principal puede llevar como sufijo un fragmento de módulo privado; esta parte solo puede aparecer en la unidad de interfaz del módulo principal y, si existe, la unidad de módulo donde aparece debe ser la única unidad del módulo. Su propósito es encapsular la interfaz y la implementación del módulo en una sola unidad de traducción, sin exponer los detalles de implementación.
Por ejemplo: quiero crear un Shape y calcular su área.
Hacia el exterior solo hace falta exponer una interfaz que cree el Shape concreto e invoque la interfaz común de cálculo del área; así podemos escribir el módulo siguiente.
export module Shape;
export class Shape {
public:
virtual double CalculateArea() = 0;
};
export { std::shared_ptr<Shape> CreateRectangle(double length, double width); }
module :private; // here
class Rectangle : public Shape {
private:
double length;
double width;
public:
Rectangle(double l, double w) : length(l), width(w) {}
double CalculateArea() override { return length * width; }
};
std::shared_ptr<Shape> CreateRectangle(double length, double width) {
return std::make_shared<Rectangle>(length, width);
}
¿Poner todos los detalles internos en private? Mi propia versión de g++ es la 13 y todavía no lo soporta; da el error siguiente:
Sobre el soporte actual de gcc, puedes consultar aquí
Mi clang local es la versión 16; lo he probado y funciona correctamente.
➜ clang++ -std=c++20 shape.cppm --precompile -o shape.pcm
➜ clang++ -std=c++20 shape.cc -fprebuilt-module-path=. shape.pcm -o shape
➜ ./shape
area is 2
De las tres partes anteriores, los fragmentos de módulo global y privado no son necesarios para la existencia del módulo; el purview es la parte obligatoria del módulo.
2.Uso de los módulos¶
2.1 Crear un módulo¶
Crear un módulo es parecido a definir un archivo de cabecera: también tiene su archivo, cuya extensión habitual es .cppm. Basta con usar en ese archivo las palabras clave export y module, seguidas del nombre del módulo, y con ello queda creado un módulo exportable. Por ejemplo:
La palabra clave export puede usarse en clases, variables, etc.; normalmente hay dos formas de escribirla:
Una forma pone la palabra clave export delante de declaraciones habituales; la otra es el bloque de exportación, parecido a la escritura de namespace, que permite exportar varios contenidos.
2.2 Exportar¶
Aquí surge una cuestión importante: ¿qué se puede exportar?
- variables, classes, structs, functions, namespaces, template functions/classes, concepts pueden exportarse
- Lo que tiene enlace interno no es exportable, como las variables y funciones static, o los namespace anónimos.
- Las declaraciones de exportación deben producirse a nivel de espacio de nombres
namespace {
export void print_no_export() { // 匿名命名空间,不可导出
std::cout << "print no export" << std::endl;
}
};
namespace light {
export void print_export() { // ok
std::cout << "print export" << std::endl;
}
};
struct Foo {
export int a; // 不能导出成员变量
};
- Exportar un espacio de nombres exporta implícitamente todo su contenido
export class Rectangle {
private:
double length;
double width;
public:
Rectangle(double l, double w) : length(l), width(w) {}
double CalculateArea() { return length * width; } // 隐式导出
};
export namespace {
void print_export() { // 隐式导出
std::cout << "print export" << std::endl;
}
};
export {
void func(); // 隐式导出
constexpr double PI{3.14}; // 隐式导出
};
- La primera declaración de una entidad exportada debe ser una declaración de exportación. Las declaraciones y definiciones posteriores no necesitan la palabra clave export.
export class Foo; // ok
export class Foo; // ok,只是会冗余
class Foo { // ok,隐式export
public:
void print() { std::cout << "this is foo" << std::endl; }
};
class Bar; // not export
export class Bar; // 无效,第一个声明已经是不可导出,后续的不可导出
Foo f; // ok
f.print(); // ok
Bar b; // not ok
- Un módulo no exportable no puede exportar
module foo;
export void print { } // error: 'export' may only occur after a module interface declaration
2.3 Importar¶
La operación correspondiente es importar; la importación también tiene sus reglas, por ejemplo:
-
No se puede importar a sí mismo
-
En una unidad de módulo, todas las importaciones deben aparecer antes que cualquier declaración de esa unidad de módulo. No se puede importar en un punto arbitrario de la unidad de módulo.
void func() {}
import shape; // error: post-module-declaration imports must be contiguous
---------------
import shape; // ok
void func() {}
- Solo se permiten importaciones en el ámbito global
- No se permiten importaciones circulares
2.4 include dentro de los módulos¶
¿Cómo usar #include <iostream> en un módulo?
2.4.1 #include en la zona purview¶
Sustituir #include por import
Por ejemplo:
Compilando con g++-13 como sigue, se puede compilar un gcm pasando iostream detrás de c++-system-header.
g++-13 -std=c++20 -fmodules-ts -x c++-system-header iostream
g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm shape.cc
Si se trata de una cabecera propia, por ejemplo consts.h, se descubre que puede ponerse directamente dentro del módulo con #include; por ejemplo:
2.4.2 #inlcude en la zona Global Module Fragment¶
Tanto el #include como las macros pueden colocarse directamente en esta zona; por ejemplo:
3.Descomposición de módulos¶
Cuando queremos descomponer un módulo grande en módulos más pequeños, podemos usar estos dos métodos:
- Particiones de módulo.
- Es decir, nos permiten descomponer el módulo en varios archivos. Sin embargo, esto resulta de hecho invisible para quien lo usa: basta con importar el módulo con normalidad.
- Submódulos.
- Es decir, nos permiten descomponer módulos grandes en una jerarquía con cualquier número de submódulos. Quien lo usa puede elegir importar el módulo completo o solo submódulos concretos.
3.1 Particiones de módulo¶
Sintaxis:
Por ejemplo: tengo un shape; para usarlo hacia el exterior basta con import shape y luego llamar a las interfaces correspondientes; aquí se usan las interfaces de la partición circle y de la partición rectangle.
shape tiene dos particiones, circle y rectangle, de modo que el contenido del módulo shape.cppm es:
El contenido de las dos subparticiones es:
// circle.cppm
module;
#include <iostream>
export module shape:circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }
--------------
// rectangle.cppm
module;
#include <iostream>
export module shape:rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }
Al compilar con clang y g++ he comprobado que clang-16 da error de compilación: no lo soporta.
g++-13 sí lo soporta; conviene tener en cuenta que al compilar hay que seguir el orden subpartición -> partición principal, o saldrá un error.
➜ g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm rectangle.cppm shape.cc
shape:circle: error: failed to read compiled module: No such file or directory
shape:circle: note: compiled module file is 'gcm.cache/shape-circle.gcm'
shape:circle: note: imports must be built before being imported
shape:circle: fatal error: returning to the gate for a mechanical issue
Debería cambiarse a:
3.2 Submódulos¶
Sintaxis:
export module <module-name>.<sub_module-name>;
import <module-name>.<sub_module-name>; // 可以在import前面添加export导出该分区接口
El ejemplo anterior también puede implementarse con submódulos: seguimos tomando shape como módulo principal, y los dos submódulos son circle y rectangle.
// shape.cppm
export module shape;
export import shape.circle;
export import shape.rectangle;
---------------------
// rectangle.cppm
module;
#include <iostream>
export module shape.rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }
---------------------
// circle.cppm
module;
#include <iostream>
export module shape.circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }
Puede verse que en el uso las particiones de módulo emplean :, mientras que los submódulos emplean ..
El código del lado que llama es el mismo que con las particiones de módulo.
Sin embargo, hay algunas diferencias al usarlos; por ejemplo: al introducir una subpartición, usar su interfaz provoca un error: internal compiler error: Segmentation fault: 11, mientras que el submódulo sí puede importarse y usarse con normalidad.
// 引入子分区
import shape:circle;
int main() {
DrawCircle(); // error
return 0;
}
// 引入子模块
import shape.circle;
int main() {
DrawCircle(); // ok
return 0;
}
Para los submódulos, al importar no puede omitirse el nombre del módulo principal; arriba, al introducir el módulo de partición dentro de la partición principal, podemos usar :circle, pero aquí no puede usarse .circle. El error es el siguiente:
4.Interfaz e implementación¶
Al escribir código solemos dividirlo en «archivo de cabecera» y «archivo de implementación»; con los módulos, esta operación deja de ser necesaria, aunque puede mantenerse el estilo de escritura de antes.
Por ejemplo: dibujar un shape.
- Definir el módulo shape.cppm
- Implementar el módulo shape.cc
import shape;
import <iostream>;
Shape::Shape() {}
void Shape::Draw() { std::cout << "draw a shape" << std::endl; }
Es básicamente igual al modelo de separación .h y .cpp que usamos habitualmente.
https://clang.llvm.org/docs/StandardCPlusPlusModules.html
https://timsong-cpp.github.io/cppwp/n4861/module.import#7.sentence-2
Temas relacionados¶
- Introducción a las corrutinas de C++20 | C++那些事
- C++20 channel en la práctica | C++那些事
- Conceptos Concepts de C++20 | C++那些事
- constexpr y la expresión coma en C++20 | C++那些事
- Orden de bytes endian en C++20 | C++那些事
- Expresiones lambda de C++20 (parte 1) | C++那些事
- La biblioteca Ranges de C++20 | C++那些事
- Diseño y trampas de std::span en C++20 | C++那些事
- Guía de las novedades de C++20 | C++那些事
- C++20 [[likely]] y [[unlikely]] | C++那些事