avatar

Andres Jaimes

C++20 Modules

By Andres Jaimes

- 3 minutes read - 474 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:

clang --version
Apple clang version 14.0.3 (clang-1403.0.22.14.1)
Target: arm64-apple-darwin22.5.0
Thread model: posix
InstalledDir: /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:

brew 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).

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

We can check the version of clang again:

clang --version
Homebrew clang version 16.0.6
Target: arm64-apple-darwin22.5.0
Thread model: posix
InstalledDir: /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.

// square.cpp
export module square;

export class square {
private:
    int side;
public:
    square(int side);
    int area();
};

square::square(int side): side(side) { }

int square::area() {
    return side * side;
}

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:

// main.cpp
import square;
#include <iostream>

int main() {
    square s(5);
    std::cout << "The area is: " << s.area() << std::endl;
    return 0;
}

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:

clang++ -std=c++2a -Xclang -emit-module-interface -c square.cpp -o square.pcm
clang++ -std=c++2a -fprebuilt-module-path=. main.cpp square.cpp -o main
./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:

// main.cpp
import square;
import std;

int main() {
    square s(5);
    std::cout << "The area is: " << s.area() << std::endl;
    return 0;
}

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

clang++ \
  -std=c++2a \
  -Xclang \
  -emit-module-interface \
  -c square.cpp \
  -o square.pcm
clang++ \
  -std=c++2a \
  -fimplicit-modules \
  -fimplicit-module-maps \
  -fprebuilt-module-path=. \
  main.cpp square.cpp \
  -o main
./main