Skip to content
C Codeloom
C++

C++ Modules Introduction

Learn C++20 modules: what they replace, how to declare and import them, and how they speed up compilation and clean up the preprocessor.

·3 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • Why modules exist
  • Module interface vs implementation
  • export and import
  • Build-system support
  • Migration tips

Prerequisites

  • Basic familiarity with C++

What and Why

For decades, C++ code-sharing has relied on the preprocessor: headers, include guards, and macro hygiene. Headers are textually pasted into every translation unit that includes them. The result is slow builds, fragile macros, and ODR-violation landmines.

C++20 modules replace this with a real component system. A module is compiled once, exports a curated set of names, and is consumed via import without textual inclusion.

Mental Model

Think of modules as compiled libraries with a clean public interface. The compiler produces a Binary Module Interface (BMI) for each module; importers read the BMI instead of reparsing source.

Headers:                          Modules:
foo.h --included into-->          foo.cppm --compiled to--> foo.bmi
a.cpp (parsed)                  a.cpp imports foo (reads BMI)
b.cpp (parsed)                  b.cpp imports foo (reads BMI)
c.cpp (parsed)                  c.cpp imports foo (reads BMI)
(parsed N times)                  (parsed once)
Header vs module compilation

Hands-on Example

A module interface unit (math.cppm):

export module math;

export int add(int a, int b) { return a + b; }

int internal_helper() { return 42; } // not exported

export namespace geom {
  double area(double r);
}

A consumer (main.cpp):

import math;
#include <iostream>

int main() {
  std::cout << add(2, 3) << "\n";
  // internal_helper(); // ERROR: not exported
}

Compile with Clang:

clang++ -std=c++20 --precompile math.cppm -o math.pcm
clang++ -std=c++20 -fmodule-file=math=math.pcm main.cpp math.cppm -o app

GCC and MSVC have analogous flags. CMake 3.28+ has native support via add_library(... FILE_SET CXX_MODULES ...).

Common Pitfalls

Mixing headers and modules badly. You can #include <vector> inside a module, but every translation unit that does will reparse it. Prefer import std; (C++23) or wrap legacy headers with import <vector>; (header units) where supported.

Macros do not cross modules. That is a feature: imports never leak macros into the importer. But it means library-style macros must become regular code or be redeclared per file.

Build system support is uneven. Module support shipped late in CMake, Bazel, and Meson. Verify your toolchain (Clang 16+, GCC 14+, MSVC 19.34+) and build system before adopting widely.

Order matters. Module BMIs must be built before consumers. Most build systems handle dependencies automatically, but ad-hoc Makefiles will not.

Circular imports. Modules cannot import each other circularly. This forces healthier dependency graphs but can require refactoring during migration.

Practical Tips

  • Start with new code in a leaf library. Don’t try to migrate an entire codebase at once.
  • Use the global module fragment (module; block) to include legacy headers safely:
module;
#include <legacy.h>
export module wrapper;
export void wrapped() { /* uses legacy */ }
  • Keep one logical concept per module. Modules don’t need to mirror file organization, but small focused modules give you the biggest build-speed wins.
  • Use export module foo:utils; for module partitions to split a large module across files.
  • Measure compile times before and after; the gains are real but vary by codebase.

Wrap-up

Modules are the most significant change to C++ build mechanics since templates. They fix problems we’ve lived with for decades: header parse time, macro pollution, ODR violations from accidental include order. Tooling is still catching up, so adopt incrementally. New library? Try modules. Million-line codebase? Wait for your build system and CI to stabilize their support, then migrate leaf-first.