Saltar a contenido

Ú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 once o #ifndef para 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í

https://gcc.gnu.org/projects/cxx-status.html

shape.cppm:14:1: sorry, unimplemented: private module fragment
   14 | module :private;

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:

// foo.cppm
export module foo;

// main.cc
import foo;

La palabra clave export puede usarse en clases, variables, etc.; normalmente hay dos formas de escribirla:

export void func();

export {
  void func();
  constexpr double PI{3.14};
};

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.
export static constexpr double PI = 3.14; // 不可导出
  • 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
namespace light {
  import shape; // not ok
};
  • No se permiten importaciones circulares
// shape.cppm
export module shape;
import circle; // 循环导入

// circle.cppm
import shape;

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

export module foo;
import <iostream>;

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:

export module foo;
#include "consts.h"

2.4.2 #inlcude en la zona Global Module Fragment

Tanto el #include como las macros pueden colocarse directamente en esta zona; por ejemplo:

module;
#ifdef
#include <iostream>
#endif
export module foo;

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:

export module <module-name>:<part-name>;
import :<part-name>; // 可以在import前面添加export导出该分区接口

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.

import shape;

int main() {
    DrawCircle();
    DrawRectangle();
    return 0;
}

shape tiene dos particiones, circle y rectangle, de modo que el contenido del módulo shape.cppm es:

export module shape;

export import :circle;
export import :rectangle;

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.

 error: sorry, module partitions are not yet supported

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:

g++ -fmodules-ts -std=c++20 -x c++ circle.cppm rectangle.cppm shape.cppm shape.cc

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:

shape.cppm:3:8: error: 'import' does not name a type
    3 | export import .circle;

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
export module shape;

export class Shape {
 public:
  Shape();
  void Draw();
};
  • 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

https://gcc.gnu.org/projects/cxx-status.html

Temas relacionados

Comentarios