33.4 C++20 modules és header unit

A header-alapú fejlesztés két rejtett költséget hordoz: a preprocesszor minden fordítási egységben újra beolvassa és feldolgozza ugyanazokat a deklarációkat, és a határok nem nyelvi határok, ezért a fejlécbe került belső részlet óhatatlanul a felhasználók interfészének része lesz. A C++20 modulok (modules) mindkettőre választ adnak: a deklarációk egyszer, egy lezárt határú egységben fordulnak le, az export pedig explicit. Ez a rész a nyelvi szerződést és a hozzá tartozó build gráfot tárgyalja; a fordítási időt itt is erőforrásként kezeljük — a mérés protokollját megadjuk, de számszerű eredményt nem állítunk.

33.4.1 A modul mint nyelvi és buildhatár

Egy modul (module) nevesített deklarációhalmaz, amelyet egy vagy több modul-egység (module unit) alkot. A moduldeklaráció (export module M; vagy module M;) egy fordítási egységet modul-egységgé tesz; a deklarációnak a fordítási egység első deklarációjának kell lennie (a global module fragment kivételével). A moduldeklarációt követő deklarációk a modul purview-jába (module purview) tartoznak.

Feltételek és szerepek:

Fontos határ: a modul nem bináris kompatibilitási szerződés. Az exportált osztályok elrendezése, az inline függvények és a name mangling továbbra is ABI-kérdés, lásd Névtorzítás, layout és C++ runtime ABI (36.1) későbbi részét.

33.4.2 Moduldeklaráció, export és import

A minimális szerkezet:

export module shapes;      // interface unit, a modul neve: shapes

export int area(int w, int h);   // exportált deklaráció

namespace detail {                // nem exportált: modul-linkage
int scale(int v);
}

Az import deklaráció szabályai: nevesített modult és header unitot csak névtér-hatókörben (namespace scope) lehet importálni, függvényen vagy osztályon belül nem; egy fordítási egységben a moduldeklaráció után, az első deklarációk között érdemes tartani. Egy modul-egységnek pontosan egy moduldeklarációja lehet, és a modulgráf nem tartalmazhat körkörös importot — ez a tervezésre is kihat: a partíciók közötti függőségnek irányítottnak kell lennie.

A export import :part; azt jelenti, hogy a partíció importált nevei nemcsak láthatók, hanem tovább is exportáltak. Ez a „homlokzat” minta alapja: a fő interface unit néhány soros, és partíciókra bontja a felületet.

33.4.3 Interface, implementation unit és partition: kidolgozott példa

A példa egy metrics modul: a :core partíció deklarálja az adattípust és a számítást, a :format partíció szövegesít, a fő interface unit pedig újraexportálja mindkettőt. A körkörös import elkerülése érdekében a formázó partíció a :core-tól függ, nem a fő egységtől.

// src/metrics-core.ccm
module;                       // global module fragment kezdete
#include <cstddef>
#include <vector>
export module metrics:core;   // interface partíció

export namespace metrics {
struct Summary { std::size_t count; double mean; };
Summary summarize(const std::vector<double>& xs);
}
// src/metrics-impl.cc  (implementation partíció: nincs export)
module metrics:core;

namespace metrics {
Summary summarize(const std::vector<double>& xs) {
    if (xs.empty()) return {0, 0.0};
    double sum = 0.0;
    for (double x : xs) sum += x;
    return {xs.size(), sum / static_cast<double>(xs.size())};
}
}
// src/metrics-format.ccm
module;
#include <string>
export module metrics:format;

import :core;                 // partíció importja csak a modulon belül

export namespace metrics {
std::string format(const Summary& s);
}
// src/metrics.ccm  (fő interface unit: vékony homlokzat)
export module metrics;
export import :core;
export import :format;
// src/main.cc
#include <cstdio>
#include <vector>
import metrics;

int main() {
    std::vector<double> xs{1.0, 2.0, 6.0};
    auto s = metrics::summarize(xs);
    std::printf("%zu %s\n", s.count, metrics::format(s).c_str());
}

Értelmezés: a main.cc sosem látja a detail-szintű segédfüggvényeket, és nem függ attól, hogy a summarize teste melyik fájlban van. A Summary egyetlen definícióval rendelkezik a :core purview-jában, tehát a header-világ klasszikus ODR-hibái (két, eltérő makróállapot mellett fordított definíció) itt szerkezetileg nehezebben állnak elő — de nem kizártak, lásd a BMI-szakaszt és ODR, inline és több fordítási egység (33.3).

33.4.4 Global module fragment, legacy header és header unit

A global module fragment (module; … az első moduldeklarációig) az egyetlen hely, ahol a modul-egységben hagyományos #include-ot érdemes használni. Az itt bevont deklarációk a globális modulhoz csatolódnak, nem a modul purview-jához. Ha egy fejlécet a moduldeklaráció után include-olunk, annak entitásai a modulhoz csatolódnak — ez más modul-attribúciót ad ugyanannak a fejlécnek, ha máshol is include-olják, és ez ODR-sértéshez, illetve diagnosztizálatlan viselkedéshez vezethet. Gyakorlati szabály: include csak a fragmentben, vagy sehol.

