如何在CMake中使用C++20模块?

60

ClangMSVC 已经支持来自未完成的 C++20 标准的 Modules TS

我可以使用 CMake 或其他构建系统构建我的模块项目吗?如何操作呢?

我尝试了 build2,它支持模块并且运行良好,但是我对它的依赖管理有一个问题(更新:该问题已关闭)。


你可以使用设置 CXX_STANDARD 的组合,以及一个 CMake if 语句来检查 Clang 或 MSVC,并根据你的编译器添加相应的编译器标志。 - Kevin
我尝试使用-std=c++2a -fmodules-ts选项使用Clang编译代码,但是它报错说致命错误:找不到模块“VulkanRender”。我该如何告诉Clang我的模块存储在哪里? - Blaze
1
我又尝试了build2并回答了我的问题。一切工作得非常好! - Blaze
我有一个带有C++模块的CMake项目示例。可以使用GCC编译。https://github.com/bravikov/cmake-cpp-modules-example - Dmitry Bravikov
你也可以尝试使用xmake,它也支持模块和头文件单元。https://github.com/xmake-io/xmake/tree/master/tests/projects/c%2B%2B/modules - ruki
9个回答

49

实验性模块支持

这反映了CMake 3.27的状态

随着情况的变化,我会不断更新这个答案。

重要提示:CMake对于C++20模块的支持目前处于功能性阶段,但仍然是实验性的。在某些情况下可能正常工作,但在其他情况下可能会出现问题。请预期版本之间存在错误和破坏性变更! 请参考CMake问题跟踪器中相关问题

请注意,支持模块需要构建系统提供比插入新的编译器选项更多的支持。它从根本上改变了在构建过程中处理源文件之间依赖关系的方式:在没有模块的世界中,所有cpp源文件可以以任意顺序独立构建。而使用模块后,这种情况不再成立,这不仅对CMake本身有影响,也对下游构建系统有影响。

详细了解CMake Fortran模块论文中的技术细节。从构建系统的角度来看,Fortran的模块行为与C++20模块非常相似。

先决条件

正确的集成目前仅适用于以下生成器:

  • Ninja 版本 1.10 或更新版本
  • Visual Studio 2022 版本 19.34 或更新版本。

模块依赖扫描当前支持以下编译器:

  • MSVC 编译器版本 19.34 或更新版本
  • LLVM/Clang 版本 16 或更新版本。

请确保您的编译器和构建系统都足够更新!

注意:要使 Clang 正常工作,至少需要 Clang 版本 16,并且可能还需要对 CMAKE_EXPERIMENTAL_CXX_SCANDEP_SOURCE 进行一些调整。由于这涉及到工具内部的深层操作,我在此回答中将不涉及该内容!如果您想尝试,请查看CMake 的功能指南。这个问题应该会在未来的 CMake 版本中得到解决。

一些一般性的备注

  • 始终使用您能获得的最新的 CMake、构建工具和编译器版本。该功能仍在积极开发中,并接收持续的关键错误修复。
  • 阅读文档:
    • D1483 解释了模块化构建过程的外观以及为何比非模块化构建更加困难。这是必读内容。
    • CMake 实验性功能指南 记载了当前实验性实现在 CMake 中的限制。如果某些功能无法按预期工作,请首先查看此处。
    • 熟悉模块化的基本功能集和术语。Daniela Engert 的演讲 是一份很好的介绍材料。
    • 阅读 Kitware 的博文 获取其他未在本回答中涵盖的信息,例如如何使用自定义版本的 gcc 尝试模块化。
  • 在构建过程中,工具将生成一堆.json文件,其中包含构建系统用于跟踪模块依赖关系的数据。如果某些功能无法按预期工作,这些文件对于调试非常有用。
  • 目前不建议在生产环境中使用任何这些功能!请记住,这是一个实验性功能,主要是为了让编译器和工具的实现者能够解决问题。

在CMake中启用模块支持

由于CMake对模块的支持目前仍处于实验阶段,您需要选择启用此功能才能使用:

cmake_minimum_required(VERSION 3.27)

project(my_modules_project)

set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API aa1f7df0-828a-4fcd-9afc-2dc80491aca7)
set(CMAKE_CXX_STANDARD 20)

这里的最后一行是可选的,但如果您不请求C++20支持,您的编译器可能会拒绝编译使用模块的代码。请注意,C++20不包含标准库的模块化版本,您至少需要C++23。

设置CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API激活模块支持。每个CMake发布版本的CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API的魔术数字都会改变,因此如果CMake报错未识别与模块相关的命令,请务必双重检查您的CMake发布版的文档

使用模块

模块源文件需要使用CMake的target_sources命令的FILE_SET特性来指定。如果您还不了解该特性,它对于没有模块的库也非常有用,所以请查看一下。

模块源文件通过属于特殊的CXX_MODULES文件集合来区分于普通源文件:

add_executable(my_app)
target_sources(my_app PRIVATE
  FILE_SET all_my_modules TYPE CXX_MODULES
  BASE_DIRS
    ${PROJECT_SOURCE_DIR}
  FILES
    a.cppm
    b.cppm
)
target_sources(my_app PRIVATE
  main.cpp
)

这里的 a.cppmb.cppm 是模块源文件,可以使用 C++20 模块的 export 关键字。相比之下,main.cpp 可以使用 import 关键字,但不能使用 export 关键字。区别在于 FILE_SET,而不是文件扩展名!在这里我们仅仅使用 .cppm 作为模块源文件的示例。

请注意,如果您的源文件是一个模块实现单元,它必须包含在 CXX_MODULES 文件集中!您也不应使用像 .cppm.ixx 这样的模块风格文件扩展名,而应该使用普通的 .cpp 作为这些文件的扩展名,因为某些编译器可能会将这些文件视为模块接口单元,这会破坏您的构建。

头文件单元目前在任何地方都不受支持(无论是CMake还是任何主要的构建系统),对于这个特性的可实现性存在严重的担忧。Daniel Ruoso在C++Now 2023上做了一个精彩演讲视频)来解释这些问题。你现在应该坚持使用命名模块。

一个完整的工作示例

你也可以在Github上找到这个示例。

// a.cppm
module;

#include <iostream>

export module MyModule;

int hidden() {
    return 42;
}

export void printMessage() {
    std::cout << "The hidden value is " << hidden() << "\n";
}

// main.cpp
import MyModule;

int main() {
    printMessage();
}

# CMakeLists.txt
cmake_minimum_required(VERSION 3.27)

set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API aa1f7df0-828a-4fcd-9afc-2dc80491aca7)

project(modules-example)

set(CMAKE_CXX_STANDARD 20)

add_executable(demo)
target_sources(demo
    PUBLIC
    main.cpp
)
target_sources(demo
  PUBLIC
    FILE_SET all_my_modules TYPE CXX_MODULES FILES
    a.cppm
)

如果你正确设置了一切,CMake 在配置阶段应该会给出以下警告:
CMake Warning (dev) at CMakeLists.txt:??? (target_sources):
  CMake's C++ module support is experimental.  It is meant only for
  experimentation and feedback to CMake developers.
This warning is for project developers.  Use -Wno-dev to suppress it.

项目在更改源文件时应该仍然能够正常构建和重新构建。如果出现错误提示,请注意以下事项。
target_sources File set TYPE may only be "HEADERS"

这意味着你的CMake版本过旧,或者你没有正确设置模块支持。在这种情况下,请仔细检查CMake的文档

7
过去18个月里有什么变化吗? - Lothar
@Lothar MSVC现在在16.8及更高版本上提供了比以前更强大的模块实现。然而,这个版本目前仍处于预览阶段,不在主要发布渠道上。目前还不清楚该实现是否已经提供了与CMake适当的构建系统集成所需的一切。总的来说,由于该功能对整个构建过程的深远影响,我预计还需要几年时间(!)才能解决这个问题,使其能够用于生产代码库。 - ComicSansMS
4
现在已经过去了三年半,仍然没有对模块的支持,一切仍处于试验阶段(参考 https://gitlab.kitware.com/cmake/cmake/-/issues/18355)。如此遗憾。 - Lothar
1
请注意,从2023年6月1日起,CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP标志不再必要,并已从CMake中删除此处 - Splines
这个配置对我来说不太适用,很遗憾。如果我添加 add_compile_options(-fmodules),它就会停止报错说找不到模块,但不幸的是,clang-scan-deps 会崩溃(完全崩溃并输出堆栈转储),除非我还添加 add_compile_options(-fno-implicit-module-maps)...老实说,我真的不太清楚这样做的后果,关于它的信息似乎很少而且晦涩难懂,甚至有时相互矛盾,但至少在这种非常简单的测试中似乎是有效的。 - undefined
显示剩余6条评论

26
这适用于Linux Manjaro(与Arch相同),但应该适用于任何Unix操作系统。当然,您需要使用新的clang进行构建(已测试使用clang-10)。
helloworld.cpp:
export module helloworld;
import <cstdio>;
export void hello() { puts("Hello world!"); }

主函数.cpp:

import helloworld;  // import declaration

int main() {
    hello();
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(main)

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})
    add_custom_target(${name}.pcm
            COMMAND
                ${CMAKE_CXX_COMPILER}
                -std=c++20
                -stdlib=libc++
                -fmodules
                -c
                ${CMAKE_CURRENT_SOURCE_DIR}/${ARGN}
                -Xclang -emit-module-interface
                -o ${PREBUILT_MODULE_PATH}/${name}.pcm

            )
endfunction()


add_compile_options(-fmodules)
add_compile_options(-stdlib=libc++)
add_compile_options(-fbuiltin-module-map)
add_compile_options(-fimplicit-module-maps)
add_compile_options(-fprebuilt-module-path=${PREBUILT_MODULE_PATH})

add_module(helloworld helloworld.cpp)
add_executable(main
        main.cpp
        helloworld.cpp
        )
add_dependencies(main helloworld.pcm)


为什么需要将 helloworld.cpp 添加到 main 可执行文件的源代码中? - Luka Govedič
你需要为那个cpp创建目标文件(.o),并将其与其他目标文件链接在一起。 - warchantua
使用CMake,您可能会添加另一个函数,该函数将从模块cpp文件创建OBJECT库,并将它们添加到可执行文件中,但这超出了本答案的范围。 - warchantua
在Manjaro上使用Clang 13尝试后,得到了以下结果: C++20‘import’仅在启用‘-fmodules-ts’时可用,而‘-std=c++20’尚未启用 此外,-stdlibm、-fmodules和-Xclang都是无法识别的编译器选项。 - Alex Vergara
对于任何对在JetBrains的Clion中使用它感到好奇的人,使用上述配方,在我的M1 Mac上编译和运行代码(使用homebrew llvm)是可行的,但是Clion无法理解模块并且出现了各种警告。不过这是一个已知的问题。 - marital_weeping
如何分离模块接口和实现? @warchantua - ideal

14

假设您正在使用gcc 11和Makefile生成器,即使没有CMake支持C++20,以下代码也应该可以正常工作:

Assuming that you're using gcc 11 with a Makefile generator, the following code should work even without CMake support for C++20:

cmake_minimum_required(VERSION 3.19) # Lower versions should also be supported
project(cpp20-modules)

# Add target to build iostream module
add_custom_target(std_modules ALL
    COMMAND ${CMAKE_COMMAND} -E echo "Building standard library modules"
    COMMAND g++ -fmodules-ts -std=c++20 -c -x c++-system-header iostream
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)

# Function to set up modules in GCC
function (prepare_for_module TGT)
    target_compile_options(${TGT} PUBLIC -fmodules-ts)
    set_property(TARGET ${TGT} PROPERTY CXX_STANDARD 20)
    set_property(TARGET ${TGT} PROPERTY CXX_EXTENSIONS OFF)
    add_dependencies(${TGT} std_modules)
endfunction()

# Program name and sources
set (TARGET prog)
set (SOURCES main.cpp)
set (MODULES mymod.cpp)

# Setup program modules object library
set (MODULE_TARGET prog-modules)
add_library(${MODULE_TARGET} OBJECT ${MODULES})
prepare_for_module(${MODULE_TARGET})

# Setup executable
add_executable(${TARGET} ${SOURCES})
prepare_for_module(${TARGET})

# Add modules to application using object library
target_link_libraries(${TARGET} PRIVATE ${MODULE_TARGET})

一些解释:

  1. 添加自定义目标以构建标准库模块,如果想要包含标准库头文件单元,则可以查找这里的“标准库头文件单元”。为简单起见,在此处仅添加了iostream
  2. 接下来,添加一个函数以方便为目标启用C++20和模块TS
  3. 首先创建一个对象库,用于构建用户模块
  4. 最后,创建可执行文件并将其链接到上一步创建的对象库。

不考虑以下main.cpp

import mymod;

int main() {
    helloModule();
}

mymod.cpp

module;
export module mymod;

import <iostream>;

export void helloModule() {
    std::cout << "Hello module!\n";
}

使用上面的CMakeLists.txt,你的示例应该可以编译成功(在Ubuntu WSL上使用gcc 1.11.0测试成功)。

更新:有时当更改CMakeLists.txt并重新编译时,可能会遇到错误。

error: import "/usr/include/c++/11/iostream" has CRC mismatch

可能的原因是每个新模块都会尝试构建标准库模块,但我不确定。不幸的是,我没有找到一个合适的解决方案(如果想添加新的标准模块,则避免在 gcm.cache 目录已经存在时重新构建是不好的,而对于每个模块进行这样的操作则会给维护带来困难)。我的简单粗暴的解决方案是删除 ${CMAKE_BINARY_DIR}/gcm.cache 并重新构建模块。不过,如果有更好的建议,我也很乐意听取。


值得一提的是:它可以与makefiles生成器一起使用,但不能与nonja一起使用——会显示错误“inputs may not also have inputs”。 - Mariusz Jaskółka
1
关于头文件单元和 CRC 不匹配问题:您可以使用 -fmodule-header 创建一个大的头文件单元(称其为“std.h”或其他名称),其中包含所有标准库,并导入它,而不是常规的头文件。 - unddoch
太完美了!谢谢。在Ubuntu 22上使用GCC 12.1毫无问题。 - ervinbosenbacher

4

2

在等待CMake支持适当的C++20模块时,我发现如果使用MSVC Windows,你可以通过在构建过程中进行一些小改动而不是在CMakeLists.txt中进行修改来假装它已经存在:使用最新的VS生成器进行连续生成,并使用VS2020打开/构建.sln文件。IFC依赖链会被自动处理 (import <iostream>; 就能工作了)。我还没有尝试过Windows clang或交叉编译。虽然这并不理想,但至少可以算是另一个相当可行的替代方案,到目前为止。

重要的后期思考: 使用 .cppm 和 .ixx 扩展名。


2

C++20的模块化编译需要考虑文件编译顺序,这是全新的特性。因此实现过程较为复杂,目前仍处于实验阶段(2023年)。请参考作者的博客文章


虽然不是对问题的回答,但仍然有帮助。 - LudvigH

1

添加 MSVC 版本(根据 @warchantua 的回答进行修订):

cmake_minimum_required(VERSION 3.16)

project(Cpp20)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(PREBUILT_MODULE_DIR ${CMAKE_BINARY_DIR}/modules)
set(STD_MODULES_DIR "D:/MSVC/VC/Tools/MSVC/14.29.30133/ifc/x64") # macro "$(VC_IFCPath)" in MSVC

function(add_module name)
    file(MAKE_DIRECTORY ${PREBUILT_MODULE_DIR})
    add_custom_target(${name}.ifc
            COMMAND
                ${CMAKE_CXX_COMPILER}
                /std:c++latest
                /stdIfcDir ${STD_MODULES_DIR}
                /experimental:module
                /c
                /EHsc
                /MD
                ${CMAKE_CURRENT_SOURCE_DIR}/${ARGN}
                /module:export
                /ifcOutput
                ${PREBUILT_MODULE_DIR}/${name}.ifc
                /Fo${PREBUILT_MODULE_DIR}/${name}.obj
            )
endfunction()

set(CUSTOM_MODULES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/modules)
add_module(my_module ${CUSTOM_MODULES_DIR}/my_module.ixx)

add_executable(test
    test.cpp
    )
target_compile_options(test
    BEFORE
    PRIVATE
    /std:c++latest
    /experimental:module
    /stdIfcDir ${STD_MODULES_DIR}
    /ifcSearchDir ${PREBUILT_MODULE_DIR}
    /reference my_module=${PREBUILT_MODULE_DIR}/my_module.ifc
    /EHsc
    /MD
)
target_link_libraries(test ${PREBUILT_MODULE_DIR}/my_module.obj)
add_dependencies(test my_module.ifc)

请问您能否解释一下如何在目录中设置文件?我正在按照您的CMakeLists进行操作,但出现了奇怪的MSVC 2022编译器错误,例如找不到“STD_MODULES_DIR”和使用“/module:export”需要“/experimental:module”,而这已经在CMake文件中了! - Alex Vergara
只有在项目中使用标准库模块(例如 std.core)时,才需要使用 STD_MODULES_DIR。如果使用 #include 指令,则无需添加此目录和 stdIfcDir 编译选项。 - Harold
也许你最好首先尝试使用命令行中的模块(这是一个不错的教程),我的演示 CMakeLists 只是将命令行命令包装成了 cmake。 - Harold
我删除了/stdIfcDir和/ifcSearchDir,现在一切正常。而且我可以使用import std.core或者其他内容,不需要这些编译选项,因为它们对我没有用。我正在使用2022 MSVC编译器,但是正如我所说的,现在我可以完全编译我的项目。 - Alex Vergara

1

CMake目前不支持C++20模块,正如其他人所述。但是,Fortran的模块支持非常相似,也许可以很容易地改为支持C++20中的模块。

http://fortranwiki.org/fortran/show/Build+tools

也许有一种简单的方法可以直接支持C++20进行修改。不确定。值得探索,如果您解决了这个问题,应该提交拉取请求。


1

我无法找到适用于模块的Cmake支持。以下是使用clang使用模块的示例。我正在使用Mac,此示例在我的系统上运行良好。我花了相当长的时间才弄清楚这一点,因此不确定在Linux或Windows中是否普遍适用。

驱动程序代码在文件driver.cxx中。

import hello;
int main() { say_hello("Modules"); } 

源代码在文件 hello.cxx 中

#include <iostream>
module hello;
void say_hello(const char *n) {
  std::cout << "Hello, " << n << "!" << std::endl;
}

源代码在文件 hello.mxx 中

export module hello;
export void say_hello (const char* name);

为了使用上述源文件编译代码,请在终端中输入以下命令行

clang++ \
  -std=c++2a                        \
  -fmodules-ts                      \
  --precompile                      \
  -x c++-module                     \
  -Xclang -fmodules-embed-all-files \
  -Xclang -fmodules-codegen         \
  -Xclang -fmodules-debuginfo       \
  -o hello.pcm hello.mxx

clang++ -std=c++2a -fmodules-ts -o hello.pcm.o -c hello.pcm

clang++ -std=c++2a -fmodules-ts -x c++ -o hello.o \
  -fmodule-file=hello.pcm -c hello.cxx

clang++ -std=c++2a -fmodules-ts -x c++ -o driver.o \
  -fmodule-file=hello=hello.pcm -c driver.cxx

clang++ -o hello hello.pcm.o driver.o hello.o

并在下一次编译时获得干净的起点

rm -f *.o
rm -f hello
rm -f hello.pcm

期望输出

./hello
Hello, Modules!

希望这能有所帮助,祝一切顺利。


16
这是一个很好的回答,但不适用于这个问题。你可以考虑提出一个问题("使用模块构建C++程序的简单示例?"),并将其与此回答一起发布。 - einpoklum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接