Vai al contenuto

I Moduli di C++20

Scopri perché i moduli sostituiscono gli header, come dichiararli ed esportarli, che cosa sono le partizioni e qual è il percorso di transizione dal codice basato sugli header.

Perché i Moduli Sostituiscono gli Header

Il sistema degli #include è stato per decenni il fondamento della compilazione in C++, ma porta con sé problemi seri. Gli header vengono inclusi testualmente — il preprocessore copia l'intero file dentro ogni unità di traduzione. Questo significa che un #include può incollare oltre 50.000 righe di codice in ogni file .cpp. Il risultato: compilazione lenta, ordine degli include fragile (macro di un header che influenzano un altro) e violazioni della One Definition Rule (ODR) che causano comportamento indefinito silenzioso.

I moduli di C++20 risolvono tutti questi problemi. Un modulo viene compilato una sola volta in un'interfaccia binaria, poi importato dai consumatori senza essere rianalizzato. Non c'è inclusione testuale, non c'è fuga di macro e non c'è dipendenza dall'ordine degli include. Nei progetti grandi i tempi di build possono migliorare di 5-20 volte.

Dichiarazione ed Esportazione di un Modulo

Un modulo ha un'unità di interfaccia (che dichiara ciò che viene esportato) e, facoltativamente, una o più unità di implementazione. La parola chiave export rende le dichiarazioni visibili a chi importa il modulo:

math_utils.cppm
// math_utils.cppm — module interface unit
export module math_utils;    // module declaration

import <cmath>;               // import a header unit

// Exported — visible to importers
export double circle_area(double radius) {
    return std::numbers::pi * radius * radius;
}

export struct Point {
    double x, y;

    double distance_to(const Point& other) const {
        double dx = x - other.x;
        double dy = y - other.y;
        return std::sqrt(dx * dx + dy * dy);
    }
};

// NOT exported — module-private (internal linkage)
double helper_function(double val) {
    return val * val;
}

// Export a namespace — all members become visible
export namespace geometry {
    double triangle_area(double base, double height) {
        return 0.5 * base * height;
    }

    double rectangle_area(double width, double height) {
        return width * height;
    }
}

Importare i Moduli

Chi usa un modulo ricorre a import per accedere alle sue dichiarazioni esportate. A differenza di #include, import non esegue alcuna inclusione testuale: macro e dettagli implementativi non trapelano oltre i confini del modulo:

main.cpp
// main.cpp — imports the module
import math_utils;

#include <iostream>  // can still use #include for non-modularized headers

int main() {
    double area = circle_area(5.0);
    std::cout << "Circle area: " << area << "\n";

    Point p1{0.0, 0.0};
    Point p2{3.0, 4.0};
    std::cout << "Distance: " << p1.distance_to(p2) << "\n";  // 5.0

    std::cout << "Triangle: " << geometry::triangle_area(10, 5) << "\n";

    // helper_function(42);  // ERROR: not exported, not visible

    return 0;
}

// Compile order matters for modules:
// 1. Compile the module interface first:
//    g++ -std=c++20 -fmodules-ts -c math_utils.cppm
// 2. Then compile the consumer:
//    g++ -std=c++20 -fmodules-ts main.cpp -o main

Partizioni di Modulo

I moduli grandi possono essere suddivisi in partizioni — sotto-moduli che insieme formano l'interfaccia completa del modulo. Le partizioni sono interne al modulo e non possono essere importate direttamente da consumatori esterni:

module_partitions.cppm
// --- math_utils-algebra.cppm (partition interface) ---
export module math_utils:algebra;

export double square(double x) { return x * x; }
export double cube(double x) { return x * x * x; }

// --- math_utils-trig.cppm (partition interface) ---
export module math_utils:trig;

import <cmath>;

export double deg_to_rad(double degrees) {
    return degrees * std::numbers::pi / 180.0;
}

export double sin_deg(double degrees) {
    return std::sin(deg_to_rad(degrees));
}

// --- math_utils.cppm (primary module interface) ---
export module math_utils;

// Re-export partitions — consumers see everything
export import :algebra;
export import :trig;

// Additional exports can go here
export double hypotenuse(double a, double b) {
    return std::sqrt(square(a) + square(b));
}

// --- Consumer ---
// import math_utils;  // gets algebra, trig, and hypotenuse

import std (C++23)

Il C++23 introduce import std;, che importa l'intera libreria standard come un modulo. È una svolta per la velocità di compilazione:

import std;  // Tutto: iostream, vector, string, algorithm, ...

int main() {
    std::println("Hello from C++23 modules!");
    std::vector<int> v{1, 2, 3};
    std::ranges::sort(v);
}

import std; sostituisce decine di direttive #include con un unico import che compila molto più in fretta, perché il modulo della libreria standard è precompilato. Esiste anche import std.compat;, che in più mette a disposizione i nomi della libreria C nel namespace globale (come printf e strlen).

Insidia

Il supporto dei compilatori sta ancora maturando: nel 2024 GCC, Clang e MSVC supportano tutti i moduli, ma con livelli di completezza diversi e una diversa integrazione con i sistemi di build. MSVC ha il supporto più maturo.

Integrazione con il sistema di build: CMake 3.28+ supporta i moduli con la funzionalità CXX_MODULE_SETS, ma l'ecosistema (scanner delle dipendenze, package manager) sta ancora recuperando terreno. Ora l'ordine di build conta — le interfacce dei moduli devono essere compilate prima dei loro consumatori.

Nessuna esportazione di macro: i moduli, deliberatamente, non esportano le macro #define. Il codice che dipende da macro definite negli header (come WIN32_LEAN_AND_MEAN o NOMINMAX) deve essere gestito diversamente.

Strategia di migrazione: parti da import std; (C++23) per sostituire gli include della libreria standard, poi modularizza gradualmente il tuo codice cominciando dalle librerie foglia, quelle con poche dipendenze.

Buona pratica

Quando adotti i moduli:

1. Mantieni snelle le interfacce dei moduli — esporta solo ciò che serve ai consumatori. Le entità non esportate hanno module linkage e non possono causare violazioni della ODR.
2. Usa le partizioni per i moduli grandi — suddividili in sotto-moduli logici (ad esempio :types, :algorithms, :io).
3. Preferisci export namespace — esporta interi namespace anziché singole dichiarazioni, per mantenere l'interfaccia ordinata.
4. Non mescolare #include e import per la stessa libreria — scegline uno e sii coerente all'interno di un'unità di traduzione.
5. Usa le estensioni .cppm o .ixx per i file di interfaccia dei moduli, così che i sistemi di build possano riconoscerli automaticamente.

Concetti chiave
  • I moduli sostituiscono #include con import — niente inclusione testuale, niente fuga di macro, niente bug dovuti all'ordine degli include
  • Usa export per rendere le dichiarazioni visibili a chi importa; le entità non esportate restano private al modulo
  • Le partizioni suddividono i moduli grandi in sotto-componenti pur presentando un'interfaccia unificata
  • import std; (C++23) sostituisce tutti gli include della libreria standard con un unico import veloce
  • I sistemi di build devono compilare le interfacce dei moduli prima dei consumatori — l'ordine di compilazione conta

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Qual è il vantaggio principale dei moduli rispetto a #include?

2. Che cosa succede a una funzione definita in un modulo ma NON marcata con `export`?

3. Che cosa fa `import std;` in C++23?