Módulos en C++20: El fin de la era de la inclusión textual

El modelo de compilación tradicional de C++ se basa en la inclusión textual mediante la directiva #include. Cuando el preprocesador encuentra un header, simplemente copia y pega el contenido de ese archivo en el punto de inserción. Esto provoca dos problemas graves: la pollución del espacio de nombres (un header puede introducir macros o tipos que no querías) y el tiempo de compilación excesivo (el mismo header se procesa una y otra vez en cada unidad de traducción).

Los módulos de C++20 [C++20] resuelven esto cambiando el paradigma hacia la importación semántica. En lugar de copiar texto, el compilador consume una Interfaz de Módulo Binaria (BMI). Un módulo es una unidad de compilación lógica que se procesa una vez y se almacena en un formato binario optimizado. Cuando haces import, el compilador no lee código fuente; simplemente carga la estructura semántica ya procesada (nombres de tipos, funciones, visibilidad).

Para lograr esto, utilizamos export module nombre_modulo; para definir la unidad. Para que algo sea visible desde fuera, debemos marcarlo explícitamente con la palabra clave export. Si quieres que un módulo actúe como una fachada (facade), puedes usar export import otro_modulo;, lo que permite re-exportar la interfaz de un sub-módulo sin que el usuario tenga que importar ambos. Finalmente, para mantener la encapsulación, podemos usar un fragmento de módulo privado (module :private;) o particiones de módulo para separar la interfaz de la implementación pesada.

El error más crítico ocurre si intentas usar #include después de la declaración export module. Todo lo que deba afectar al espacio de nombres global (como macros de librerías externas o headers de la STL si no usas header units) debe residir en el Fragmento de Módulo Global (module;) al principio absoluto del archivo. Si lo haces mal, romperás las reglas de la ODR (One Definition Rule) o causarás errores de sintaxis crípticos que el compilador no podrá diagnosticar fácilmente.

// --- constants.cppm (Módulo de constantes) ---
export module constants;

export namespace math_data {
    export constexpr double PI = 3.14159265358979323846;
    export constexpr double GRAVITY = 9.80665;
}

// --- physics.cppm (Módulo principal con fragmento privado) ---
module; // Fragmento de módulo global para headers preprocesados
#include <string>
#include <iostream>

export module physics;
export import constants; // Re-exportamos constants para que el usuario no necesite importarlo aparte

export namespace physics {
    class Projectile {
    public:
        Projectile(std::string name, float mass);
        void launch();
        void status() const;
    private:
        std::string name_;
        float mass_;
        void apply_internal_physics(); // Implementación oculta (no exportada)
    };
}

// Sección de implementación (Private Module Fragment)
// Todo lo contenido aquí es invisible para el importador
module :private;

// Re-incluimos headers necesarios para la implementación
#include <iostream>
#include <string>

physics::Projectile::Projectile(std::string name, float mass) 
    : name_(std::move(name)), mass_(mass) {}

void physics::Projectile::launch() {
    // El usuario no ve esta llamada a la función privada
    apply_internal_physics();
    std::cout << "Proyectil " << name_ << " lanzado.\n";
}

void physics::Projectile::status() const {
    std::cout << "Status: " << name_ << " | Masa: " << mass_ << "kg\n";
}

void physics::Projectile::apply_internal_physics() {
    // Lógica compleja que no queremos exponer en el BMI
    std::cout << "[DEBUG] Calculando trayectoria con gravedad: " << math_data::GRAVITY << "\n";
}

// --- main.cpp (Consumidor del módulo) ---
// Compilación (ejemplo GCC): 
// g++ -std=c++20 -fmodules-ts constants.cppm physics.cppm main.cpp -o app

import physics;
import <iostream>; // Header unit: importa <iostream> como un módulo

int main() {
    // Gracias a 'export import', podemos usar PI directamente desde physics
    std::cout << "Valor de PI: " << math_data::PI << "\n";

    physics::Projectile p("Interceptor", 15.5f);
    p.launch();
    p.status();

    // p.apply_internal_physics(); // ERROR DE COMPILACIÓN: No es visible
    
    return 0;
}

Desglose del ejemplo

  1. export module constants;: Define una unidad de módulo independiente. Al usar export constexpr, la constante PI se incluye en la interfaz del módulo.
  2. export import constants;: En el módulo physics, esto hace que cualquier código que haga import physics; también tenga acceso a math_data::PI de forma automática. Esto reduce la carga cognitiva del usuario.
  3. module; (Global Module Fragment): Es vital. Si necesitas usar #include <string> para definir la estructura de la clase, debe ir aquí. Si lo pones después de export module physics;, el compilador intentará tratarlo como parte del módulo, lo cual es un error de diseño.
  4. module :private;: Esta es la clave de la optimización. Todo lo que escribimos después de esta línea (la implementación de los métodos de Projectile y la lógica de apply_internal_physics) no se codifica en el archivo BMI que se distribuye. El importador solo recibe la “promesa” de que esos métodos existen, pero no el código de su cuerpo. Esto permite cambiar la implementación de apply_internal_physics sin obligar a recompilar todos los archivos que importan physics.
  5. import <iostream>;: Esto se conoce como una header unit. A diferencia de #include <iostream>, el compilador procesa el header una vez y lo convierte en un módulo binario, siendo mucho más rápido en proyectos de gran escala.

El error frecuente

Un error clásico al migrar de headers a módulos es intentar gestionar dependencias circulares como si fueran cabeceras. En el modelo de cabeceras, puedes usar forward declarations para romper ciclos. En los módulos, si el Módulo A importa el Módulo B, y el Módulo B importa el Módulo A, el compilador se detendrá con un error de dependencia circular. Los módulos imponen un Grafo Acíclico Dirigido (DAG) estricto. Si tienes ciclos, la única solución es extraer la interfaz compartida a un tercer módulo o usar particiones de módulo (export module mi_modulo:parte1;) para dividir la unidad sin crear dependencias entre archivos distintos.

104

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio