Modules in C++20 and CMake

Starting with the word „Modules“, I have heard that .o or .obj files are modules, and it gets confused when talking about modularisation. Real modules is a different thing, read on!

For at least since 1990 modules have been used in the Fortran language, so the idea is pretty old, but why care?

Well, in C and C++ we have a big problem with included header files. Every #include means to include the whole file and pass it as whole to the sources which are to be resolved by the compiler. The process itself is fine as we know, but with many includes and multiple files the build time gets very large, especially when standard libraries are used. It is pretty much wasted time because many of the same includes in different sources (especially in bigger projects) are passed over and over again.

Finally the C++20 standard addresses that issue with modules and defines some rules similar to what is established in Fortran:

  • No more duplication of code passed to the compiler as import behaves differently compared to using #include with header files. Modules are precompiled units
  • You can select the units you are interested, meaning what is not required will not be considered thus reducing the compile time even further
  • No more include guards or similar compile hacks required since modules are imported only once

Set let’s start using C++20 today! Well, not quite. The standard is a bit over a year old (published on Dec 2020) and unfortunately the software is WIP. If you want to test it and keep opensource you should try Clang. GCC has been becoming better, but you need to compile system libraries into modules for your program. I am testing here GCC 11.1.0 and clang 13.0.0. You also should ensure you have the latest version of these compilers. With an up-to-date Arch or Manjaro that is the case. With Ubuntu you should wait for 22.04 LTS when it’s release or the Alpha/Beta daily build, being provided currently. On Visual Studio side use version 19.25 or later.

Now let’s check a module we created for our application. We call it helloworld.cpp:

module;

#include <iostream>

export module helloworld;

export void hello(){
    std::cout << "Test" << std::endl;
}

And for the main.cpp we just have:

import helloworld;  // import declaration

int main() {
    hello();
}

Finally, we create a CMakeLists.txt which should work with Clang, GCC and MSVC. We used the ninja generator to get this built, but as a generated Makefile it should work as well:

cmake_minimum_required(VERSION 3.16)
project(main LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

set(PREBUILT_MODULE_PATH ${CMAKE_BINARY_DIR}/modules)

function(add_module name)
    file(MAKE_DIRECTORY ${PREBUILT_MODULE_PATH})
          if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
            # using Clang
            add_custom_target(${name}.pcm
                    COMMAND
                        ${CMAKE_CXX_COMPILER}
                        -std=c++20
                        -c
                        ${CMAKE_CURRENT_SOURCE_DIR}/${ARGN}
                        -Xclang -emit-module-interface
                        -o ${PREBUILT_MODULE_PATH}/${name}.pcm
                    )
          elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
            # using GCC
            add_custom_target(${name}.pcm
                    COMMAND
                        ${CMAKE_CXX_COMPILER}
                        -std=c++20
                        -fmodules-ts
                        -c
                        ${CMAKE_CURRENT_SOURCE_DIR}/${ARGN}
                        -o ${name}.pcm
                    )
            #g++ -std=c++20 -fmodules-ts -xc++-system-header iostream
          elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
            # using Visual Studio C++
            add_custom_target(${name}.obj
                    COMMAND
                        ${CMAKE_CXX_COMPILER} /experimental:module
                        /c
                        ${CMAKE_CURRENT_SOURCE_DIR}/${ARGN}
                    )
          endif()
endfunction()

if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
  # using Clang
  add_compile_options(-fprebuilt-module-path=${PREBUILT_MODULE_PATH})

  add_module(helloworld helloworld.cpp)
  add_executable(mainExe main.cpp helloworld.cpp)

  add_dependencies(main helloworld.pcm)

elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  # using GCC
  add_compile_options(-fmodules-ts)
  add_module(helloworld helloworld.cpp)
  add_executable(main main.cpp)

  add_custom_target(module helloworld.cpp)

  target_link_options(main PUBLIC "LINKER:helloworld.pcm")
  add_dependencies(main helloworld.pcm)

elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
  # using Visual Studio C++
  add_compile_options(/experimental:module /c)

  add_module(helloworld helloworld.cpp)
  add_executable(mainExe main.cpp)


  add_dependencies(main helloworld.pcm)

endif()

As you can see the main file does not use any #include, but instead one import. Within the modules you can include files so legacy support is still possible as to be expected.

Conclusion

As you can see, the feature is very promising, but CMake does not yet support this directly. The CMakeLists.txt I created as example helps you to get around, but it is not as portable as it could be. Let’s hope CMake improves in future versions and creates some good interface for the use of modules directly.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.