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
export
se 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.cpp
importa el módulomymodule
y usa las entidades que han sido exportadas (la funciónfoo
y 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
.cpp
debe 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 guards
o#pragma once
para 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
.