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.
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:
export-tal kezdődik. Ez az egység írja le a modul kifelé
látszó felületét.module M;,
export nélkül. Ugyanahhoz a modulhoz tartozik, de a nevei nem
exportáltak; ez a „belső forrásfájl” modulokban.module M:part; néven a
modul belső tagolása. Partíciót csak a saját modulján belül lehet
importálni — kívülről a import M:part; forma nem elérhető.
A partíció lehet interface (export module M:part;) és
implementation partíció is.export-deklarációval
vagy export { ... } blokkal jelölt entitás. A nem exportált
nevek a modul purview-jában modul-linkage-dzsel (module
linkage) rendelkeznek, tehát más fordítási egységből nem hivatkozhatók —
ez erősebb izoláció, mint a headerben használt static.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.
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.
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).
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:
#define-okra épülő feltételes fordítás (pl.
NDEBUG, feature-kapcsolók) a modulok világában nem működik
tovább: a konfigurációt típusos vagy constexpr alakban kell
átadni, lásd Compile-time adatok és típusos
konfiguráció (33.2).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.
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.
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 buildTá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.
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.
export module m; után #include <vector>:
a std::vector a modulhoz csatolódik. Javítás: global module
fragment, vagy header unit.import config; után #ifdef DEBUG nem működik.
Javítás: constexpr bool/inline constexpr
konfiguráció exportálása.import metrics:core; a main.cc-ben hibás.
Javítás: a fő interface unit exportálja a szükséges neveket.:core
partíció.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.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á.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.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.
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.
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.