Po skoro dvou letech se vracím k modulům. Už se to dá používat? Jak daleko se posunul vývoj? A jaké máme nástroje?
Moduly se do C++ dostaly s verzí 20 a byly doručeny jen jako papírový „koncept“, než reálná funkční věc. V tomto směru byla situace horší, než třeba u korutin, jedná se zase o další oblast, kde standarizační komise řeší budoucnost, než aby se snažila pokrýt současnost. Rozdílem oproti korutinám byl ten, že korutiny fungovaly a pouze chyběl (chybí) standardní výbava STL. Korutiny tak stále jsou v režimu „udělej si sám“.
S moduly je to horší, protože jejich fungování je závislé na překladačích, a ty v roce 2020 nebyly ready. Po cca 3 letech sice proběhl živelný vývoj, ale o připravenosti se nedalo mluvit, nejdále v tomhle směru byl Microsoft, který byl schopen (prý) celý svůj office přeložit v modulech a výrazně tak zrychlit překlad. Problém je v tom, že Microsoft si opět šel vlastní cestou ve formě nějakých extensions do svého MSVC ekosystému, z poloviny neveřejných. Ostatních překladače na tom byly hůře, překladače padaly, hlásily chyby ICE (internal compiler error) a některé konstrukce nebylo možné přeložit. Nejhůře na tom byl GCC. Ovládací rozhraní nebyla sjednocena a způsob sestavení se lišil podle překladače. Více najdete v mém starším článku
Uběhly další skoro dva roky, a jaká je situace dnes? Lepší, ale ne ideální.
Dodám, že mě to zklamalo natolik, že jsem si vyvinul vlastní build system! A díky tomuto build systému se programování modulů stalo výrazně jednodušší a hlavně jsem s tím schopen přeložit středně velký projekt do finální binárny (ve všech třech překladačích) a funguje!
Proto tento článek berte jako takové představení nástroje, který se jmenuje cairn.
(tento název je anglickým překladem slova mužík, což je taková ta hromada kamenů, které tvoři mohylu, nebo turistickou značku, vyznačuje se tím, že horní kámen je podpírán spodními kameny, jako náš modulový program)
Moduly v C++ jsou navrženy tak, že se zásadně mění pohled na organizaci projektu. Už první změnou, kterou si člověk uvědomí je způsob vyvolávání modulu. Na rozdíl od headerů se totiž nepoužívají názvy souborů jako referenci na zdrojový kód. Místo toho se používá název modulu. Porovnejte následující příklad
Klasické #include "utils/threadpool.hpp" #include <nlohamm/json.h> #include <vector> Moduly import cairn.utils.threadpool import nlohamm.json import std; //import <vector>
Rozeberme se jednotlivé řádky
/usr/local/include/nlohamm/json.h. U modulů opět jen plně kvalifikované jméno modulu a nic vícTo hlavní, co si z toho je třeba odnést je, že nové modulové zdrojové kódy se vůbec neodvolávají na soubory, a je věcí build systému, aby dokázal vyhledat a poskytnout překladačům referenci na soubory, v nichž se importované moduly nachází.
Můj osobní názor: Je to skvělá věc, protože přidává svobodu do organizaci projektu. Není třeba přesně dodržovat struktůru projektu diktovanou externímy knihovnami. Můžeme si projekt uspořáda tak ja potřebujeme , stačí jen build systému sdělit, jak má patřičné moduly najít.
Další úlohou nástroje cairn je samozřejmě pomáhat s identifikací jednotlivých souborů. To že se soubor jmenuje log.ifc.cpp neříká nic o tom, co je zač. Jedná se možná o interface, ale neměli bychom se spoléhat na jméno. Navíc těch informací budeme potřebovat víc.
Jakmile překladač má přeložit nějaký soubor importující moduly, musí dostat fyzický obsah modulů jako součást příkazové řádky, nebo nepřímo přes referenci na repozitář. To co ale překladač potřebuje není zdrojový kód, ale binární reprezentace interface. Co to znamená?
TIP: prostudujte si jak jsou moduly definované ve standardu
Pokud nějaký modul něco exportuje, je třeba z jeho textu vygenerovat soubor BMI. To je zkratka binary module interface. – pozor, žádný překladač to nepoužívá jako příponu, najdete tam přípony gcm (gcc compiled module – gcc), pcm (precompiled module – clang) nebo ifc (interface – msvc). Soubor obsahuje všechny exportované symboly, tedy nejen funkce, ale i celé třídy, typy, dokonce šablony a to v binární podobě. Výhodou je, že překladač už nemusí provádět textovou analýzu (parsing), samotné načtení BMI je velice rychlé a mělo by to výrazně urychlit překlad.
Nevýhodou tohoto řešení je, že najednou záleží na pořadí překladu. Pokud nějaký modul importuje jiný modul, musí tento importovaný modul být přeložen první a teprve potom může být referencován a importován. Závislosti je třeba vyřešit před zahájením překladu. Proto na to nelze použít make se soubory deps, protože ty jsou nepovinné, generují se při prvním překladu a v zásadě jen pomáhají build systému detekovat změny. Tam chybějící závislost není chyba, pouze způsobí nový překlad, zatímco zde chybějící závislost způsobí selhání překladu
Shrnutí: Nástroj cairn řeší
Kromě jiného se snaží řešit i jednoduchou standarizaci rozhraní pro všechny tři překladače, aspoň na úrovni rozhraní k modulům.
O
( ) Cairn
()( ) C++20 module builder
( )( )
( )( )
()( ) )
( )( ) )
(__)(__)(_)
Když už jsem se rozhodl, že napíšu vlastní build systém, kladl jsem si za cíl neduplikovat funkci konkurenčních systému, jako je třeba cmake. Spíš než komplikovaný nástroj na definici projektu mi tak jde jen o něco, co spustí překladač a vytvoří spustitelný soubor.
Nejprve malé zamyšlení. V C++ modules platí že všechno je modul. Prakticky se nebudete setkávat s jiným typem souborů. Tedy až na jednu výjimku a to je počáteční soubor. Ten totiž jediný nemusí být modul, ale pouze soubor cpp, který importuje ostatní moduly, ale sám nic neexportuje. Přeložením tohoto souboru tak můžeme získat jedině spustitelný soubor – no a nebo bootstrap knihovny, dynamické knihovny atd. Protože tento soubor obsahuje všechny importy, které drží projekt pohromadě, to je ten nejvyšší kámen v mohyle.
Z pohledu širšího build systému (zde myslím cmake) jde o sestavení targetu z jednoho jediného cpp. Stačí tedy target nadefinovat jako add_custom_target a uvést celou příkazovou řádku.
A tu si teď ukážeme
$ cairn <přepínače> <target>=<soubor.cpp> <compiler> <přepínače překladače>
g++, g++-15, clang++, clang++-20, cl.exe, případně rozšířeně pro msvc cl.exe@x64-17.13 , kdy se za @ uvádí verze překladač a cílová platforma (ti co mají msvc budou vědět)Příklad:
$ cairn -j16 -cbuild/compile_commands.json -Bbuild \
bin/cairn=src/cairn/main.cpp \
clang++-18 -std=c++20 -O3
Tento zápis sice spustí překlad, ale samotný nemusí stačit. Hodí se jen na malé projekty, které se vejdou do jednoho adresáře. Předpokládá se, že všechny soubory se nachází v adresáři src/cairn , případně v podadresářích. Pro rozsáhlejší projekty je třeba pomoci speciálním souborem.
Soubor by se měl minimálně nacházet v adresáři počátečního souboru (takže src/cairn/modules.yaml), případně lze explicitně jeden uvést parameterem -f. Soubor by měl obsahovat seznam všech souborů, které tvoří moduly, a případně nápovědu na další cesty, kde hledat další moduly. Pokud chybí, vezme cairn celý adresář a bude také v případě potřeby prohledávat podadresáře. Ale definiční soubor je lepší (lze snadno detekovat změny v něm)
.Příklad souboru modules.yaml ---- files - main.cpp - build_plan.cpp - build.cpp - abstract_compiler.cpp - source_def.cpp - ... - ... další soubory - ... prefixes: cairn.utils: utils cairn.compiler: compilers
Jak je vidět, není to nic komplikovaného, prostě se uvede seznam souborů, které obsahují moduly a které se účastní sestavení. V sekci prefixes se pak uvádí, kde má cairn hledat další moduly, podle prefixu, takže například cairn.utils najde ve složce ./utils. A i tam bude hledat modules.yaml. Celkový formát vypadá takto:
src, default je aktuální adresářZaměřme se nyní na pár detailů z implementace, které asi nejvíc vystihují celé obtíže s orchestrací modulů. A není to kupodivu exekuce build procesu, i když i o tom by se dalo hovořit dlouze, třeba jaké cesty zvolili jednotlivé překladače k řešení tak jednoduchého úkolu jako je reference všech potřebných BMI pro překlad jednoho souboru. Zaměřme se na discovery a hlavně na scanning
Scanning je process, při které se cairn seznamuje s typem souboru (v modulovém pojetí). Navek jsou to všechno textové soubory, ale způsob, jak jsou zapsané pak určuje jejich význam. Pokud je v souboru uvedeno:
.o .o a musí se přiložit linkeru při finálním sestavení. Může existovat víc souborů implementující modul A. Je třeba je všechny najít, chybějící implementace pak vede na chyby linkeruBMI a .o .o. Nemusí jít o vyloženě o systémový header, postačí, když je header v cestách na headery (-I)Chytří lidé dali hlavy dohromady a přišly s myšlenkou, že by bylo vhodné, kdyby překladače uměly prohlédnout soubor a rozhodnout, o jaký typ modulu jde, co exportuje a importuje. A k tomuto účelu navrhli i formát popsaný v proposalu P1689r5. Když si ale pořádně pročtete, jaké informace vám tento formát poskytne, zjistíte, že to je naprosto nedostatečně. Ze všech údajů, které lze z formátu vyextrahovat máme k dispozici jen requires a provides.
Například, toto je výstup pro soubor scanner.cpp mého projektu ve formě P1689r5 za použití `clang-scan-deps
{
"primary-output": "build/obj/scanner_f4ce731b09f65819.o",
"provides": [
{
"is-interface": true,
"logical-name": "cairn.source_scanner",
"source-path": "src/cairn/scanner.cpp"
}
],
"requires": [
{
"logical-name": "cairn.module_type",
"source-path": "src/cairn/module_type.cpp"
}
]
},
A takto vypadá výstup z nástroje cairn
$ cairn --scan src/cairn/scanner.cpp clang++-18 --- module_name: cairn.source_scanner type: interface exports: [] imports: - type: interface name: cairn.module_type - type: system_header name: algorithm - type: system_header name: string - type: system_header name: vector - type: system_header name: filesystem
To je docela velký rozdíl ne? Nástroj cairn poskytne informaci, že se jedná o interface (generuje BMI, exportuje sebe), neexportuje nic (to jsou reexporty), a importuje jiný modul a další 4 header modules.
Oproti tomu se z P1689 dozvíme, že modul poskytuje nějaký modul (tady vidímě, že jde patrně o export module), přičemž „is_interface“ říká, že to bude jeho vlastní interface. Dozvíme se i, že importuje jiný modul, ale nic o headerech. Je fakt, že se dozvíme i jména patřičných souborů, kde lze moduly hledat, ale k tomu je třeba poznámku – toto clang poskytne až po tom, co máme všechny soubory pohromadě v jeho compile databazi. Pokud tam nejsou jména souborů tam nebudou. Oproti tomu cairn používá scan před discovery fáze, kdy se budou moduly teprve hledat.
Ještě jeden příklad
{
"primary-output": "build/obj/log_9a8dee51f130cf79.o",
"requires": [
{
"logical-name": "cairn.utils.log",
"source-path": "src/cairn/utils/log.ifc.cpp"
}
]
},
== vs ==
module_name: cairn.utils.log
type: implementation
exports: []
imports:
- type: interface
name: cairn.utils.log
- type: system_header
name: iostream
- type: system_header
name: mutex
- type: system_header
name: functional
- type: system_header
name: vector
Výstup z clanga je naprosto tragický, dozvídáme se, že se jedná o nějaký soubor, který požaduje cairn.urils.log. V případě cairn získáme informaci
cairn.utils.log (module cairn.utils.log).Možná teď někoho napadlo, jak cairn vlastně získává metadata o vlastních souborech. A odpoveď asi někoho překvapí
Cairn si sám parsuje C++ zdrojový kód
Opravdu, nekecám! Ale nejde o plnohodnotný parsing, jen o jednoduchou tokenizaci. Předně se odstraní všechny komentáře. Následně se musí vyhodnotit #define konstanty i případně #include, které se mohou objevit v globální sekci. Ty se prochází jen pro hledání definicí konstant aby se nakonec mohlo vyhodnotit příkazy pro podmíněný překlad, tedy #if případně #ifdef. A teprve pak se hledají CPP souboru klíčová slova module a import na základní úrovni.
Protože – v samotné hlavičce modulu by se neměly používat makra. Nesmíte napsat
#define MODULE_NAME mojemodule expor module MODULE_NAME; //chyba #define EXPORT export #define IMPORT import #define BUILD_MODULE_NAME(f) module.f EXPORT IMPORT BUILD_MODULE_NAME(foo); //chyba
Žádný výše uvedená preprocesorová onanie není povolena, takže není třeba provádět plnohodnotný preprocesing. Co ale povolené je, tak to podmíněný překlad. Takže následující příklad bude fungovat
export module cairn.utils.process; #ifdef _WIN32 export import :win; #else export import :posix; #endif
Skenovací systém v nástroji cairn by měl brát v úvahu právě tyto situace ale nic navíc, makra se tedy neexpandují a hledají se čistě export import a module definice. Výsledek skenování je uložen interně do struktury a lze je případně potřeby exportovat ve formátu YAML
Prohledávání potřebných modulů probíhá do šířky. Začíná se souborem přiřazený k danému cíli a prochází se závislosti. Nalezené moduly jsou spojeny se známými zdrojovými soubory. Pokud není mapování nalezeno, rozšíří se hledání podle prefixů v modules.yaml, Do interní databáze se tak přidávají další lokace, které dál mohou prohledávat. Prohledávání se zastaví, jakmile namapovány všechny požadované moduly.
Samotný build pouze vybírá soubory, které se přímo a nepřímo dotýkají daného cíle. I když je nalezeno víc souborů, k překladu se použijí jen ty co jsou potřeba.
Nástroj cairn operuje nad pracovní složkou ve které udržuje databázi, ale i cache modulů. Databáze je tedy primárně spravována v paměti a na disk se pouze ukládá její serializovaná kopie (v binární formě). V podadresářích jsou pak realizovány cache přeložených BMI nebo objektových souborů .o. Na rozdíl třeba od CMake, tyto cache bývají bez vnitřní struktury, , pouze jeden velký adresář plný souborů.
Tento adresář se primárně jmenuje .build ale parametrem -B lze nastavit jiný název nebo cestu
Následuje pár postřehů z praktického používání. Na začátek je třeba dodat, že mám k dispozici projekt, který je napsaný plně modulárně: Je to samotný cairn. Jinými slovy, cairn potřebuje sám sebe, aby se přeložil.
To že výsledkem pokusů s moduly je program napsaný pomocí modulů, který lze přeložit a spustit ukazuje, že používat se to dá. Ale jak dobře?
Kolik IDE dneska umí moduly? Budou si s moduly vůbec rozumět? Pochopí syntaxi, správně jí obarví, správně budou napovídat? Já vím, že se mezi čtenáři najde spoustu programátorů, kterým k práci stačí vim a základní obarvení klíčových slov. Mně bohužel ne, už jsem si příliš zvykl na asistenty. I když ty s umělou inteligencí tedy zatím používám co nejméně (prodražuje to vývoj), tak i přesto zde nastává regulérní otázka, zda se v module vyzná i umělá inteligence, například copilot.
Já osobně používám dvě IDE. Dlouhodobě píšu v Eclipse CDT, poslední dobou také používám Visual Studio Code. Na úvod je třeba říct, že jsem byl „očekávaně“ zklamán, protože drtivá většina IDE neví která bije. Dokonce poslední Visual Studio 2026 má s moduly problémy.
IDE zpravidla hned podtrhnou klíčové slovo module nebo export, protože jej neznají. Identifikace typů, metod, funkcí, o tom už ani nemluvím. Naštěstí existují workaroundy
Nejprve Eclipse. To lze naštěstí provozovat pouze v režimu „obarvení syntaxe“ bez analýzy kódu, takže je z toho klasický textový editor, co umí poznat základní klíčová slova. Content Assist (napovídač) ale bude nejspíš napovídat genericky a na základě okolního textu. Velmi podobně funguje Microsoft IntelliSense, naštěstí aspoň klíčové slovo module zná.
Výrazně lepší je situace, pokud nasadíme clangd. Ten jsem byl schopen nasadit hlavně v linuxu. Ve windows mi to „nějak nefungovalo“, ale je možné, že jsem neměl poslední verzi. Osobně jsem zkoušel clangd-18, zejména proto, že verzi 18 považuji za minimum pro překlady modulů v clang++. Je třeba dodat, že kód přeložený clang-20 neumí clangd-18 načíst, zřejmě nerozumí svým pcm souborům z novější verze.
Nástroj clangd lze integrovat do Eclipse a Visual Studio Code jako extension. Pokud je mu předložen správně zkonstruovaný compile_commands.json je schopen poznat nejen moduly, ale propojit všechny exporty a správně napovědět v každém místě programu.
Generovat soubor compile_commands.json nástroj cairn samozřejmě zvládne přes přepínač -c (například -cbuild/compile_commands.json). Je třeba říct, že k vyplnění všech polí používá skutečné příkazové sekvence z překladu, a proto je dobré projekt překládat v clang++ odpovídající verze. Pokud projekt přeložíte v gcc, pak si nebude vědět rady.
Neznámým prostředím je zde Windows. Příkazy pro překlad v Windows používají cl.exe s jeho specifickou command line, a je otázkou, jak si s tím clangd poradí. Jak píšu výše, mně to nefungovalo, ve windows jsem tedy zůstal u generického IntelliSense
Tohle je asi otázka za zlatého bludišťáka. Na výběr jsou v zásadě tři základní typy: interface, implementation a partition (z toho ještě exportni a neexportní partition).
Pokud jde o drobné moduly nejspíš bude stačit vše psát do jednoho souboru. Třídy dokonce nemusí metody deklarovat 2×, stačí tělo metody vložit přímo do deklarace třídy. Symboly, které chceme exportovat se musí označit klíčovým slovem export. Pokud není symbol označen, nebude v importovaném kódu viditelný.
U větších modulů bych možná volil rozdělení mezi interface a implementation. Vzniknou tak dva soubory, a pracuje se s tím stejně jako u vztahu header-source. Já osobně označuju interface část jako .ifc.cpp. Někde se setkáte s označením .ixx. (Může to být trochu matoucí, protože drtivá část mých modulů jsou interface, měl bych to plné .ixx). Rozdělení do dvou souborů umožňuje i rozdělit překlad, implementation část se pak překládá paralelně s ostatními moduly, které používají stejný interface. Implementation část není v závislostech. Proto u velkých modulů, kde implementace má velké množství kódu, je dobré ji překládat zvlášť.
Implementační část také může sloužit ke skrytí implementačních detailů, stejně jako doposud, pouze z těch důvodů skrývání zmizel důvod „nezaplevelování namespace“, které moduly řeší právě přes export.
Implemenračních částí může být i víc, tedy mohu mít víc souborů s implementací k jednomu interface (interface může být jen jeden soubor). Tady je třeba si dát pozor na správné discovery. Nástroj cairn musí být schopen všechny implementace odhalit. Typicky stačí je mít ve stejném adresáři, nebo je správně zmínit v modules.yaml, kde by měly být pospolu s interface
Poslední částí jsou partitions. Přiznám se, že ještě nemám představu, jak přesně je použít. Zatím je používám k platformově závislého kódu, kdy si podle platformy vyberu příslušnou partition
#ifdef _WIN32 import :win; #else import :posix; #endif
K tomu je třeba dodat, že cairn neumožňuje podmíněný discovery nad soubory v modules.yaml a tak je schopen objevit a zařadit všechny implementace a partitions které najde. Avšak díky tomu, že partition je třeba importovat, nedochází tady k automatickému „přiložení“ všech implementací, ale jen těch, které se skutečně importují. Takže v tomto případě se program na windows přeloží s win partition, jinak se použije posix partition.
Osobně netuším :)
Tento příkaz zajistí, že jedno module exportuje obsah celého jiného module – tedy jeho veřejnou část samozřejmě. Z hlediska jazyka to řeší pouze viditelnost. Modul, který reexportuje jiný modul je ekvivalentní situaci, kdy by reexportovaný modul byl prostě vložen do veřejné části modulu, který jej reexportuje.
Z hlediska buildu pak znamená, že cairn musí všude tam, kde se je reference na module, které reexportuje, přiložit i všechny reexportované moduly – tranzitivně.
Dává mi smysl reexportovat vybranou exportní partition nebo submodule, tak aby kód, který jej importuje se nemusel potýkat s vnitřní strukturou importovaného modulu.
Klíčové slovo export se píše před template, ne před class
//špatně template<typename T> export class Foo; //správně export template<typename T> class Foo;
Hodně mě překvapilo, že částečné specializace šablony nejde provádět v jiných modulech.
export module A;
export template<typename Foo> class Bar;
export module B
import A;
template<>
class Bar<std::string> {//chyba, B nedefinuje Bar
...
...
};
Pokud je váš kód postaven na specializaci centrální šablony, nebo specializujete třeba std::format, tak to budete muset přepsat.
Pokud vás zajímá mé řešení, tak se podívejte na implementaci cairn.utils.log, kde používám std::format na zápis patternu pro logování a kde ho rozšiřuju o podporuj invokable funkcí.
Jiné řešení najdete v cairn.utils.serializer a cairn.utils.serializer.rules. Serializer provádí jednoduchou binární serializaci na základě pravidel, a ty najdete v tom druhém modulu. A protože je to jiný modul, nedá se použít částečná specializace třídy představující pravidla. Proto se to řeší přes koncepty, jako například is_linear_container (vector, string) či is_assoc_container (mapy)
S importem hlavičkových soubory si člověk užije legrace. Zřejmě za to může vágní definice ve standardu, nebo co já vím, ale všechny překladače trpí nějakými problémy a člověk občas narazí na nutnost organizovat projekt podle překladače
Pokud jednu hlavičku vložíte přes #include a jinou přes import, mohou mít tyto hlavičky společnou část, která je najednou v kolizi. Překladače gcc a clang si budou stěžovat na tuto kolizi, zvlášť pokud kód v hlavičce v různých situacích vede na různá AST. Pak máme k jednomu symbolu několik duplicitních AST a překladač vyhlásí dost nečitelnou chybu, případně spadne s ICE (gcc). U Microsoft jsem zase narazil na konflikt importuju header only knihovny sdílející kusy STL.
Následující příklad to pěkně demonstruje
module; #ifdef _WIN32 #include <fkyaml.hpp> #endif module cairn.module_resolver; #ifndef _WIN32 import <fkyaml.hpp>; #endif
Pokud importuju knihovnu fkyaml.hpp, překlad v msvc skončí touto chybou
module_resolver_2d411b6e0cf5dfe1.obj : error LNK2019: Nerozpoznaný externí symbol "class fkyaml::v0_4_2::detail::basic_str_view<char,struct std::char_traits<char> > const fkyaml::v0_4_2::detail:: default_secondary_handle_prefix"
Pokud naopak nechám verzi s #include přeložit v clang, bude si clang stěžovat na duplicitní definici .
/usr/bin/include/c++/14/bits/vector.tcc:534:8:error: '_Guard_elts' has different definitions in different modules; first difference is defined here found constructor with no body
Což mně naprosto nezajímá, nechť si to překladač nějak vyřeší, je to přece standardní knihovna.
Microsoft ale obecně má problém s linkerem. Výmluvně vypadá i tato definice
#ifdef _MSC_VER module; #include <filesystem> //msvc requires #endif export module cairn.abstract_compiler;
Pokud filesystem chybí jako include, msvc ohlásí chybu
preprocess_2d411b6e0cf5dfe1.obj : error LNK2001: Nerozpoznaný externí symbol "public: static wchar_t const std::filesystem::path::preferred_separator" (?preferred_separator@path@filesystem@std@@2_WB main_2d411b6e0cf5dfe1.obj : error LNK2001: Nerozpoznaný externí symbol "public: static struct std::strong_ordering const std::strong_ordering::less" (?less@strong_ordering@std@@2U12@B)
Zdá se, že Microsoft má problém s korektním exportem BMI z hlaviček a chybí v nich symboly, které běžně patří do OBJ souboru. Problém je, že hlavičky negenerují OBJ soubor
A ještě jedna stížnost na Microsoft a ta se týká transitivními závislostmi. Norma C++ řeší pouze import hlavičky, třeba import <filesystem>, čímž je zahrnuto vše, co tato hlavička obsahuje, včetně třeba <string>. Microsoft z neznámých důvodů bude vyžadovat BMI od <string>, přestože všechny data by měly být k dispozici BMI od <filesystem>, Jinak by to totiž vedlo k tomu, že by si build systém musel poctivě evidovat všechny vnitřní závislosti STL knihovny.
Bohužel msvc to vyžaduje, navíc vyžaduje přikládat BMI hlaviček importovaných modulů, přestože modul, který se právě překládá, žádnou z hlaviček nevyužívá ani přímo ani nepřímo.
Nástroj cairn to řeší tak, že provádí tranzitivní uzávěr všech závislostí a přikládá tak všechna BMI hlaviček, které v tomto uzávěru uvízly. Tímto mi drtivá většina problémů prošla a tam kde to nestačilo, tam jsem holt dané hlavičky uvedl do import abych si závislost (a přiložení) vynutil. Toto je specialita Windows sestavení, clang ani gcc tím netrpí. Naštěstí uvedení zbytečných importů není chybou.
Překladače se bohužel ani neshodnout na tom, které moduly je třeba naimportovat aby se uspokojili všechny závislosti vyplývající z kódu. Pokud programátor zvolí jednoduchou strategii která by se dala stručně popsat slovy „doplním importy podle toho, na co si překladač bude stěžovat“ může být bohužel cesta k vlastní záhubě, kdy se programátor stane vazalem překladače a bude bezhlavě doplňovat importy do míst, kde to nedává smysl. Navíc v některých situacích překladače nepomohou. Místo chyby „neznám symbol“ vyskočí chyba „symbol v STL se odkazuje na neúplně definovanou šablonu někde v STL“. Po hlubší analýze člověk zjistí, že to je std::vector s typem, který jsem zapomněl naimportovat
Je zajímavý, že při překládání projektu fungující v msvc jsem narazil na spoustu chybějící importů v clang. Něco bylo oprávněné, něco bylo „na hraně“.
----------
export module A;
export struct Foo {...}
----------
export module B;
import A;
export Foo bar();
----------
module main;
import B;
void test() {
auto x = bar(); //musim importovat A?
}
Zeptal jsem se umělé inteligence a neshodnou se. Podle jednoho (grok) zápis export Foo bar() exportuje nejen funkci bar, ale i typ Foo, tím se stane Foo viditelný v modulu main. Podle druhého (chatgpt) naopak vyžaduje export import A v modulu B.
Prakticky následující export vyžaduje reexport celého modulu <format>, kvůli jedné deklaraci v šabloně – proč mi přijde, že pak moduly jaksi ztrácí smysl?
export struct Log {
//...
template<typename ... Args>
static void debug(std::format_string<Convert<Args>...> fmt, Args && ... args) {
output(Level::debug, fmt.get(), std::forward<Args>(args)...);
}
};
Je to tím, že std::format_string je součástí exportované funkce, ale patří do <format>. Bez importu tohoto modulu se třída Log nedá používat.
A z toho možná pramení nesoulad a to vede k chaosu v importech
Překladač gcc v tomto souboji hraje roli podivného tvora. Začalo to jeho module mapperem (p1184), který připomíná jadernou elektrárnu, vyžaduje instalaci serveru, za použití socketů nebo rour. Naštěstí module mapper lze použít i ve formě manifest souboru (a takhle to používá i cairn)
Pokud není použit module mapper, vytváří gcc modulovou cache v adresáři .gcm-cache. To je naprosto úžasný nápad, proč to nemají jiní? Úplně bych se vyhnul nutnosti referencovat BMI souborů, a jen bych zajistil správné pořadí! Kdyby existoval způsob, jak překladači vnutit vlastní adresářovou cestu . Jenže to jednoduše nejde. Hold někoho napadlo, že tak, jak je to teď, je to lepší!
GCC je zatím nejvíc ICE překladač. Některé běžné konstrukce nedává a padá na nich. Tuhle jsem řešil chybu ICE při překladu nlohamm jsonu, ten code review fakt nebudí důvěru. Moduly jsou postaveny výhradně nad binárním interface, které je v rukou některých programátorů velice křehké a stačí malá nesrovnalost, tu ulítne o bajt a celý se to sesype.
Tahle chyba v GCC mluví za vše. Ukazuje jaký zmatek má gcc v importovaných headerech
reference to ‘int64_t’ is ambiguous stdint-intn.h(27, 19): candidates are: ‘typedef __int64_t int64_t’ stdint-intn.h(27, 19): ‘typedef __int64_t int64_t’
(vidí header 2×, jednou jako include a jednou jako import. Jak se to stane? No je includován na více místech)
Tento nástroj najdete v mém github repositáři. Možná někoho napadne, jak nástroj přeložit, když potřebuje sám sebe, aby se přeložil?
U verze pro Windows je k dispozici compile.bat, Ten stačí spustit v rámci Native Tools Developer Command Prompt for Visual Studio Community. Výsledkem by měl být cairn.exe v adresáří .install
U Linuxu je k dispozici Makefile. Tento soubor je na míru sestaven pro překladač clang++ aspoň verze 18.
$ make all $ sudo make install
Pro verzi gcc zatím makefile nechystám. Ona se poslední verze cairn nedá v gcc přeložit (ani ve verzi 15).
Samotný skript se nedá použít k ničemu jinému, než k počáteční instalaci, je totiž sestaven na míru pro exekuci překladu. Neumí poznat nové závislosti, neumí poznat změny v souborech, každé spuštění udělá nové kompletní sestavení.
cairn skutečně provádíPokud chcete vidět podrobnější činnost nástroje, použijte přepínač -d (debug)
modules.db z pracovního adresáře, pokud existujemodules.db Na začátku byla výzva překonat počáteční vstupní problémy a dostat se dál, pomoci ostatním programátorům. Na konci je „nějak“ fungující nástroj a taková hořká příchuť v puse. Ale bez zkušenosti bych nemohl soudit
Co se mi líbí
npm, tak aby člověk pouze zadal název modulu a on by se mu stáhnul do složky „modules“Co se mi nelíbí
Jakou příponu používat pro moduly?
.cpp, protože všechno je modul.cppm pro moduly .ixx pro interface
Intenzivně se zabývám programováním zejména v jazyce C++. Vyvíjím vlastní knihovny, vzory, techniky, používám šablony, to vše proto, aby se mi usnadnil život při návrhu aplikací. Pracoval jsem jako programátor ve společnosti Seznam.cz. Nyní jsem se usadil v jednom startupu, kde vyvíjím serverové komponenty a informační systémy v C++
Přečteno 57 683×
Přečteno 27 744×
Přečteno 26 411×
Přečteno 24 375×
Přečteno 22 877×