A header unit a fejléc lefordított formája: import <vector>; vagy import "my_header.h";. Ilyenkor a fejléc deklarációi egy külön lefordított egységből származnak, nem szövegesen másolódnak be. Két következmény:

A header unit támogatottsága egyenetlen: a CMake modulszkennelése jellemzően a named module-ökre épül, a header unitot a build rendszer gyakran nem kezeli automatikusan. Ezt a választott kiadás dokumentációjában kell ellenőrizni, mielőtt a import <...>; formát a projekt alapjává tesszük.

33.4.5 A BMI és a build gráf toolchainfüggősége

A fordító a modul-egységből BMI-t (Binary Module Interface) készít; a nevek implementációnként eltérnek (Clang .pcm, MSVC .ifc, GCC esetében a modul-gyorsítótárazott könyvtár). A BMI nem stabil, hordozható artefaktum: nem szerződés két compiler vagy két verzió között, és nem is cél, hogy az legyen (ezt a témaköri határ kizárja).

Artefaktum Előállító Mihez kötött Hordozhatóság
forrás (.ccm, .cpp) fejlesztő — hordozható
BMI compiler compiler és verzió, target, nyelvi mód, flag-ek, makróállapot, az importált modulok BMI-je nem hordozható; újragenerálandó
tárgykód (.o) compiler target ABI, flag-ek ABI-függő
szkennelési gráf scanner + build rendszer scanner verziója a build gépen újragenerálható

Ebből következik a build gráf alakja: a fordítás nem lineáris, hanem DAG. Először szkennelni kell a forrásokat, hogy kiderüljön, melyik egység melyik modult importálja; a topológiai sorrendben elkészülnek a BMI-k, majd a tárgykódok. A main.cc csak akkor fordítható, ha a metrics BMI-je már létezik.

Kritikus következmény: ha ugyanazt a modult kétféle flag-készlettel (pl. eltérő -D makróval, -O szinttel vagy -std értékkel) fordítjuk le, a két BMI nem ugyanazt a programszabályt képviseli. A build rendszernek a BMI-t a teljes fordítási konfigurációra kell kulcsoznia, különben a különböző TU-k eltérő definíciót látnak — ez pontosan az ODR, inline és több fordítási egység (33.3) ODR-szabályának megsértése, és a tünet gyakran nem linkelési hiba, hanem csendes viselkedésbeli eltérés.

33.4.6 CMake modulszkennelés és támogatási feltételek

Modern CMake-ben a modulokat a FILE_SET CXX_MODULES és a bekapcsolt szkennelés kezeli. Az alábbi CMakeLists.txt a fenti példához tartozik; a verziószámok és a compilerflag-ok ellenőrizendők a laborban, nem tekintendők itt igazoltnak:

cmake_minimum_required(VERSION 3.28)   # a támogatás verziófüggő: ellenőrizd!
project(metrics_demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_SCAN_FOR_MODULES ON)     # pontosabban target property-ként

add_library(metrics)
target_sources(metrics
  PUBLIC FILE_SET CXX_MODULES FILES
    src/metrics.ccm
    src/metrics-core.ccm
    src/metrics-format.ccm
  PRIVATE
    src/metrics-impl.cc)
target_compile_features(metrics PUBLIC cxx_std_20)

add_executable(demo src/main.cc)
target_link_libraries(demo PRIVATE metrics)
# laborprofil-jelölt; a compiler- és CMake-verziót rögzíteni kell
cmake -S . -B build -G Ninja \
  -DCMAKE_CXX_COMPILER=g++-14 \
  -DCMAKE_CXX_FLAGS="-fmodules-ts"
cmake --build build

Támogatási feltételek, amelyeket a felvétel előtt ellenőrizni kell: (1) a compiler verziója támogatja-e a partíciókat és a szóban forgó flag-et; (2) a build rendszer verziója ismeri-e a CXX_MODULES file setet; (3) a generátor (Ninja/Makefile) képes-e a dinamikusan felfedezett függőségek kezelésére; (4) a fordító tud-e helyes sorrendet felállítani a BMI-kból. Ha bármelyik hiányzik, a projekt essen vissza a headeres változatra — ez nem kudarc, hanem a hordozhatósági szerződés része.

33.4.7 Headeres alternatíva és a fordítási idő

Ugyanaz a felület headerrel: include/metrics.hpp (a Summary és a két függvény deklarációja), src/metrics_header.cpp (a definíciók), és a main a #include "metrics.hpp" formát használja.

add_library(metrics_header STATIC src/metrics_header.cpp)
target_include_directories(metrics_header PUBLIC include)
add_executable(demo_header src/main_header.cc)
target_link_libraries(demo_header PRIVATE metrics_header)

