Los módulos ofrecen una alternativa más sencilla, eficiente y segura frente al sistema tradicional de inclusión de archivos mediante el preprocesador.
Tradicionalmente el código se en C++ se ha organizado en ficheros de cabecera .h y ficheros de implementación .cpp. Pero esto, francamente, siempre ha sido un engorro heredado de C 🤷.
La introducción de módulos en C++11 y su formalización en C++20 busca cambiar por completo la forma en que se estructuran y gestionan los proyectos C++.
El concepto de módulo existe en muchos lenguajes, y se ha convertido en un estándar en programación. Supone una enorme mejora para C++.
¿Qué es un módulo?
En términos sencillos, un módulo en C++ es una unidad de código que se compone de una declaración y definición de funciones, clases, todo en el mismo fichero.
Características clave
- Encapsulamiento: Los módulos permiten definir qué es accesible desde fuera (lo que se exporta) y qué es privado para el módulo (lo que se mantiene interno).
- Optimización de la compilación: Los módulos permiten compilar una vez y reutilizar ese resultado, reduciendo los tiempos de compilación.
- Eliminación de dependencias circulares: Los módulos permiten importar solo los componentes necesarios, evitando las dependencias innecesarias.
Si eres un programador habitual, a estas alturas deberías estar llorando de ilusión por ver todas estas ventajas (yo sigo llorando 😭)
Sintaxis Básica de los Módulos
Declarar un Módulo
Un módulo en C++ se declara mediante la palabra clave module seguida del nombre del módulo.
La declaración se coloca generalmente en un archivo separado con extensión .cppm (o a veces .ixx, aunque .cppm es más común y estándar en la mayoría de los compiladores).
module mymodule; // Declaramos un módulo llamado "mymodule"
export void foo() { // Exportamos la función foo
std::cout << "Hello from foo!" << std::endl;
}
export class MyClass { // Exportamos una clase MyClass
public:
void sayHello() {
std::cout << "Hello from MyClass!" << std::endl;
}
};
En este ejemplo:
module mymodule;declara el módulomymodule.- Las funciones y clases que se prefijan con
exportse hacen accesibles desde fuera del módulo.
Importar un Módulo
Para usar un módulo en otro archivo, simplemente usamos la palabra clave import, que reemplaza al tradicional #include.
import mymodule; // Importamos el módulo "mymodule"
int main() {
foo(); // Llamamos a la función exportada desde mymodule
MyClass obj;
obj.sayHello(); // Usamos la clase exportada desde mymodule
}
En este ejemplo,
- El archivo
main.cppimporta el módulomymoduley usa las entidades que han sido exportadas (la funciónfooy la claseMyClass).
Ejemplo con y sin módulos
Vamos a ver las ventajas de los módulos haciendo un ejemplo sencillo con el sistema tradicional de cabeceras, y con el nuevo sistema de módulos.
Ejemplo usando cabeceras
Primero veamos la opción tradicional, con ficheros de implementación y cabeceras. Tendríamos,
Este archivo incluye el encabezado y usa la función sum.
#include <iostream>
#include "sum.h"
int main() {
int result = sum(3, 4);
std::cout << "The sum is: " << result << std::endl;
return 0;
}
Este archivo contiene la declaración de la función sum.
#ifndef SUM_H
#define SUM_H
int sum(int a, int b);
#endif
Este archivo define la función sum.
#include "sum.h"
int sum(int a, int b) {
return a + b;
}
Para compilar este programa, normalmente se usaría un comando como:
g++ main.cpp sum.cpp -o program
:::
Ejemplo usando módulos
Ahora implementamos el mismo ejemplo usando módulos. En lugar de tener un archivo de encabezado y un archivo de implementación separados, creamos un único archivo de módulo.
Este archivo contiene tanto la declaración como la definición de la función sum, y se exporta para que otros archivos puedan usarlo.
export module sum;
export int sum(int a, int b) {
return a + b;
}
Para utilizar el módulo, simplemente lo importamos en el archivo main.cpp.
import sum;
#include <iostream>
int main() {
int result = sum(3, 4);
std::cout << "The sum is: " << result << std::endl;
return 0;
}
Para compilar el programa con módulos, el comando sería algo similar a:
g++ -fmodules-ts sum.cppm main.cpp -o program
La opción -fmodules-ts se usa en algunos compiladores como GCC para habilitar los módulos de C++20, aunque las opciones pueden variar según el compilador.
Beneficios de los Módulos en C++
En gran medida los módulos de C++ están pensados para sustituir el uso de los archivos de encabezado en el lenguaje.
Los módulos introducen una forma más moderna, eficiente y segura de organizar y reutilizar código, solucionando varios problemas asociados con el sistema de encabezados tradicionales.
Por si aún no os he convencido, vamos a ver algunas de sus ventajas 👇
Compilación más rápida
- En el sistema tradicional, cada archivo
.cppdebe incluir todos los encabezados necesarios, lo cual genera una gran cantidad de procesamiento redundante. - Los módulos permiten compilar unidades de código una sola vez y reutilizarlas, lo que disminuye significativamente los tiempos de compilación.
Mejor encapsulamiento
- Los archivos de encabezado tradicionales exponen tanto la interfaz (las declaraciones de clases, funciones, etc.) como algunos detalles de implementación a otros archivos que los incluyan.
- Los módulos definen de manera clara qué partes del código son visibles y accesibles desde otros módulos o archivos, lo que facilita un mejor control sobre la visibilidad y evita dependencias accidentales.
Eliminación de problemas de redefinición
- Los encabezados suelen requerir el uso de
#include guardso#pragma oncepara evitar múltiples inclusiones que causen errores de redefinición. - Los módulos eliminan la necesidad de estos mecanismos, ya que su diseño previene automáticamente la inclusión múltiple y la redefinición. 🎉🎉🎉
Evitación de macros no deseadas
- Las macros en los encabezados pueden generar conflictos cuando se incluyen en diferentes archivos y pueden afectar partes del código de forma no intencional.
- Con los módulos, las macros no se propagan fuera del módulo en el que se definen.
Mejoras en la lectura y mantenimiento
- Los módulos proporcionan un sistema más organizado que facilita la comprensión de las dependencias del proyecto, ya que cada módulo define explícitamente sus interfaces y no depende del uso de directivas de preprocesador como
#include.