Skip to content

Last updated:2026-10-12

C++20 Modules

When we use headers we wrote ourselves or third-party libraries, we usually pull them in with the #include directive to import a library — a technique everyone uses all the time. In effect, this approach copies all the code of one source file (the header) into another file.

This approach, however, runs into the following problems:

  • A source file may be included multiple times in the same target, so we usually use #pragma once or #ifndef to keep a source file from being included multiple times in the same translation unit.
  • Copying code makes compile times longer: once a header is modified, every file that indirectly includes that header has to be recompiled.
  • #include ordering problems, which sometimes cause baffling compile errors.

C++20 introduced a new way to replace the #include directive, called modules.

Let's take a deep dive into modules.

1. Module Units

A C++ module consists of one or more translation units (tu) containing specific keywords for module declarations. Such translation units are called module units.

A translation unit that is not a module unit is considered part of the global module. The global module is anonymous, has no interface, and contains regular non-module code.

1.1 Global Module Fragment

A module unit may be prefixed with a global module fragment. When headers cannot be imported (particularly when headers use preprocessor macros as configuration), this global module fragment can keep using the original code directly.

For example: in the code below, the part between module; and export module Foo; is the global module fragment.

module;
#include <iostream>
#ifdef Say
void hello();
#endif
export module Foo;
// purivew
void world();

One thing you must note: if the module has a global module fragment, the first declaration must be module; — in other words, putting this declaration anywhere else is an error.

1.2 purview

The purview can be understood as the module's entire scope. It starts at the module declaration and extends to the end of the translation unit.

For example: hello is not in it, while world and GetData are both in the purview.

module;
void hello();
// <- 这里不在Foo模块的purview内
export module Foo;
// <- 在Foo模块的purview内
void world();
export void GetData();

1.3 Private module fragment

The primary module interface unit may be suffixed with a private module fragment. This part may appear only in the primary module interface unit, and if it exists, the module unit it appears in must be the module's only unit. Its purpose is to wrap the module's interface and implementation in a single translation unit without exposing implementation details.

For example: I want to create a Shape and compute its area.

Only an interface for creating a concrete Shape needs to be exposed externally, calling a shared area-computing interface, so we can write the following module.

export module Shape;

export class Shape {
  public:
   virtual double CalculateArea() = 0;
};

export { std::shared_ptr<Shape> CreateRectangle(double length, double width); }


module :private; // here

class Rectangle : public Shape {
 private:
  double length;
  double width;

 public:
  Rectangle(double l, double w) : length(l), width(w) {}

  double CalculateArea() override { return length * width; }
};

std::shared_ptr<Shape> CreateRectangle(double length, double width) {
  return std::make_shared<Rectangle>(length, width);
}

Can we put all the internal details inside the private fragment? My own g++ version is 13; it does not support this yet and reports the following error:

For gcc's current support status, see here

https://gcc.gnu.org/projects/cxx-status.html

shape.cppm:14:1: sorry, unimplemented: private module fragment
   14 | module :private;

My local clang is version 16; I tested it and it runs fine!

➜  clang++ -std=c++20 shape.cppm --precompile -o shape.pcm  
➜  clang++ -std=c++20 shape.cc -fprebuilt-module-path=. shape.pcm -o shape
➜  ./shape 
area is 2

Of the three parts above, the global and private module fragments are not required for a module to exist; the purview is the part a module cannot do without.

2. Using Modules

2.1 Creating a Module

Creating a module is similar to defining a header file: it is also a file, usually with the .cppm suffix. We just use the export and module keywords in this file, followed by the module name, and we have created an exportable module. For example:

// foo.cppm
export module foo;

// main.cc
import foo;

The export keyword can be used on classes, variables, and so on. There are usually two ways to write it:

export void func();

export {
  void func();
  constexpr double PI{3.14};
};

In one form, the export keyword goes in front of an ordinary declaration; the other is the export block, similar to the namespace syntax, which can export multiple things.

2.2 Exporting

This raises an important question: what can be exported?

  • variables, classes, structs, functions, namespaces, template functions/classes, concepts can be exported
  • Entities with internal linkage cannot be exported, such as static variables and functions, and anonymous namespaces.
export static constexpr double PI = 3.14; // 不可导出
  • Export declarations must appear at namespace level
namespace {
  export void print_no_export() { // 匿名命名空间,不可导出
    std::cout << "print no export" << std::endl;
  }
};
namespace light {
  export void print_export() { // ok
    std::cout << "print export" << std::endl;
  }
};

struct Foo {
  export int a; // 不能导出成员变量
};
  • Exporting a namespace implicitly exports everything inside it
export class Rectangle {
 private:
  double length;
  double width;
 public:
  Rectangle(double l, double w) : length(l), width(w) {} 

  double CalculateArea() { return length * width; } // 隐式导出
};

export namespace {
  void print_export() { // 隐式导出
    std::cout << "print export" << std::endl;
  }
};

export {
  void func();  // 隐式导出
  constexpr double PI{3.14}; // 隐式导出
};
  • The first declaration of an exported entity must be an export declaration. Subsequent declarations and definitions do not need the export keyword.
export class Foo; // ok
export class Foo; // ok,只是会冗余

class Foo { // ok,隐式export
 public:
  void print() { std::cout << "this is foo" << std::endl; }
};

