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.

An icon indicating this blurb contains information

This also means macro-based configuration idioms — feature-flag macros, X-macro tables, include-guard-style patterns — don’t cross a module boundary. If a module needs to hand a compile-time value to its importers, use a constexpr or inline variable instead; both are proper exportable C++ entities.