A fordítási idő összehasonlítása csak azonos protokollal értelmes: tiszta build (rm -rf build), azonos gép és terhelés, azonos flag-ek, hideg gyorsítótár, legalább három futás mediánja, és a mért mennyiség kimondása (teljes build, adott target újrafordítása, vagy csak a fejlécfeldolgozás). Compiler-specifikus profilozás (GCC -ftime-report, Clang -ftime-trace) alkalmas a bontásra, de a számok target- és verziófüggők. Ebben a könyvben nem állítunk mért értéket; a mérés a tanuló laborjának feladata.

33.4.8 Tipikus hibák

33.4.9 Feladatok

  1. Alakítsd át a metrics példát úgy, hogy a format függvény a :core partícióba kerüljön, és indokold, miért romlik vagy javul a build gráf.
  2. A main.ccben a #include <cstdio> után import metrics; áll, és a printf formátumsztringjében %zu szerepel. Egészítsd ki a példát egy olyan második main-változattal, amely std::format-ot használ, és mondd meg, milyen include/import és milyen szabványfeltétel kell hozzá.
  3. Tervezz olyan CMake-konfigurációt, amely ugyanabban a projektben egyszerre tartalmazza a modulos metrics és a headeres metrics_header változatot, és magyarázd el, miért nem kerülhet ugyanabba a célba a metrics.hpp include-ja és a metrics modul importja.
  4. Készíts mérési tervet a két változat fordítási idejének összehasonlítására; add meg a független változókat, a rögzítendő feltételeket és azt, hogy mit Nem lehet a mérésből kiolvasni.

33.4.10 Megoldások

1. A format átkerül a :core-ba: a :format partíció megszűnik, a fő interface unit csak export import :core;-t tartalmaz. A build gráf csomópontjai száma csökken (egy BMI-vel kevesebb), a párhuzamosíthatóság romlik, mert a formázás (és a <string> feldolgozása) a :core BMI-jébe kerül, így minden :core-tól függő egység a formázás fordítási költségét is viseli. Ellenőrzési feltétel: a metrics felülete változatlan (a format továbbra is metrics::format), és a main.cc változtatás nélkül fordul.

2. A std::format a C++20 <format> fejlécében érhető el, de a library-támogatás nem minden C++20-as implementációban teljes; ezért a laborprofilban ellenőrizni kell. Modulos változatban a fejlécet a fragmentbe kell tenni:

module;
#include <format>
#include <string>
export module metrics:format;
import :core;
export namespace metrics {
std::string format(const Summary& s);
}

Ellenőrzési feltétel: a main.cc-ben továbbra is #include <cstdio> maradhat (nem modul-egység), és a %zu helyett {} formázás használható; ha a <format> nem elérhető, a megoldás nem tekinthető késznek, csak a C++20-as alapra visszavezetett változat.

3. Két külön target: metrics (FILE_SET CXX_MODULES) és metrics_header (STATIC, target_include_directories-szel). Egy célon belül a metrics.hpp include-ja és a import metrics; egyazon TU-ban nem keverhető össze: a Summary két különböző modul-attribúciójú definíciója keletkezne (header → globális modul, modul → metrics purview), ami ODR-sértés. Ellenőrzési feltétel: a két executable külön target, és a main.cc csak az egyik formát használja.

4. Független változó: a felület megvalósítási módja (modul vs. header). Rögzítendő: compiler és verzió, CMake/Ninja verzió, flag-ek, gép, mag- és memóriakonfiguráció, hideg gyorsítótár, ismétlésszám. Mérendő: teljes tiszta build falaóra-ideje és a demo target újrafordítási ideje. Nem olvasható ki: általános következtetés más compilerre vagy targetre, és nem bizonyíték arra, hogy a modul „mindig gyorsabb” — a BMI előállítás egyszeri költsége és a párhuzamosíthatóság is számít.

33.4.11 Összefoglaló

A modul nyelvi határ: az interface unit exportál, az implementation unit és a partíció belső szerkezetet ad, a partition külvilág felé nem importálható. A #include helye a global module fragmentre korlátozandó, a header unit deklarációkat közvetít, makrókat nem. A BMI compiler-, verzió- és flagfüggő artefaktum, ezért a build gráf DAG-ját szkennelés építi fel, és a konfigurációt kulcsozni kell. A CMake CXX_MODULES támogatása verziófüggő, és mindig kell legyen headeres visszaesési út. A fordítási idő csak dokumentált protokollal hasonlítható össze.

33.4.12 Források és korlátok

Ehhez az egységhez nem kaptam olvasott forrásjegyzetet, ezért itt nincs hivatkozható elsődleges forrás és nincs igazolt szakaszszám. A hivatkozott szabványelemek (C++20 [module], ODR, constexpr) nevesítve, de paragrafusra nem támaszkodva szerepelnek; a compiler-, CMake- és flag-részletek verziófüggők, és a megadott buildparancsok nem futtatott, ellenőrizendő példák. A tanulónak a választott compiler, standard library és CMake kiadás hivatalos dokumentációjában kell igazolnia a támogatást, mielőtt a modulokat a projekt alapjává teszi.