Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: Four steps, because modules are not ready everywhere at once · Step 1 changes no code: a wrapper module beside the header · Practise: wrap the second library. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

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.

Four migration steps: wrap the header in a module, switch consumers to import, move macros out, and attach the code last.Before: headers onlyevery file has #include "textutil.h"Step 1: wraptextutil.cppm re-exports the header with export using#include and import both workStep 2: switch consumersone file at a time, from #include to importStep 3: separate the macrostextutil_config.h for macros, exported constants for valuesStep 4: attachthe module owns declarations and codeonly once no file includes the headeradd a module file, change no codethe header stays as a fallbackimporters never see macrosnames change: no header user may remain

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. Question 1 of 2 Which step of the migration is the first to change what the library itself compiles to?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Step 4, attach

    The table's own rule: steps 1 to 3 never change the code of the library, only how its consumers reach it, and step 4 is the only one that changes what the library compiles to.

  2. Question 2 of 2 Some teams that use your library build with plain Makefiles. Why does that rule out converting the library in one go?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Their builds cannot scan imports, so they could not use the library at all; with a wrapper they keep including the header

    Users of a library may build with an older compiler or with a build system that cannot scan imports. A gradual migration keeps the header working for them while the others move.

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:

The header, its wrapper module, and a consumer that imports it

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:

Step 1 and the start of step 2, with both compilers Python · wrap_session.py
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:

Interactive · step by step One library, reached through an import and through an include Step through step 1 of the migration. An arrow is an import, an include or a definition, and the chip is what travels along it.
One library, reached through an import and through an include, step 6 of 6. Showing main.cpp, report.cpp, textutil.cppm (exports trim, split, upper), textutil.h, textutil.cpp, app (same five lines). In focus: app.#includebodiesglobal module fragmentimporttextutil.omain.cppimport textutil;report.cpp#include "textutil.h"textutil.cppmexports trim, split, uppertextutil.hdeclares trim, split, uppertextutil.cppunchangedappsame five lines

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Before step 1. Both main.cpp and report.cpp include textutil.h, which declares trim, split and upper and defines the macro TEXTUTIL_VERSION; textutil.cpp holds their bodies.
  2. A wrapper next to the header. Step 1 adds one file, textutil.cppm. Its global module fragment includes textutil.h, so the three functions stay declared in the global module, exactly as before, and textutil.cpp is not touched.
  3. Names exported, nothing declared. Three export using declarations export trim, split and upper from the module without declaring anything new, and version gives importers the value of TEXTUTIL_VERSION under a name they can see.
  4. One consumer switches. main.cpp replaces its #include with import textutil; and reads textutil::version and textutil::trim through the module.
  5. The other one stays. report.cpp still includes textutil.h and calls split and upper: the same functions that main.cpp reaches through the module, declared once, in the global module.
  6. One program. The link joins textutil.o, report.o, the wrapper's object file and main.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

  1. Question 1 of 2 textutil.cppm includes textutil.h in its global module fragment and exports using textutil::trim;. To which module is trim attached?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The global module, like any declaration that comes from a global module fragment

    Declarations in a global module fragment belong to the global module, the module of ordinary files. A using declaration only makes an existing name available under the module; it declares no new entity. That is why a file that includes textutil.h and a file that imports textutil call the same function.

  2. Question 2 of 2 After step 1, main.cpp imports textutil while report.cpp still includes textutil.h. What happens when both are linked into one program?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It links and runs, because both files call the same functions with the same linkage names

    The wrapper re-exports the header's functions without moving them, so the importer and the includer both refer to the functions defined in textutil.cpp. Mixing the two ways of reaching a library is safe as long as its declarations stay attached to the global module.

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:

The header to wrap, and the old consumer that must keep building

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.

  1. Question 1 of 6 Put the four steps of a gradual migration in the order that keeps the project building after each one.

    Give each item its position, from 1 (first).

    Show the answer: Question 1 of 6

    Answer:

    1. Wrap each library in a module that re-exports its header with export using
    2. Switch consumers from
    3. Move configuration macros into a small header and export constants for values
    4. Move the declarations and code into the module, once no file includes the header

    Wrapping changes no code, so it cannot break anything. Consumers can then move one at a time while the header stays as a fallback. Macros have to be dealt with before any consumer that uses them can import. Attaching changes the names the linker sees, so it comes last.

  2. Question 2 of 6 A consumer switches from #include "textutil.h" to import textutil; and the build fails on TEXTUTIL_VERSION. Which fix keeps #if TEXTUTIL_VERSION >= 3 working in that file?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: Move the macro into textutil_config.h and include that header next to the import

    No import carries a macro, and there is no way to export one. A constant can replace a macro that is read in code, but the preprocessor cannot read a C++ constant, so a macro tested in #if must come from a header. A small config header holds just those macros.

  3. Question 3 of 6 Which conditions should hold before you move a library's declarations and code into its module?

    Choose every answer that is right.

    Show the answer: Question 3 of 6

    Answer:

    • The library's tests pass with consumers that import it
    • No file that is built still includes the library's old header
    • Every consumer's build can scan imports, for example CMake with Ninja

    Attaching gives the functions new linkage names, so a file that still includes the old header asks the linker for names that no longer exist. Every consumer must also be able to build with modules, and the tests are the proof that the earlier steps changed nothing. Macros are not a blocker: they live on in a config header.

  4. Question 4 of 6 What does export using textutil::trim; in the wrapper declare?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: Nothing new: it exports the existing trim from the module

    The export-using style re-exports names that the global module fragment brought in, which is why Clang's documentation counts it among the styles that keep the library's ABI.

  5. Question 5 of 6 Why does textutil.cppm declare inline constexpr int version = TEXTUTIL_VERSION;?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: To give importers the macro's value under a name an import can carry

    Macros never cross an import. Turning a value that importers need into an exported constant is the second half of step 3, done here already for the version.

  6. Question 6 of 6 The wrapper's object file holds only an empty module initializer. Why link it wherever textutil is imported anyway?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: Importers call the module's initializer, which a module that defines global objects needs to run

    A module unit has an initializer that importers call. Here it is empty, but the habit of linking every module's object file wherever the module is imported keeps the build right when the module grows.

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.

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

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

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.
  • #include and import of 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.