C++20 Modules: Interface Units, Imports and Build Order with a Worked Library Split

Follow one small C++20 module from its exported interface through a hidden implementation and importing client. Trace the result and build dependency graph.

KnowledgeGate Team

Exam prep & CS education

Updated 30 Aug 20266 min read

An experienced C++ programmer knows the frustration: a header is textually pasted into every translation unit that includes it, so public declarations, private definitions, macros and include order can become entangled. C++20 modules offer a language-level boundary, but their keywords alone do not explain visibility or build order. A score.stats library has an interface, an implementation and a client; marks {72, 81, 93, 64} pass through the code and its dependency graph. The Coding & Skill Development Courses category connects C++ modules to the wider programming path.

C++20 modules versus textual headers: what actually changes

In the traditional model, #include "score_stats.h" asks the preprocessor to place the header's tokens into every including translation unit. Include guards or #pragma once stop repeated inclusion inside one translation unit, but they do not create a semantic export boundary. That is the familiar header-and-source structure covered alongside fundamentals in C Programming & Data Structures.

A module interface begins with export module score.stats;, explicitly exports selected declarations, and lets a client write import score.stats;. The compiler uses module metadata for that import instead of textually including the module source. Compilation and linking still happen.

Before adopting modules, be comfortable with declarations, definitions, translation units and linkage. The C++ Tutorial gives that learning sequence. Modules can clarify interfaces and dependencies, but the project's compiler, standard library and build system still shape a practical migration.

C++20 module units, export and import: the minimum vocabulary

Term

Exact cue

Job

Primary module interface unit

export module score.stats;

Declares what importers may use

Module implementation unit

module score.stats;

Supplies definitions without exporting the unit

Exported declaration

export int sum(...);

Becomes visible to importers after import

Importing translation unit

import score.stats;

Consumes the compiled interface

The named module is the collection of its module units, not one file. Its primary interface is unique. An implementation unit belongs to the same named module and has the interface declarations needed for its definitions. Clients import the module, not that unit.

The next layer includes partitions, which divide a module into named pieces; header units, which import suitable headers through a separate mechanism; global module fragments, which can hold preprocessing before a module declaration; and export import, which re-exports another imported interface. None is needed here. Suffixes such as .cppm, .ixx and .mpp are tool conventions, not C++ syntax.

C++20 module interface and implementation: split the exact library

Conceptually, score.stats.cppm is the primary interface. Because this example needs no legacy includes, its module declaration comes first. Only two declarations form the client-facing surface:

export module score.stats;

export int sum(const int* values, int count);
export double average(const int* values, int count);

The implementation, conceptually score.stats.cpp, belongs to the same module:

module score.stats;

int add_all(const int* values, int count) {
    int total = 0;
    for (int i = 0; i < count; ++i) total += values[i];
    return total;
}

int sum(const int* values, int count) {
    return add_all(values, count);
}

double average(const int* values, int count) {
    if (count == 0) return 0.0;
    return static_cast<double>(sum(values, count)) / count;
}

The definitions of sum and average complete the exported declarations from the interface. add_all is deliberately absent from that interface. It belongs to the module implementation and supports the exported functions, but an importing client does not receive its name as part of the public API.

Importing the C++20 module: trace the exact values and output

The client in main.cpp imports the library. <iostream> remains a traditional include, so the example does not assume support for standard-library header units or a standard std module.

#include <iostream>
import score.stats;

int main() {
    int marks[4] = {72, 81, 93, 64};
    std::cout << "sum=" << sum(marks, 4) << '\n';
    std::cout << "average=" << average(marks, 4) << '\n';
}

Trace add_all carefully. It begins with total = 0. Adding 72 gives 72; adding 81 gives 153; adding 93 gives 246; and adding 64 gives 310. Therefore, sum(marks, 4) returns 310. The average function converts the numerator before division, so 310.0 / 4 = 77.5.

The exact output is:

sum=310
average=77.5

For count == 0, this library returns 0.0. That is its chosen policy, not a rule imposed by modules. A client expression such as add_all(marks, 4) fails name lookup because the namespace-scope helper attached to the module was never exported. It is not private class membership.

Three-column split of the score.stats library: exported interface, implementation with add_all, and client output sum=310, average=77.5.

C++20 module visibility, reachability and macros: keep the boundaries straight

After import score.stats;, exported names such as sum and average participate in the client's ordinary name lookup. The non-exported helper add_all does not. The export keyword controls the interface. Merely placing a declaration in a file whose name ends in .cppm does not export it.

Visibility asks whether ordinary name lookup can find a declaration. Reachability lets the compiler use declarations needed to interpret an imported entity, even when their names are not separately offered as client API. This client needs only the two exported signatures, which use primitive types. It never needs the helper's name.

Macros need a separate mental model. Importing a named module does not cause ordinary macro definitions to flow into the importer, so it cannot simply replace a header whose purpose is to define client macros. Header units are a separate migration technique and should not be assumed to behave exactly like named modules.

C++20 module build order: interface first, consumers next, linker last

The conceptual build has three stages. First, compile score.stats.cppm to produce interface metadata and, commonly, an object file. Second, compile score.stats.cpp and main.cpp. Both need that metadata, but after it exists they are independent and may compile in parallel. Third, link all three object files into the executable.

Step

Prerequisite

Result

score.stats.cppm

Its source dependencies

score.stats interface metadata and score.stats.iface.o

score.stats.cpp

score.stats interface metadata

score.stats.impl.o

main.cpp

score.stats interface metadata

main.o

link

score.stats.iface.o, score.stats.impl.o, main.o

app

These are generic labels. Real toolchains may call the metadata BMI, CMI, PCM or IFC, and may choose other filenames and suffixes.

If a consumer is compiled before the interface artifact exists, its import cannot be resolved. If score.stats.impl.o is omitted at link time, the definitions of sum and average are missing. A cyclic interface dependency is a design problem that command reordering cannot repair. Compiler flags are toolchain-specific, so no one command line is universally portable.

Build dependency graph: the score.stats interface compiles first, then implementation and main.cpp before linking into the app.

C++20 modules traps and question patterns: test semantics, not flags

Mistake

What goes wrong

Correction

Treating import as textual paste

Wrong semantic model

Use compiled interface metadata

Assuming every declaration is exported

Hidden names stay unavailable

Export the intended API

Calling add_all from the client

Name lookup fails

Call sum or average

Compiling consumers first

Metadata is unavailable

Build the interface first

Omitting implementation objects

Definitions are missing

Link every required object

Expecting macros from a named module

Macros do not flow through import

Retain the header or assess header units

Treating flags or suffixes as standard

The recipe becomes tool-specific

Check tool documentation

Questions can ask you to classify export module score.stats; against module score.stats;, identify visible names (sum and average, not add_all), order the graph (interface, implementation and client, link), or trace {72, 81, 93, 64} to 310 and 77.5. “Modules remove the linker” is false.

For adoption, confirm the documentation for the compiler and build-system versions in use. Isolate one library boundary, retain legacy headers where required, express the interface dependency in the build graph, and test mixed module and header code in continuous integration.

C++20 modules: the short version and next step

Remember four lines: export module names the primary interface; export selects client-visible declarations; module score.stats supplies the hidden implementation; and import score.stats consumes the compiled interface. The build order is interface -> implementation and client -> link, and the worked result is 310 / 4 = 77.5.

Reproduce the three files, predict why add_all is unavailable, then follow your own tools' documentation. For wider language study, use C++ Programming. To extend C++ into placement coding alongside other languages, use Coding For Placements.