Skip to content

Modules (C++20) — Module Interface, Export, Import, Module Partitions, Header Units

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Modules (C++20). We cover key concepts, practical examples, and best practices to help you master this topic.

C++20 modules replace header files with a compiler-managed module system — export, import, and module partitions — eliminating textual inclusion, reducing build times, and providing true Encapsulation.

What You'll Learn

You will create module interface files with export module, use import to consume modules, control visibility with export declarations, organize large modules with partitions, understand the advantages over headers (no ODR violations, no preprocessor leaks), and migrate existing projects from headers to modules incrementally.

Why It Matters

The C++ preprocessor-based header system has fundamental flaws: macros leak across files, ODR violations are easy to create, and every #include re-parses the same text thousands of times. Modules solve all of these. C++ modules compile 2-10x faster than equivalent header-based code, prevent macro leakage, and enforce proper encapsulation with explicit export boundaries.

Learning Path

graph LR
    A["57: Coroutines"] --> B["58: Modules"]
    B --> C["59: Exception Safety"]
    C --> D["60: RAII & Resource Management"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Basic Module Structure

A module consists of a module interface file (.cppm) that defines what is exported, and module implementation files (.cpp) that provide the implementation.

// math.cppm — Module Interface File
export module math;  // Declare module 'math'

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

export int multiply(int a, int b) {
    return a * b;
}

// Not exported: invisible to importers
int internalHelper(int x) {
    return x * 2;
}
// main.cpp — Consumer
import math;  // Import the module

int main() {
    return add(3, 4);  // OK: exported
    // internalHelper(5);  // Error: not exported
}

Exporting Declarations

You can export individual declarations, groups, or entire namespaces.

// geometry.cppm — Module Interface
export module geometry;

// Export individual function
export double areaOfCircle(double radius);

// Export a group
export {
    double circumference(double radius);
    constexpr double PI = 3.141592653589793;
}

// Export a namespace (everything inside is exported)
export namespace shapes {
    struct Rectangle {
        double width, height;
    };
    double area(const Rectangle& r);
}

// Implementation (can be in a separate .cpp file)
double areaOfCircle(double radius) {
    return PI * radius * radius;
}

double circumference(double radius) {
    return 2.0 * PI * radius;
}

double shapes::area(const shapes::Rectangle& r) {
    return r.width * r.height;
}
// main.cpp
import geometry;

int main() {
    double a = areaOfCircle(5.0);
    double c = circumference(5.0);

    shapes::Rectangle rect{3.0, 4.0};
    double ra = shapes::area(rect);
}

Module Partitions

Large modules can be split into partitions — internal submodules that are only visible within the module.

// math.cppm — Primary module interface
export module math;

// Import partitions (internal: not re-exported)
import :algebra;   // imports math:algebra partition
import :geometry;  // imports math:geometry partition

// Re-export selected symbols
export {
    // Re-export from partitions
    using ::algebra::solveQuadratic;
    using ::geometry::distance;
}
// algebra.cppm — Module partition
export module math:algebra;  // Partition of module math

export namespace algebra {
    struct QuadraticSolution {
        double root1, root2;
        bool hasRealRoots;
    };

    QuadraticSolution solveQuadratic(double a, double b, double c) {
        double discriminant = b * b - 4 * a * c;
        if (discriminant < 0) return {0, 0, false};

        double sqrtD = std::sqrt(discriminant);
        return {
            (-b + sqrtD) / (2 * a),
            (-b - sqrtD) / (2 * a),
            true
        };
    }
}
// geometry.cppm — Another partition
export module math:geometry;

export namespace geometry {
    double distance(double x1, double y1, double x2, double y2) {
        double dx = x2 - x1;
        double dy = y2 - y1;
        return std::sqrt(dx * dx + dy * dy);
    }
}

import vs #include

Modules coexist with headers. You can mix both.

// hybrid.cpp — Mixing modules and headers
#include <iostream>      // Traditional header — still works
import math;             // Module import
import <vector>;         // Import header as module unit (if supported)
import <string>;         // Same

int main() {
    std::cout << add(3, 4) << "\n";  // From module

    std::vector<int> v = {1, 2, 3};  // From header/module
    std::cout << v.size() << "\n";
}

Module Ownership Rule

Each entity in C++ must belong to at most one module (or the global module). This prevents ODR violations.

// module_a.cppm
export module module_a;
export int shared() { return 1; }

// module_b.cppm
export module module_b;
// int shared() { return 2; }  // Error: 'shared' already defined in module_a

// But you can import and re-export:
export import module_a;  // Re-export everything from module_a
export int added() { return shared() + 1; }

Global Module Fragment

The module; declaration at the top creates a global module fragment for #include directives needed by the module implementation.

// file_io.cppm — Using global module fragment
module;  // Global module fragment starts here

#include <fstream>
#include <string>
#include <vector>

export module file_io;  // Module declaration starts here

// These headers are local to the module and don't leak to importers
export std::vector<std::string> readLines(const std::string& path) {
    std::vector<std::string> lines;
    std::ifstream file(path);
    std::string line;
    while (std::getline(file, line)) {
        lines.push_back(line);
    }
    return lines;
}

Private Module Fragment

The private module fragment lets you keep implementation details in the same file without exporting them.

// container.cppm
export module container;

export class IntVector {
    int* data_;
    size_t size_;
    size_t capacity_;

public:
    IntVector();
    ~IntVector();
    void push_back(int value);
    int& operator[](size_t index);
    size_t size() const;
};

// Private module fragment: implementation stays here
module :private;  // Everything after is not part of the module interface

IntVector::IntVector() : data_(nullptr), size_(0), capacity_(0) {}
IntVector::~IntVector() { delete[] data_; }

void IntVector::push_back(int value) {
    if (size_ >= capacity_) {
        size_t newCap = capacity_ * 2 + 1;
        int* newData = new int[newCap];
        for (size_t i = 0; i < size_; ++i) newData[i] = data_[i];
        delete[] data_;
        data_ = newData;
        capacity_ = newCap;
    }
    data_[size_++] = value;
}

int& IntVector::operator[](size_t index) { return data_[index]; }
size_t IntVector::size() const { return size_; }

Build System Integration

Modules require build system support. CMake 3.28+ and Ninja 1.11+ support C++20 modules.

# CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Enable module support
set(CMAKE_EXPERIMENTAL_CXX_MODULES CMAKE_EXPERIMENTAL_CXX_MODULES)

# Add module sources
add_library(mylib
    math.cppm          # Module interface
    math_impl.cpp      # Module implementation (if separate)
    geometry.cppm      # Another module
)

# Executable that imports modules
add_executable(main main.cpp)
target_link_libraries(main PRIVATE mylib)

Common Mistakes

Mistake 1: Using #include inside a module interface

export module mymodule;
#include "helper.h"  // Leaks everything from helper.h to importers!

Use global module fragment for includes that should stay local.

Mistake 2: Forgetting to export

export module mymodule;
int hidden() { return 1; }  // Not exported: invisible to importers
export int visible() { return hidden(); }  // Correct

Mistake 3: Circular imports between modules

// a.cppm: import b;
// b.cppm: import a;  // Error: circular

Modules cannot import each other. Extract shared interfaces into a third module.

Mistake 4: Expecting modules to work without build system support

Most compilers need specific flags (-fmodules-ts for GCC, /std:c++20 + module support for MSVC).

Mistake 5: Mixing module partitions incorrectly

Partitions are not independent modules. They belong to exactly one parent module and cannot be imported by other modules.

Practice Questions

  1. What is the primary advantage of modules over headers? Answer: Modules eliminate textual inclusion, prevent macro leakage, reduce build times (no re-Parsing), and provide true encapsulation with explicit export boundaries.

  2. How do you create a module interface in C++20? Answer: Create a .cppm file starting with export module modulename; and mark exported declarations with export.

  3. What is a module partition? Answer: An internal submodule of a larger module, declared as export module parent:partition;. Only visible within the parent module.

  4. Can modules and headers be mixed? Answer: Yes. You can import some modules and #include headers in the same file, though mixing is a transition Strategy.

  5. What is the global module fragment? Answer: Code before the module; declaration (the global module fragment) is used for #include directives that should not leak to module consumers.

FAQ

What are C++20 modules

Modules replace header files with a compiler-managed system. They use export and import keywords, provide true encapsulation, and avoid preprocessor-related issues.

Do modules improve compilation speed

Yes, typically 2-10x faster for large projects. Modules are compiled once and cached as binary module files, unlike headers which are re-parsed in every translation unit.

Can I use modules with existing libraries

You can import header units (import ) or use traditional #include. Many standard libraries now ship module interfaces.

What compilers support C++20 modules

MSVC has the best support (Visual Studio 2022+). Clang 16+ and GCC 14+ have partial support. Build system support (CMake 3.28+) is recommended.

How do modules prevent ODR violations

Each entity belongs to exactly one module (or the global module). The compiler tracks ownership and detects duplicate definitions across modules.

Mini Project

Convert a small C++ library from headers to modules. Take this simple math library and create module interface files:

// Traditional header-based library:
// math_utils.h — contains add, subtract, multiply, divide, factorial

// Task: Convert to C++20 modules

// math_utils.cppm — module interface

// math_utils_impl.cpp — implementation (optional)

// main.cpp — import the module and use the functions
import math_utils;

int main() {
    std::cout << add(5, 3) << "\n";         // 8
    std::cout << factorial(5) << "\n";      // 120
    std::cout << divide(10, 3) << "\n";     // 3.333...
    // internalHelper(42);                   // Error: not exported
}

This project demonstrates how C++ modules transform the compilation model. Compare with Java packages and Python modules, which have always had first-class module systems.

What's Next

You now understand modules — C++20's replacement for headers. Next, you will explore exception safety in C++, learning how to write code that is exception-safe and how to design with the noexcept specification.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro