Private Module Fragment (Sphynx)
The Module Deep Dive chapter introduced module :private;: a section at the end of a primary module interface unit for implementation code that never needs to be reachable outside the module, similar in spirit to keeping code out of a header entirely. This chapter builds a working example — and shows the naturally tempting way to write it that turns out not to work, because it runs straight into a rule that has nothing to do with modules at all.
![]() |
Neither GCC 15 nor Clang 22 fully agree on this feature yet. GCC has not implemented private module fragments at all — |
A First Attempt (and Why It Fails)
The obvious way to use a private fragment looks like this: declare a class as an exported forward declaration, then define it — the whole thing, data members and all — after module :private;.
1 module;
2
3 #include <print>
4
5 export module Sphynx;
6
7 using std::println, std::print;
8
9 export class Sphynx; //
10 export auto meow() -> void; //
11
12 module :private;
13
14 class Sphynx {
15 private:
16 int value{};
17
18 public:
19 auto print() -> void {
20 meow();
21 }
22 };
23
24 auto meow() -> void {
25 println("Sphynx cat bred in Canada says 'meow, eh!'");
26 }
Paired with the obvious main.cpp:
1 import Sphynx;
2
3 auto main() -> int {
4 Sphynx sphynx;
5 sphynx.print();
6
7 return 0;
8 }
Clang rejects it:
1 error: missing '#include'; 'Sphynx' must be defined before it is used
2 4 | Sphynx sphynx;
3 | ^
4 note: definition here is not reachable
5 16 | class Sphynx {
6 | ^
This is not a compiler bug, and not Clang being unusually strict. Anything declared in a private module fragment is unreachable from every other translation unit — full stop, regardless of whether a matching name was exported earlier. export class Sphynx; only ever exported an incomplete forward declaration; the complete definition, sitting after module :private;, never becomes visible to importers. main.cpp’s Sphynx sphynx; needs a complete type at that point to allocate it on the stack — exactly the same completeness rule C++ has always applied to stack objects, modules or not. There’s no private-fragment mechanism that lets a name declared as exported-but-incomplete become complete later just because the same name shows up again after the fragment boundary.
What Actually Works
The fix splits the two concerns the naive version conflated. A class’s shape — its data members and member declarations — has to be reachable if callers are going to instantiate it by value; only the member function bodies can be hidden, the same way a header/source split keeps a class’s layout in the header and its method bodies in the .cpp file.
1 module;
2
3 #include <print>
4
5 export module Sphynx;
6
7 using std::println, std::print;
8
9 export class Sphynx {
10 private:
11 int value{};
12
13 public:
14 auto print() -> void;
15 };
16
17 export auto meow_eh() -> void;
18
19 module :private;
20
21 auto Sphynx::print() -> void {
22 meow_eh();
23 }
24
25 // meow_eh_impl is never declared outside the private fragment, so it is
26 // entirely unreachable from importers -- the actual hidden implementation.
27 auto meow_eh_impl() -> void {
28 println("Sphynx cat bred in Canada says 'meow_eh, eh!'");
29 }
30
31 auto meow_eh() -> void {
32 meow_eh_impl();
33 }
export class Sphynx { ... } (lines 9-15) is now a complete definition, exported before the private fragment — the class has one private data member and one member function declaration, auto print() -> void; (line 14), with no body. export auto meow_eh() -> void; (line 17) is likewise just a declaration. Both bodies live after module :private; (line 19): Sphynx::print()’s out-of-line definition (lines 21-23), and meow_eh()’s (lines 32-34).
The last piece is the split the chapter title promised: meow_eh() is the public entry point — it’s declared before the fragment, so importers can call it — but its body does nothing except call meow_eh_impl() (line 33), which is never declared anywhere outside the private fragment at all. meow_eh_impl() isn’t merely hidden the way meow_eh()’s body is hidden; it has no exported declaration to be hidden from in the first place. That’s the difference between “callers can call this, but can’t see how it works” (meow_eh) and “callers have no idea this exists” (meow_eh_impl) — both are legitimate uses of a private fragment, and this module demonstrates both at once.
User Code: main.cpp
1 import Sphynx;
2
3 auto main() -> int {
4 Sphynx sphynx;
5 sphynx.print();
6
7 return 0;
8 }
This is the same main.cpp as the naive attempt — the fix was entirely inside Sphynx.cppm. Sphynx sphynx; (line 4) now works because the class’s shape is complete and reachable; sphynx.print() (line 5) works because print()’s declaration is all a caller ever needs to call it.
Building With CMake
Sphynx builds the same way as Aegean — one library target from the single .cppm, linked into the executable — gated to Clang at the top level:
1 if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
2 add_subdirectory(Sphynx)
3 endif ()
1 cmake_minimum_required(VERSION 3.30.5)
2 project(Sphynx LANGUAGES CXX)
3 set(module_name sphynx)
4
5 add_executable(${PROJECT_NAME} main.cpp)
6 target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
7 target_link_libraries(${PROJECT_NAME} ${module_name})
8
9 add_library(${module_name})
10 target_compile_features(${module_name} PUBLIC cxx_std_23)
11 target_sources(${module_name}
12 PUBLIC
13 FILE_SET cxx_modules TYPE CXX_MODULES
14 FILES Sphynx.cppm
15 )
