C++ Module 17 – Modules and coroutines
Migrating a C++ project to modules
Move a header-based C++ project to modules in four steps that keep it building: wrap, switch consumers, separate macros, then attach.
What you will learn
- Plan a gradual migration from headers to modules in four steps
- Wrap an existing header in a module with export using declarations
- Combine #include and import of one library in the same program
- Handle macros and configuration once consumers switch to import
- Recognise when moving code into a module breaks remaining header users
Before you start
What you’ll learn · Before you start
What you will learn
- Plan a gradual migration from headers to modules in four steps
- Wrap an existing header in a module with export using declarations
- Combine #include and import of one library in the same program
- Handle macros and configuration once consumers switch to import
- Recognise when moving code into a module breaks remaining header users
Before you start
On this page
A project moves to modules safely in four steps, each of which leaves it building. Wrap every library first: a
module file that includes the library’s existing header in its global module fragment and re-exports its names with
export using declaration: A declaration such as export using textutil::trim; in a module's purview. It exports a name declared elsewhere, for example in a header of the global module fragment, without declaring anything new., so no code moves and #include and import
keep working side by side. Switch consumers from #include to import one file at a time. Separate the
macros, because no import ever carries one: configuration macros move into a small header of their own, and values
that importers need become exported constants.
Attach last: move the declarations into the module and the code into implementation units. attachment (modules): The module a declaration belongs to: the named module in whose purview it is declared, or the global module for the declarations of ordinary files and global module fragments. Attaching a library's declarations to its module changes the names the linker sees. them to the module changes the names the linker sees, so it waits until no file includes the old header. Until then, the header stays as a fallback for every consumer that cannot import yet.
Four steps, because modules are not ready everywhere at once
A conversion in one go has to change every file, every build script and every consumer on the same day, and modules are not equally ready everywhere. GCC 16 still calls its support experimental, Apple’s clang needs an extra flag and cannot be used by CMake for modules at all, CMake supports no header units and no Makefile generator, and editors need their own module support. Users of a library may build with an older compiler, or with a build system that cannot scan imports, and they would be locked out. A gradual migration keeps one rule: after each step the whole project still builds and its tests pass, so a problem shows up in the step that caused it.
A gradual migration from headers to modules, in four steps
Text description of the diagram
The diagram shows the order of a gradual migration of the textutil library, from top to bottom.
Before the migration every file includes textutil.h.
- Wrap: add textutil.cppm, a module whose global module fragment includes textutil.h and which re-exports its functions with export using declarations. No code changes, so files that include the header and files that import the module can live in one program.
- Switch consumers: change files one at a time from #include to import. The header stays, as a fallback for files and projects that cannot import yet.
- Separate the macros: importers never see macros, so configuration macros move to a small header of their own, textutil_config.h, and values that importers need become exported constants.
- Attach: move the declarations into the module and the code into implementation units. Their linkage names change, so this step comes last, once no file includes the header any more.
Each step changes one thing and keeps the rest working, as the diagram shows from top to bottom:
| Step | What changes | What keeps working | What can go wrong |
|---|---|---|---|
| 1. Wrap | One new module file per library | Every #include |
Almost nothing |
| 2. Switch consumers | One file at a time | The header, for everyone else | A macro the file used |
| 3. Separate the macros | A config header, exported constants | Macros in #if, through the config header |
Values that only a macro carried |
| 4. Attach | Declarations and code move into the module | Imports only | Any file that still includes the header |
Steps 1 to 3 never change the code of the library, only how its consumers reach it. Step 4 is the only one that changes what the library compiles to.
Checkpoint
Check: Four steps that each keep the build working
Step 1 changes no code: a wrapper module beside the header
textutil is a small string library with a classic header. The wrapper is the whole of step 1:
C header · wrapped/textutil/textutil.h
// textutil: small string helpers, as a classic header library.
#pragma once
#include <string>
#include <string_view>
#include <vector>
#include "textutil_config.h"
namespace textutil {
std::string trim(std::string_view text);
std::vector<std::string> split(std::string_view text, char separator);
std::string upper(std::string_view text);
} // namespace textutil CPPM · wrapped/textutil/textutil.cppm
// Step 1: a module that wraps the existing header. No code moves: importers and includers reach the same functions.
module;
#include "textutil.h"
export module textutil;
export namespace textutil {
using textutil::split;
using textutil::trim;
using textutil::upper;
inline constexpr int version = TEXTUTIL_VERSION; // the macro's value, under a name that can be exported
} // namespace textutil C++ · wrapped/app/main.cpp
// Step 2: this file imports textutil, while report.cpp still includes textutil.h.
#include <print>
#include "../report/report.h"
import textutil;
int main() {
std::println("textutil version {}", textutil::version);
std::print("{}", report::table(" asha: 91, ravi :64 , meera:78 "));
std::println("[{}]", textutil::trim(" padded "));
} The global module fragment, between module; and export module textutil;, includes the header, so trim, split
and upper stay exactly the functions they were, attached to the global module. Each using declaration in the
export block then exports one of those names from the module. Clang’s documentation calls this the export-using
style and lists it among the styles that keep the library’s ABI. The constant version gives importers the value of
the TEXTUTIL_VERSION macro under a name they can see. The header already keeps that macro in a small header of its
own, textutil_config.h: that split is step 3, made here in advance so that one folder serves steps 1 to 3. In the
build below, main.cpp imports textutil while report.cpp still includes textutil.h, and the two link into one
program:
from session import run
run("""
# Step 1: compile the wrapper module next to the unchanged library.
clang++ -std=c++23 -fcxx-modules -c wrapped/textutil/textutil.cppm -fmodule-output=textutil.pcm -o textutil-module.o
clang++ -std=c++23 -c wrapped/textutil/textutil.cpp -o textutil.o
clang++ -std=c++23 -c wrapped/report/report.cpp -o report.o
# Step 2 begins: main.cpp imports textutil, report.cpp still includes textutil.h, and they link together.
clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c wrapped/app/main.cpp -o main.o
clang++ textutil-module.o textutil.o report.o main.o -o app && ./app
g++-16 -std=c++23 -fmodules wrapped/textutil/textutil.cppm wrapped/textutil/textutil.cpp wrapped/report/report.cpp wrapped/app/main.cpp -o app-gcc && ./app-gcc
""") Output
# Step 1: compile the wrapper module next to the unchanged library. $ clang++ -std=c++23 -fcxx-modules -c wrapped/textutil/textutil.cppm -fmodule-output=textutil.pcm -o textutil-module.o $ clang++ -std=c++23 -c wrapped/textutil/textutil.cpp -o textutil.o $ clang++ -std=c++23 -c wrapped/report/report.cpp -o report.o # Step 2 begins: main.cpp imports textutil, report.cpp still includes textutil.h, and they link together. $ clang++ -std=c++23 -fcxx-modules -fprebuilt-module-path=. -c wrapped/app/main.cpp -o main.o $ clang++ textutil-module.o textutil.o report.o main.o -o app && ./app textutil version 3 ASHA 91 RAVI 64 MEERA 78 [padded] $ g++-16 -std=c++23 -fmodules wrapped/textutil/textutil.cppm wrapped/textutil/textutil.cpp wrapped/report/report.cpp wrapped/app/main.cpp -o app-gcc && ./app-gcc textutil version 3 ASHA 91 RAVI 64 MEERA 78 [padded]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 wrap_session.py
Step through how the two consumers reach the same three functions:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Before step 1. Both
main.cppandreport.cppincludetextutil.h, which declarestrim,splitandupperand defines the macroTEXTUTIL_VERSION;textutil.cppholds their bodies. - A wrapper next to the header. Step 1 adds one file,
textutil.cppm. Its global module fragment includestextutil.h, so the three functions stay declared in the global module, exactly as before, andtextutil.cppis not touched. - Names exported, nothing declared. Three export using declarations export
trim,splitandupperfrom the module without declaring anything new, andversiongives importers the value ofTEXTUTIL_VERSIONunder a name they can see. - One consumer switches.
main.cppreplaces its#includewithimport textutil;and readstextutil::versionandtextutil::trimthrough the module. - The other one stays.
report.cppstill includestextutil.hand callssplitandupper: the same functions thatmain.cppreaches through the module, declared once, in the global module. - One program. The link joins
textutil.o,report.o, the wrapper's object file andmain.o. Both compilers printed the five lines the project printed before it had any module.
Both compilers print the same five lines that the project printed before it had any module, which is the test of
step 1. The library’s own textutil.cpp is untouched; the only new object file is the module’s. Link it wherever
textutil is imported: this wrapper’s object holds only an empty module initializer, but a module that defines
global objects needs its initializer to run, and importers then call it.
Key idea
A wrapper changes how consumers reach a library, never the library: its functions stay in the global module, so a file that includes the header and a file that imports the module call the same code.
Checkpoint
Check: A wrapper module beside the header
Practise: wrap the second library
report formats a table from a line of name: marks pairs and has a header of its own, report.h. Here it is, with
old_main.cpp, an old consumer that includes both headers and uses the macro:
C header · wrapped/report/report.h
// report: formats "name: marks" lines into a table; built on textutil.
#pragma once
#include <string>
#include <string_view>
namespace report {
std::string table(std::string_view csv_line);
} C++ · wrapped/app/old_main.cpp
// A consumer that cannot move yet: it keeps the headers, and the macro from textutil_config.h.
#include <print>
#include "../report/report.h"
#include "../textutil/textutil.h"
int main() {
std::println("textutil version {}", TEXTUTIL_VERSION);
std::print("{}", report::table("kiran:88"));
} Write report.cppm, a wrapper in the style of textutil.cppm, and a program that uses both libraries through
import alone, with no header of either. Keep old_main.cpp building from the same object files. The build order
matters: which module files must exist before your program compiles?
Six questions on the plan and the wrapper follow, the idea you have practised least first.
Practice that adapts to you
Practise this lesson
6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
The whole migration, with the build log of every step Pro: opens with a Pro, Premium or Ultimate pass
Every step of this migration leaves a program that builds and runs, and two of the steps break something first. This part records each build, the two breakages included, with the fix for each.
In this part:
- Step 0: the header project to start from
- The macro that stops a consumer, reproduced, and the fallback that keeps others building
- Both libraries wrapped: the exercise, worked through
- Step 4: attaching textutil, and the link failure it causes, reproduced
1 diagram · 8 runnable examples (C header, C header (report.h), C header (textutil.h), C++, C++ (macro_main.cpp), C++ (main.cpp), C++ (old_main.cpp), C++ (report.cpp), C++ (textutil.cpp), C++ (textutil_impl.cpp), CPPM, Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Wrap, extern C++ or attach: choosing with the trade-offs Pro: opens with a Pro, Premium or Ultimate pass
A library does not have to go all the way to step 4. This part compares the three ways to give a library a module interface, shows the middle one running, and checks the tools a migration depends on.
In this part:
- Three ways to give a library a module interface: the trade-offs
- Moving declarations without changing their names: extern “C++”, run
- Checking your tools against module files before the migration
- One header for both worlds, when a library must keep shipping headers
- A migration checklist
1 diagram · 3 runnable examples (C++ (textutil_extern_impl.cpp), C++ (textutil_extern_impl_wrong.cpp), CPPM, Python) · about 8 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Senior to staff
You maintain a 500-file C++ library used by other teams on GCC, Clang and MSVC, some with older compilers and plain Makefiles. You are asked to move it to modules. Plan the migration: what you ship first, what you must never break, and how you know each step worked.
-
Mid to senior
After a teammate moved a library's declarations into its module, the link fails with undefined symbols for functions that clearly exist. nm -C on the library shows textutil::split@textutil. Explain the failure and give two ways to fix it.
-
Mid to senior
Users of your library select a faster code path with -DTEXTUTIL_FAST_SPLIT, which the header's inline code tests, and they check TEXTUTIL_VERSION in #if. How do you keep both working once they import the module instead of including the header?
Key takeaways
- Migrate in four steps that each keep the project building: wrap, switch consumers, separate the macros, attach.
- A wrapper module includes the header in its global module fragment and re-exports names with
export using; the library’s code and ABI do not change. #includeandimportof a wrapped library can live in one program, which makes the header a fallback at no cost.- Imports never carry macros: keep configuration macros in a small header and export constants for values.
Terms in this lesson
- attachment (modules)
- The module a declaration belongs to: the named module in whose purview it is declared, or the global module for the declarations of ordinary files and global module fragments. Attaching a library's declarations to its module changes the names the linker sees.
- export using declaration
- A declaration such as export using textutil::trim; in a module's purview. It exports a name declared elsewhere, for example in a header of the global module fragment, without declaring anything new.
References
- Standard C++ Modules (Clang documentation): Transitioning to modules and ABI impacts (LLVM Project)
- Working Draft, Programming Languages: C++ (N4950), 10 Modules and 9.11 Linkage specifications (ISO/IEC JTC1/SC22/WG21)
- C++ Modules (Using the GNU Compiler Collection) (GNU Project)
- cmake-cxxmodules(7): C++ modules support in CMake (Kitware)
- Overview of modules in C++ (Microsoft)
- clangd features: experimental C++20 modules support (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress