Antwoord op de vraag.
Geschiedenis van de vraag
Voor C++20 was het C++-compilatiemodel afhankelijk van tekstuele inclusie via preprocessdirective. Wanneer een headerbestand werd opgenomen, kopieerde de preprocessor letterlijk de tekst van die header naar het opnemende bestand. Dit mechanisme zorgde ervoor dat macros gedefinieerd in headers lekten naar de globale namespace van elke vertaaleenheid die ze opnam, wat leidde tot subtiele bugs en naamconflicten die moeilijk te diagnosticeren waren.
Het probleem
Macro-lekken zorgden voor onderhoudsnachtmerries in grote codebases. Een macro gedefinieerd in een externe bibliotheek kon stilletjes keywords of veelvoorkomende identifier in consumenten-code herdefiniëren, wat leidde tot compilatiefouten of runtime-fouten die ongerelateerd leken aan de werkelijke oorzaak. Traditionele oplossingen zoals #undef-beveiligingen waren handmatig, foutgevoelig en schaling over complexe afhankelijkheidsgrafieken was niet mogelijk. Het fundamentele probleem was dat de preprocessor geen concept had van scope of interfacegrenzen.
De oplossing
C++20-modules introduceren een semantisch importmechanisme dat opereert op het taalniveau in plaats van op het preprocessor-niveau. Wanneer een module wordt geïmporteerd met import module_name;, verwerkt de compiler de geëxporteerde interface van de module zonder de preprocessdirective van de inbrengende vertaaleenheid uit te voeren. Macros gedefinieerd binnen de module blijven privé voor de implementatie van die module, tenzij expliciet geëxporteerd. Deze eigenschap zorgt ervoor dat macros niet lekken over grenzen van de vertaaleenheid, wat echte encapsulatie biedt en naamvervuiling voorkomt.
// mathlib.cpp (Module-implementatie) module; #define INTERNAL_CALC_FACTOR 3.14 // Privé macro, niet gelekt export module mathlib; export double compute(double x) { return x * INTERNAL_CALC_FACTOR; } // main.cpp (Consument) import mathlib; // INTERNAL_CALC_FACTOR is HIER NIET zichtbaar // #ifdef INTERNAL_CALC_FACTOR zou onwaar zijn int main() { double result = compute(10.0); // Werkt prima }
Situatie uit het leven
Een financieel handelsbedrijf onderhield een grote codebase met miljoenen regels code verspreid over honderden modules. Ze vertrouwden op een legacy-wiskundebibliotheek die macros zoals MIN en MAX definieerde in zijn openbare headers. Deze macros botsten vaak met standaardbibliotheekfuncties en derdepartij JSON-parserbibliotheken die min en max als variabelen of functiematrices gebruikten.
De eerste overweging was om alle externe headers te omringen met #pragma once-stijlbeveiligingen en handmatig problematische macros #undefen na elke inclusie. Dit vereiste dat ontwikkelaars zich herinnerden welke headers welke macros definieerden en moesten opruimen na elke opname. De benadering was kwetsbaar omdat het missen van een enkele #undef fouten kon veroorzaken in niet-verbonden delen van de codebase. Het verhoogde ook aanzienlijk de compilatietijden omdat de preprocessor dezelfde headertekst herhaaldelijk verwerkte over vertaaleenheden.
De tweede overweging was om de wiskundebibliotheek te converteren naar inline-functies en templates in plaats van macros. Hoewel dit het lekprobleem oploste, vereiste het uitgebreide wijzigingen aan de legacy-bibliotheek. De wiskundebibliotheek werd door meerdere teams gebruikt en het wijzigen ervan riskeerde bestaande berekeningen te breken die afhankelijk waren van specifieke evaluatiesemantiek van macros of neveneffecten. De refactorin inspanning werd geschat op zes maanden en werd als te riskant beschouwd voor het handelsplatform.
De gekozen oplossing was migreren naar C++20-modules. Het team convertte de wiskundebibliotheek naar een module die wiskundige functies exporteerde terwijl de macros intern voorbehouden bleven aan de module-implementatie. Door import mathlib; te gebruiken in plaats van #include <mathlib.h>, zagen consumerende vertaaleenheden de MIN- en MAX-macros niet meer. Deze aanpak vereiste minimale wijzigingen in de bibliotheekimplementatie—alleen exportverklaringen toevoegen en headers omzetten naar module-interface-eenheden. De migratie nam twee weken in beslag in plaats van zes maanden. Het resultaat was de eliminatie van macro-gerelateerde naamconflicten in de codebase en een vermindering van 15% in de compilatietijden dankzij de gecompileerde interface van de module.
Wat kandidaten vaak missen
Hoe voorkomt het gecompileerde binaire formaat van de module-interface-eenheid macro-lekken in vergelijking met tekstuele headerinclusie?
Kandidaten missen vaak dat C++20-modules gecompileerde module-interface-eenheden (CMI) produceren die binaire representaties zijn van de geëxporteerde interface van de module. In tegenstelling tot tekstuele headers die door de preprocessor worden verwerkt en macro-definities als tekst bevatten, slaan CMI's semantische informatie op over geëxporteerde functies, types en templates. De preprocessor verwerkt de inhoud van een geïmporteerde module niet; deze ziet alleen de importverklaring. Daarom zijn macros gedefinieerd in de implementatie van de module of zelfs in zijn interface-eenheid niet zichtbaar voor de importeur. Dit is fundamenteel anders dan #include, dat letterlijk tekst kopieert, inclusief #define-directieven. Dit begrijpen vereist het herkennen dat modules verschuiven van een tekstueel inclusiemodel naar een semantisch importmodel.
Waarom gedragen macros die vanuit een module worden geëxporteerd met export import zich anders dan macros van #include-directieven?
Kandidaten verwarren vaak export import van macros met regulier macrogedrag. Hoewel C++20 het exporteren van macros via export import toestaat, hebben deze macros alleen invloed op de code die de module importeert en lekken niet buiten die importscope. In tegenstelling tot #include, waar macros in de vertaaleenheid persistent blijven tot ze expliciet zijn gedefinieerd of het einde van het bestand, zijn geëxporteerde macros van modules beperkt tot de blootstelling van de importerende vertaaleenheid aan die module. Bovendien, als meerdere modules conflicterende macros exporteren, wordt het conflict gedetecteerd tijdens de importtijd in plaats van stille herdefinitiefouten later in de compilatie te veroorzaken. Dit scoping-gedrag biedt de hygiëne die tekstuele inclusie mist.
Hoe beïnvloedt de onafhankelijkheid van de module van de preprocessor de integratie van het buildsysteem en het scannen van afhankelijkheden?
Kandidaten missen vaak dat C++20-modules vereisen dat buildsystemen module-afhankelijkheden begrijpen vóór de compilatie begint, in tegenstelling tot headers waarbij afhankelijkheden tijdens de compilatie worden ontdekt. Omdat modules gecompileerde eenheden zijn in plaats van tekstbestanden, moet het buildsysteem module-interface-eenheden ontleden om te bepalen wat ze exporteren en wat ze importeren. Dit vereist een twee-fasen-buildproces: eerst het scannen van module-interface-eenheden om een afhankelijkheidsgrafiek te bouwen, vervolgens compilen in afhankelijkheidsvolgorde. De onafhankelijkheid van de preprocessor betekent dat traditionele #ifdef-beveiligingen voor headerinclusie irrelevant zijn, en op macros gebaseerde configuratie van module-interfaces is beperkt. Buildsysteem moeten gecompileerde module-artikelen volgen (BMI - Binaire Module-Interface) in plaats van alleen bronbestanden, wat fundamenteel verandert hoe afhankelijkheidstracking en incrementele builds werken.