class Bar;  // not export
export class Bar; // 无效,第一个声明已经是不可导出,后续的不可导出


Foo f; // ok
f.print(); // ok
Bar b; // not ok
  • A non-interface module unit cannot export
module foo;

export void print { }  // error: 'export' may only occur after a module interface declaration

2.3 Importing

The counterpart is importing, which also has its rules, for example:

  • A module cannot import itself

  • In a module unit, all imports must appear before any other declaration in that module unit. You cannot import at an arbitrary point inside a module unit.

void func() {}
import shape; // error: post-module-declaration imports must be contiguous

---------------
import shape; // ok
void func() {}
  • Only global-scope imports are allowed
namespace light {
  import shape; // not ok
};
  • Circular imports are not allowed
// shape.cppm
export module shape;
import circle; // 循环导入

// circle.cppm
import shape;

2.4 include in Modules

How do you use #include <iostream> inside a module?

2.4.1 #include in the purview region

Replace #include with import

export module foo;
import <iostream>;

For example:

Compiling with g++-13 as follows; you can pass iostream after c++-system-header to build the gcm.

g++-13 -std=c++20 -fmodules-ts -x c++-system-header iostream 
g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm shape.cc

For your own headers, such as consts.h, it turns out you can simply #include them inside the module, for example:

export module foo;
#include "consts.h"

2.4.2 #include in the Global Module Fragment region

Both #include and macros can be placed directly in this region, for example:

module;
#ifdef
#include <iostream>
#endif
export module foo;

3. Decomposing Modules

When we want to break a large module into smaller modules, we can use the following two approaches:

  • Module partitions.
  • These allow us to split a module into multiple files. This is effectively invisible to users, who simply import the module as usual.
  • Submodules.
  • These allow us to break a larger module into a hierarchy of any number of submodules. Users can choose to import the whole module, or only specific submodules.

3.1 Module Partitions

Syntax:

export module <module-name>:<part-name>;
import :<part-name>; // 可以在import前面添加export导出该分区接口

For example: I have a shape; when consuming it, all you need is import shape and then calling the corresponding interfaces — here the interfaces of the circle partition and the rectangle partition are used respectively.

import shape;

int main() {
    DrawCircle();
    DrawRectangle();
    return 0;
}

shape has two partitions, one is circle and the other is rectangle, so the module shape.cppm contains:

export module shape;

export import :circle;
export import :rectangle;

The two partitions contain:

// circle.cppm
module;
#include <iostream>
export module shape:circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }

--------------
// rectangle.cppm

module;
#include <iostream>
export module shape:rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }

After compiling with both clang and g++, clang-16 reports an error — not supported.

 error: sorry, module partitions are not yet supported

g++-13 supports it; note that when compiling you must go in the order partition -> main partition, otherwise it fails.

➜ g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm rectangle.cppm shape.cc
shape:circle: error: failed to read compiled module: No such file or directory
shape:circle: note: compiled module file is 'gcm.cache/shape-circle.gcm'
shape:circle: note: imports must be built before being imported
shape:circle: fatal error: returning to the gate for a mechanical issue

It should be changed to:

g++ -fmodules-ts -std=c++20 -x c++ circle.cppm rectangle.cppm shape.cppm shape.cc

3.2 Submodules

Syntax:

export module <module-name>.<sub_module-name>;
import <module-name>.<sub_module-name>; // 可以在import前面添加export导出该分区接口

The example above can also be implemented with submodules: shape stays the main module, with circle and rectangle as the two submodules.

// shape.cppm
export module shape;

export import shape.circle;
export import shape.rectangle;

---------------------
// rectangle.cppm
module;
#include <iostream>
export module shape.rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }

---------------------

// circle.cppm
module;
#include <iostream>
export module shape.circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }

Note that in usage the module partition form uses :, while the submodule form uses ..

The caller-side code is the same as for module partitions.

There are some usage differences, though: for example, when a partition is imported directly, using its interface triggers internal compiler error: Segmentation fault: 11, whereas a submodule can be imported and used normally.

// 引入子分区
import shape:circle;

int main() {
    DrawCircle(); // error
    return 0;
}


// 引入子模块
import shape.circle;

int main() {
    DrawCircle(); // ok
    return 0;
}

For submodules, the main module name cannot be omitted in the import. Above, when importing a partition module from within the main partition, we could use :circle; here .circle cannot be used. The error is as follows:

shape.cppm:3:8: error: 'import' does not name a type
    3 | export import .circle;

4. Interface and Implementation

When writing code, we usually split it into a "header file" and an "implementation file". With modules, this step is no longer needed. You can still follow the old writing style, though.

For example: drawing a shape.

  • Define the module shape.cppm
export module shape;

export class Shape {
 public:
  Shape();
  void Draw();
};
  • Implement the module shape.cc
import shape;
import <iostream>;

Shape::Shape() {}
void Shape::Draw() { std::cout << "draw a shape" << std::endl; }

This is basically the same as the usual .h and .cpp separation.

https://clang.llvm.org/docs/StandardCPlusPlusModules.html

https://timsong-cpp.github.io/cppwp/n4861/module.import#7.sentence-2

https://gcc.gnu.org/projects/cxx-status.html

Comments