Macros and the Module Boundary (Norwegian Forest)
The Module Deep Dive chapter listed macros first among what cannot be exported from a module — they belong to the preprocessor, not the language, and a module’s interface only ever describes real C++ entities. This chapter shows exactly what that means in practice, since it’s an easy assumption to carry over from headers, where a macro always leaks into every translation unit that includes them.
Module NorwegianForest
1 module;
2
3 #include <print>
4
5 // A macro defined in the global module fragment never becomes part of the
6 // module's interface -- macros are a preprocessor-only concept, and a
7 // module only ever exports real C++ entities (types, functions, objects).
8 // Contrast this with #include, where a macro defined in a header always
9 // leaks into every translation unit that includes it.
10 #define PURR_SOUND "Mjau"
11
12 export module NorwegianForest;
13
14 export auto meow() -> void {
15 std::println("Norsk skogkatt sier «{}».", PURR_SOUND);
16 }
PURR_SOUND (line 10) is defined in the global module fragment, before export module (line 12). It’s used freely inside meow()’s body (line 15) — nothing stops a macro from being used within the module that defines it. What changes is what an importer can see.
User Code: main.cpp
1 import NorwegianForest;
2
3 // PURR_SOUND is not visible here -- uncomment the next two lines to see it
4 // fail to compile:
5 // #include <print>
6 // auto try_leak() -> void { std::println(PURR_SOUND); } // error: 'PURR_SOUND' was not declared in this scope
7
8 auto main() -> int {
9 meow();
10
11 return 0;
12 }
import NorwegianForest; (line 1) brings in meow(), but PURR_SOUND simply doesn’t exist here. Uncomment the two commented-out lines (lines 5-6) and the build fails with an undeclared-identifier error — not a linker error, not a warning, a hard compile failure, because as far as the preprocessor in main.cpp’s translation unit is concerned, PURR_SOUND was never defined at all.
This is a genuine, sharp difference from #include. A header that does #define PURR_SOUND "Mjau" makes that macro available to every file that includes it, transitively, whether or not the file wanted it — one of the classic sources of header pollution and naming collisions between unrelated libraries. Modules close that door entirely: the preprocessor state of one translation unit is invisible to another, module or not, with the sole exception of macros defined via -D on the command line or genuinely shared header files textually included by both sides.
![]() |
This also means macro-based configuration idioms — feature-flag macros, |
