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.

An icon indicating this blurb contains a warning

Neither GCC 15 nor Clang 22 fully agree on this feature yet. GCC has not implemented private module fragments at all — module :private; fails outright with sorry, unimplemented. Clang has implemented it, and implements it correctly enough to reject the naive first attempt below. Sphynx is gated to Clang only in this project’s CMake build for exactly that reason.

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 )