Converting a C++ Application to Modules
(This blog posting is also available as a PDF document)
We have converted the C++ sources of our Cadifra UML Editor from using header files to C++ 20 modules.
The sources are organized into ~40 packages. Each package uses a C++ namespace with the same name as the package. In the drawing, you can see the most important packages with their dependencies. The picture was drawn using our UML Editor. Some less important packages have been omitted.

We had roughly one major class per *.h/*.cpp pair. We used forward declarations for
classes to minimize dependencies between packages, following the guideline
by Herb Sutter:
Guideline: Never #include a header when a forward declaration will suffice.
First attempt
In a first naive attempt, I converted nearly every header file to an interface module (.ixx),
with the implementation module in the .cpp file.
Then I had a problem with the forward declarations. For example, for the interface
View.IShiftControl:
export module View.IShiftControl;
import Base.Forward;
import Core.Forward;
import d1.Point;
import d1.Shared;
namespace View
{
export class IShiftControl: public d1::Shared
{
Core::IElement& itsElement;
public:
IShiftControl(Core::IElement& m):
itsElement{ m }
{
}
auto Element() const -> Core::IElement& { return itsElement; }
virtual void Shift(Core::Env&, const Base::ShiftVector&,
const d1::fPoint& mouse_pos) = 0;
virtual void Finalize(Core::Env&) = 0;
IShiftControl(const IShiftControl&) = delete;
IShiftControl& operator=(const IShiftControl&) = delete;
};
}
I imported Core.Forward, which contains forward declarations for all classes in
the package Core:
export module Core.Forward;
export namespace Core
{
class CopyRegistry;
class ElementSet;
class Env;
class ExtendSelectionParam;
class IClub;
class IDiagram;
class IDirtyMarker;
class IDirtyStateObserver;
...
}
The problem with this is, that according to the C++ 20 language specification, a name, which is declared in a module, is attached to that module and must thus be defined in that same module.
So, this didn’t work. But there is a – partial – solution for this.
Module partitions
I changed the line
export module Core.Forward;
to
export module Core:Forward;
thus replacing the dot (.) in the middle with a colon (:).
This now defines partition Forward of module Core.
So now, we have a bigger module named Core, which is separated (partitioned) into
a number of partitions.
Partitions are just a means for splitting the source files of an interface module (Core).
The same applies to, for example, View.IShiftControl, which must be changed to
View:IShiftControl accordingly.
The fun part now is, that all declarations in every partition of Core are attached
to module Core, not to a partition module.
Which in turn means, we can forward declare classes inside Core.
To glue the partitions together, I created a file Core/Module.ixx, which contains:
export module Core;
export import :Contains;
export import :CopyRegistry;
export import :Elements;
export import :ElementSet;
export import :Env;
export import :Exceptions;
export import :ExtendSelectionParam;
export import :Finalizer;
export import :FollowUpJob;
export import :Forward;
export import :IClub;
export import :IDiagram;
export import :IDirtyMarker;
export import :IDirtyStateObserver;
export import :IDocumentChangeObserver;
export import :IElement;
export import :IElementPtr;
export import :IFilter;
export import :IGrid;
...
Then, wherever something from Core is needed somewhere, we have to
import Core;
Note that if a class outside of Core is used by reference (or a pointer),
we now have to import Core as well, since we cannot forward declare a class
in a module, which is defined in a different module. We also cannot import
Core:Forward outside of Core.
Partitions can only be used inside a module anyway and may only be exported by
the primary interface of the module (Core/Module.ixx in our case).
Inside module Core, we can import the Forward partition with
import :Forward;
which imports the forward declarations of the classes of module Core into the
current partition.
Final remark
For the conversion to modules, no refactorings of our design were needed. The classes were ready for the conversion.
(last edited 2025-09-29)