An Introduction to Partitions
We have converted the sources for our UML Editor to using C++ modules. The editor runs on Windows and we use the MSVC toolchain with MSBuild.
When I first read about C++ modules, I saw that modules also provide partitions, but I didn’t really understand how important they are. Partitions proved to be quite essential for the conversion.
In this blog post, I would like to show how we used partitions in the Core
package of our editor.
I’ve uploaded a partial snapshot of our sources to github,
which contains three of our packages: d1
, WinUtil
and Core
(in the
code directory)
d1
and WinUtil
are utility packages, Core
contains base abstractions. d1
contains a number
of small modules, while WinUtil
and Core
are both bigger modules divided into partitions.
Starting point
The starting point is the interface of the Core
module, which we have in the source file
Core/_Module.ixx:
export module Core;
export import :Attach;
export import :Container;
export import :Exceptions;
export import :IDiagram;
export import :IElement;
export import :Interfaces;
export import :IView;
export import :Names;
export import :Transaction;
The first line of the file starts with the keywords export module
, which indicates that this
is the interface of a module. The line ends with the name of the module (Core
).
The C++ standard uses the term “primary module interface unit”. Quote:
A module interface unit is a module unit whose module-declaration starts with export-keyword; any other module unit is a module implementation unit. A named module shall contain exactly one module interface unit with no module-partition, known as the primary module interface unit of the module; no diagnostic is required.
Then follows a list of (exported) imports. The names of the imports are all preceded by a colon,
which indicates that these are names of partitions of the Core
module (Attach
, Container
,
Exceptions
, etc). Partition names are local to the module.
The standard mandates, that all (non-internal) partitions of the module need to be export-imported in the module interface. Quote:
All module partitions of a module that are module interface units shall be directly or indirectly exported by the primary module interface unit (module.import). No diagnostic is required for a violation of these rules.
Partitions can only be imported inside other parts of the same module.
The Transaction partition
The Transaction
partition is in the
file Core/Transaction.ixx:
export module Core:Transaction;
import :IElement;
import d1.Rect;
import d1.Shared;
import std;
namespace Core
{
export class IFollowUpJob
{
...
};
...
The file starts with the keywords export module
, followed by the name of the module (Core
),
followed by a colon and the name of the partition (Transaction
).
The export
keyword at the beginning indicates, that this partition contributes to the interface
of the Core module. Exported partitions must be export-imported in the interface of the module
(file Core/_Module.ixx
).
Without the export keyword, the partition would be an internal partition, which we have used
for example in the ScreenCanvas
package (not part of the published snapshot yet):
module ScreenCanvas:Dashes;
import :DeviceContext;
import d1.Rect;
namespace ScreenCanvas::Dashes
{
// Functions that draw dashed lines which do not depend on
// the zoom factor ("Dash" and "Space" lengths in pixels).
void horizontal(
DeviceContext&,
d1::int32 startx, d1::int32 endx, d1::int32 y, // startx <= endx
const d1::Rect& redrawArea, // l <= r, t <= b
const d1::int32 Dash = 3,
const d1::int32 Space = 3);
void vertical(
DeviceContext&,
d1::int32 starty, d1::int32 endy, d1::int32 x, // starty <= endy
const d1::Rect& redrawArea, // l <= r, t <= b
const d1::int32 Dash = 3,
const d1::int32 Space = 3);
}
Internal partitions cannot export anything. The contents of internal partitions do not contribute to the interface of the module. Compiling internal partitions with the MSVC compiler requires setting a special compiler flag.
But let’s go back to the Core:Transaction
partition: It continues with an import of the sister
partition :IElement
(in
file Core/IElement.ixx).
If a partition needs definitions from other partitions, then those need to be imported.
Note that the chain of imports may not have cycles. Import cycles will be caught as
errors by the compiler.
Then follows the import of the module d1.Rect
(in
file d1/Rect.ixx).
The dot in the name is just a convention. The name denotes a module in our
d1 package. Every *.ixx
file
in the d1
directory contains a module. I’ve decided to use small modules in the d1
package,
because it turned out to be too much of a pain to have a monolithic d1
module. When I had
a single d1
module, almost everything had to be recompiled when I changed a single file in
d1
. It is normally recommended to have a bit bigger modules, which contain more than a class
definition or two, but it turned out to be useful to separate d1
into smaller bits. There was
not much of a difference when doing a full build of the project.
import std
We have used import std
for the standard library. Which made a noticeable difference for
the time needed for a full build (now ~2 minutes in total). The MSVC compiler builds the
std library on the fly, as needed.
Module names and file names
Note that the module names and partition names have no relation with the names of the files that contain them. The compiler scans the files for module and partition names. It builds a map of module or partition to file names on the fly, which happens really quickly during builds.
Module names and namespace names
C++ namespace names are orthogonal to module names, meaning these are separate things.
We used the namespace Core
for the Core
module as a convenience, but technically, it
doesn’t have to be like this. You can take whatever names you like, but it might be confusing
for the readers of the sources, if the name of the module and the name of the primary
namespace don’t match.
Incomplete types
An important aspect of partitions is, that they enable forward declarations of classes across partitions of the same module (the C++ standard uses the term “incomplete type” for forward declarations). Classes cannot be forward declared across module boundaries, but across partitions. Every name declared in a module must be defined in the same module, but it can be declared in one (or more) partition(s) and defined in a different partition of the same module (as mandated by the C++ Standard).
The C++ language differentiates between exported and non-exported forward declarations of classes. Exported classes need to use the export keyword also on forward declarations.
It would be possible to forward-declare module-internal classes across module partitions, but the MSVC compiler currently still has a bug which prevents their use.
Module implementations
Module implementations can be split into multiple *.cpp
files. All implementation files
start with the module
keyword, followed by the name of the module. Optionally, a module
implementation file may start with the character sequence module;
which marks the start
of the
global module fragment.
If an implementation file needs a good old header file, it must be included in the global
module fragment.
For example, we have the
file Core/Transaction.cpp,
which contains implementations of member functions of the Transaction
class.
Note that module implementation files do not need to import the interface of the module. Everything from the interface is implicitly imported. This can be vast for a big module.
Conclusion
I really love the isolation which modules provide. For example, we have the file d1/wintypes.ixx:
module;
#include <Windows.h>
export module d1.wintypes;
export namespace d1
{
using ::BYTE;
using ::WORD;
using ::DWORD;
using ::UINT;
using ::LONG;
using ::LRESULT;
using ::WPARAM;
using ::LPARAM;
...
}
which exports selected types from the giant Windows.h
header. If you ever have been
bitten by some horrible macro defined in Windows.h
, you will appreciate being able to
import just those types and nothing else.
(last edited 2025-10-14)