avatar

Andres Jaimes

C++20 Modules

By Andres Jaimes

- 3 minutes read - 499 words

In this article, we’ll see how to create and use C++20 modules. We’ll use clang-16 to compile our code.

Prerequisites

OSX comes with clang-14 by default (2023). We can check its version with the following command:

1clang --version
2Apple clang version 14.0.3 (clang-1403.0.22.14.1)
3Target: arm64-apple-darwin22.5.0
4Thread model: posix
5InstalledDir: /Library/Developer/CommandLineTools/usr/bin

The latest version as of the time of this writing is clang-16. Use the following command to install it with homebrew:

1brew install llvm

Important output:

To use the bundled libc++ please add the following LDFLAGS:
  LDFLAGS="-L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++"

llvm is keg-only, which means it was not symlinked into /opt/homebrew,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.

If you need to have llvm first in your PATH, run:
  echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc

For compilers to find llvm you may need to set:
  export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
  export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"

So, to set up our clang-16 environment, we have to use the following commands. Remember that these changes will be lost when we close the terminal. To make them permanent, we should add them to our shell configuration file (e.g. ~/.zshrc).

1export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
2export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
3export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"

We can check the version of clang again:

1clang --version
2Homebrew clang version 16.0.6
3Target: arm64-apple-darwin22.5.0
4Thread model: posix
5InstalledDir: /opt/homebrew/opt/llvm/bin

Creating a module

We’ll create a simple module called square. The module will contain a class with the same name and a function to calculate its area.

 1// square.cpp
 2export module square;
 3
 4export class square {
 5private:
 6    int side;
 7public:
 8    square(int side);
 9    int area();
10};
11
12square::square(int side): side(side) { }
13
14int square::area() {
15    return side * side;
16}

Notice that we are adding the implementation to the same file. This is not a requirement, but it’s a common practice for modules.

Let’s now create the corresponding main file and import our module:

1// main.cpp
2import square;
3#include <iostream>
4
5int main() {
6    square s(5);
7    std::cout << "The area is: " << s.area() << std::endl;
8    return 0;
9}

Notice that we are mixing the old include and the new import syntax.

At this point we can create a module interface file for the square.cpp file, and then use it to compile the main.cpp file:

1clang++ -std=c++2a -Xclang -emit-module-interface -c square.cpp -o square.pcm
2clang++ -std=c++2a -fprebuilt-module-path=. main.cpp square.cpp -o main
3./main

clang++ includes a std module map that allows us to import the whole standard library by using import std; instead of the older #include statements:

1// main.cpp
2import square;
3import std;
4
5int main() {
6    square s(5);
7    std::cout << "The area is: " << s.area() << std::endl;
8    return 0;
9}

To compile it, we need to add -fimplicit-modules -fimplicit-module-maps to the previous command:

 1clang++ \
 2  -std=c++2a \
 3  -Xclang \
 4  -emit-module-interface \
 5  -c square.cpp \
 6  -o square.pcm
 7clang++ \
 8  -std=c++2a \
 9  -fimplicit-modules \
10  -fimplicit-module-maps \
11  -fprebuilt-module-path=. \
12  main.cpp square.cpp \
13  -o main
14./main