I moduli di C++20
Per decenni il C++ ha organizzato il codice attraverso i file di intestazione e il meccanismo testuale del preprocessore. Questo sistema, ereditato dal C, ha mostrato con il tempo limiti significativi in termini di prestazioni di compilazione, isolamento e robustezza. Lo standard C++20 introduce i moduli, un modo moderno di suddividere e riutilizzare il codice che affronta alla radice questi problemi, pur richiedendo un cambiamento nelle abitudini consolidate.
I problemi del modello a intestazioni
Nel modello tradizionale, la direttiva #include copia letteralmente il contenuto di un file di intestazione nel punto in cui compare. Se la stessa intestazione viene inclusa da molti file sorgente, il suo contenuto viene analizzato ripetutamente, rallentando la compilazione. Inoltre le macro e le dichiarazioni definite in un'intestazione si propagano a chiunque la includa, generando conflitti e dipendenze indesiderate. Per arginare le inclusioni multiple si è dovuto ricorrere a espedienti come le guardie di inclusione, che complicano il codice senza risolvere il problema di fondo.
Dichiarare ed esportare un modulo
Un modulo si dichiara con la parola chiave module, e ciò che deve essere visibile dall'esterno viene contrassegnato con la parola chiave export. Tutto ciò che non viene esportato rimane un dettaglio interno del modulo, invisibile al codice che lo utilizza. Questo controllo esplicito dell'interfaccia è uno dei vantaggi principali rispetto alle intestazioni, dove ogni dichiarazione è per forza pubblica.
// File: math_utils.cppm
export module math_utils;
// Funzione esportata: fa parte dell'interfaccia pubblica
export int add(int a, int b) {
return a + b;
}
// Funzione interna: non esportata, resta privata al modulo
int helper(int value) {
return value * 2;
}
Importare un modulo
Per utilizzare un modulo si impiega la parola chiave import, che rende disponibili tutti gli elementi esportati. A differenza di #include, l'importazione non copia testo: il compilatore attinge a una rappresentazione già elaborata del modulo, prodotta una sola volta. Ne consegue che le importazioni sono più rapide e non risentono dell'ordine in cui compaiono.
// File: main.cpp
import math_utils;
#include <iostream>
int main() {
// add e visibile perche esportato dal modulo
std::cout << add(4, 7) << std::endl;
return 0;
}
La funzione helper del modulo, non essendo esportata, non è accessibile in main.cpp: rimane un particolare dell'implementazione, completamente isolato dal codice che utilizza il modulo. Questo isolamento previene i conflitti di nomi e chiarisce quale sia il contratto pubblico del modulo.
Separare interfaccia e implementazione
I moduli permettono di distinguere l'unità di interfaccia, che dichiara cosa il modulo offre, dalle unità di implementazione, che contengono il codice effettivo. Questa separazione ricorda la suddivisione tra file di intestazione e file sorgente, ma con confini più netti e senza il costo della rielaborazione testuale ripetuta. Un modulo può inoltre essere suddiviso in partizioni, componenti interni che collaborano tra loro, per organizzare progetti di grandi dimensioni in modo ordinato.
La compatibilità con il codice esistente
La transizione ai moduli è graduale e non impone una riscrittura immediata. È possibile far convivere codice basato su moduli e codice basato su intestazioni, e persino importare intestazioni della libreria standard come unità di intestazione, in attesa che le librerie adottino pienamente il nuovo modello. Il supporto ai moduli da parte dei compilatori e dei sistemi di build è maturato progressivamente, e va verificato nella toolchain specifica prima di adottarli in produzione.
I benefici dei moduli
I moduli offrono vantaggi concreti su più fronti. Le prestazioni di compilazione migliorano perché ogni modulo viene elaborato una sola volta anziché a ogni inclusione. L'isolamento è più solido, poiché le macro e le dichiarazioni interne non si propagano oltre i confini del modulo. L'interfaccia pubblica diventa esplicita e controllata grazie alla parola chiave export. Infine, scompare la necessità delle guardie di inclusione e di molti accorgimenti storici legati al preprocessore. I moduli rappresentano una modernizzazione profonda del modello di organizzazione del codice, destinata a diventare lo standard con cui si struttureranno i progetti C++ del futuro.