C++ Modules in 2024: Part 1
import std;
auto main() -> int {
std::println("Hello, {}!", "world");
}
You wish you could write this kind of code! Let me teach you how.
Getting the stuff
This assumes you’re using Linux of some kind. I’ll probably cover macOS and Windows some time later.
What we need to get this to work is the following:
- CMake 3.28 + Ninja 1.11
- A recent C++ compiler. And by C++ compiler I mean Clang. I’m using version 18 and you should too (though this will probably work with some of the older versions as well)
- A custom-built libc++. This will allow us to use
import std;
, though we’d have to work for it a bit
I use Arch btw, so my package manager already takes care of the first two items. If this is not the case for you, you can download binary releases for both CMake and Clang. Or if you’re feeling adventurous you can always build them from source.
Anyway, let’s get our hands on the LLVM sources and build libc++:
; git clone https://github.com/llvm/llvm-project.git
; cd ./llvm-project
; mkdir build-libcxx
; cd ./build-libcxx
The options to build the necessary components are fairly straightforward.
I specify -DLIBCXX_ABI_UNSTABLE=ON
since we’re going to be linking libc++
statically anyway and it doesn’t really make sense to care about ABI compatibility.
The option -DLIBCXX_ENABLE_STD_MODULES=ON
was required until recently, but it doesn’t seem
to be the case for the latest version from the main branch of LLVM.
; cmake \
-DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind" \
-DCMAKE_BUILD_TYPE=Release \
-DLIBCXX_ABI_UNSTABLE=ON \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-GNinja ../runtimes
; ninja
Yay! Now we have our binary files in ./lib
, all of our headers in ./include
and our module files (which basically just include the headers and export the necessary names)
in ./modules
.
Using the stuff
Now let’s create our project in a different directory.
First we’ll need a CMake file that defines the target for our std
module.
It expects the variable LIBCXX_PATH
to point to our LLVM build directory.
It sets the C++ standard, copies the module definition files into the project
build directory, so that CMake won’t complain about out-of-tree builds.
Then it defines a function add_module
that basically just adds a target
as a static library and adds the provided files to it as C++ module sources.
This function is used to define the std
target. Linking to it will do the following:
- Link statically with our versions of libc++, libc++abi and libunwind
- Expose libc++’s include path
- And finally, add module
std
to the search path
# file: ./modules.cmake
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_EXTENSIONS OFF)
file(COPY "${LIBCXX_PATH}/modules/c++/v1/"
DESTINATION "${PROJECT_BINARY_DIR}/c++std/")
function(add_module target)
add_library(${target} STATIC)
target_sources(${target} PUBLIC FILE_SET CXX_MODULES FILES ${ARGN})
endfunction()
add_module(std "${PROJECT_BINARY_DIR}/c++std/std.cppm")
target_include_directories(std PUBLIC "${LIBCXX_PATH}/include/c++/v1")
target_compile_options(std PRIVATE
-Wno-reserved-module-identifier
-Wno-reserved-user-defined-literal
)
target_compile_options(std PUBLIC -nostdlib++ -nostdinc++)
target_link_options(std PUBLIC -nostdlib++)
target_link_libraries(std PRIVATE
"${LIBCXX_PATH}/lib/libc++.a"
"${LIBCXX_PATH}/lib/libc++abi.a"
"${LIBCXX_PATH}/lib/libunwind.a"
)
set_target_properties(std PROPERTIES OUTPUT_NAME "c++std")
In CMakeLists.txt
we just need to include the above file and link our program
with the std
target.
# file: ./CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(cpp_modules_test)
include(modules.cmake)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE std)
Our program looks the same:
// file: ./main.cpp
import std;
auto main() -> int {
std::println("Hello, {}!", "world");
}
Now to compile it. Don’t forget to pass LIBCXX_PATH
to CMake:
; mkdir build
; cd build
; cmake \
-DLIBCXX_PATH="/path/to/your/llvm-project/build-libcxx" \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-GNinja ..
; ninja
And what do you know? It actually works:
; ./main
Hello, world!
Extending the stuff
For now we’ve only used one module: std
.
As much as it pains me to admit it, sometimes we need to write more complex
programs than a “Hello, world!”. This means we need to learn how to create and use our own modules.
Luckily, it’s not very difficult.
We didn’t need to define our module for now only because the main
function is not supposed
to live inside of any module unit. But now, let’s say we have a module greeter
:
// file: greeter.cppm
export module greeter;
import std;
export void greet(std::string_view who) {
std::println("Hello, {}!", who);
}
The file doesn’t actually have to have a .cppm
extension,
but it’s a convention so let’s stick to it.
If your IDE freaks out and doesn’t recognize .cppm
file as C++, I hereby grant you permission
to just not care and use .cpp
.
Also note that the name of the file has absolutely nothing to do with the name of the module. You are allowed to name your files completely differently from the modules that they provide. But as usual, it’s best to keep it reasonable.
Let’s modify main.cpp
to use this module instead:
// file: main.cpp
import greeter;
auto main() -> int {
greet("world");
}
And let’s define a new CMake target for the module by calling
our trusty add_module
function. We’ll also need to tell CMake
that greeter
depends on std
and main
depends on greeter
:
# file: CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(cpp_modules_test)
include(modules.cmake)
add_executable(main main.cpp)
add_module(greeter greeter.cppm)
target_link_libraries(greeter PRIVATE std)
target_link_libraries(main PRIVATE greeter)
And that’s it, if you build and run it now it should work:
; ninja && ./main
Hello, world!
Our custom modules can of course depend on other modules than just std
.
Just be careful not to create a circular dependency, since that won’t work.
Partitioning the stuff
If you’re using module partitions you should add all the partitions as one CMake target
(so use one add_module
call). Module partitions are a bit different in this regard
to standalone module units. Here’s an example:
add_module(big_module
big_module.cppm
big_module_partition_a.cppm
big_module_partition_b.cppm
big_module_partition_c.cppm
)
The partitions should look like this:
// file: big_module_partition_a.cppm
export module big_module:partition_a;
// ...
And the main module file like this:
// file: big_module.cppm
export module big_module;
export import :partition_a;
export import :partition_b;
export import :partition_c;
I hope this helps someone. It took me some time to figure out all the details :)
Stay tuned for a dive into generating module definition files automatically from headers! Also maybe about getting all of this to work on macOS or Windows. Or maybe about something else entirely! See ya.