C++ Modules (WIP)

Introduction

In C++ 20 and beyond, modules can be used instead of include files to help structure software projects and improve compile time. This article provides some (very) basic notes on their usage.

A Basic Example

Consider the following files

// math.cppm
export module math;

export int add(int a, int b);
export void print_int(int a);
// math.cpp
module math;

import <iostream>;
import <sstream>;

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

void print_int(int a) {
    std::cout << a;
}
// main.cpp
import math;

#include <iostream>

int main() {
    std::cout << add(2,3) << "\n";
}

These define and run a basic module called math. This has two methods, the first takes two integers and returns their sum. The second prints those integers. The module methods are defined in the math.cppm file - this is similar to header files in traditional C++. The implementation of the module is done in the math.cpp file, and the methods are used in main.cpp.

To compile the module, the following GCC command is used:

g++ -std=c++20 -fmodules-ts math.cppm math.cpp main.cpp

However, this will likely fail with the following error

In module imported at math.cpp:3:1:
/usr/include/c++/16/iostream: error: failed to read compiled module: No such file or directory
/usr/include/c++/16/iostream: note: compiled module file is ‘gcm.cache/./usr/include/c++/16/iostream.gcm’
/usr/include/c++/16/iostream: note: imports must be built before being imported
/usr/include/c++/16/iostream: fatal error: returning to the gate for a mechanical issue
compilation terminated.

This is due to the standard library, which has been imported in math.cpp, not having compiled modules. Modules, even those provided by the C++ standard library, must be compiled before they can be used in compilation. Compilers do not ship modules pre-compiled as their compilation depends on the compiler arguments used, e.g. optimisation and debug symbols.

Using Modules for the Standard Library

G++ provides a convenient way to compile an individual header file for exactly the purpose described above:

g++ -std=c++20 -fmodules-ts -x c++-system-header <HEADER_NAME>

However, doing this for every standard library module is tedious. The following Makefile can be used to compile all the standard library modules:

# system_headers.make

CXX := g++
CXXFLAGS := -std=c++20 -fmodules-ts

# Add or remove headers as desired.
STD_HEADERS := \
    algorithm \
    any \
    array \
    atomic \
    bit \
    bitset \
    chrono \
    compare \
    complex \
    concepts \
    coroutine \
    deque \
    exception \
    filesystem \
    format \
    forward_list \
    functional \
    future \
    initializer_list \
    iomanip \
    ios \
    iosfwd \
    iostream \
    iterator \
    limits \
    list \
    map \
    memory \
    mutex \
    numbers \
    numeric \
    optional \
    queue \
    random \
    ranges \
    ratio \
    regex \
    set \
    span \
    sstream \
    stack \
    stdexcept \
    stop_token \
    streambuf \
    string \
    string_view \
    system_error \
    thread \
    tuple \
    type_traits \
    typeindex \
    typeinfo \
    unordered_map \
    unordered_set \
    utility \
    valarray \
    variant \
    vector

.PHONY: system_headers system_headers_clean

system_headers: $(STD_HEADERS:%=.stamp-%)

.stamp-%:
    $(CXX) $(CXXFLAGS) -x c++-system-header $*
    @touch $@

system_headers_clean:
    rm -f .stamp-*
    rm -rf gcm.cache 

This can then be used to compile the previous project using this Makefile:

# Makefile

include system_headers.make

.PHONY: all clean
.DEFAULT_GOAL := all

all: main

main: system_headers
    g++ -std=c++20 -fmodules-ts math.cppm math.cpp main.cpp -o main

clean: system_headers_clean
    rm -f main