如何将 c++20 模块与 CMake 一起使用



Clang 和 MSVC 已经支持未完成的 C++20 标准的模块 TS。 我可以使用 CMake 或其他构建系统构建基于模块的项目吗?

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

实验模块支持

这反映了 CMake 3.27 的状态

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

重要提示:CMake 对 C++20 模块的支持目前正常运行,但仍处于实验阶段。在某些情况下,事情可能会起作用,但在其他情况下会中断。预计版本之间会出现错误和重大更改!另请参阅 CMake 问题跟踪器中的相关问题。

请注意,与插入新的编译器选项相比,支持模块需要来自构建系统的更多支持。它从根本上改变了在构建过程中处理源文件之间依赖关系的方式:在预模块世界中,所有 cpp 源文件都可以以任何顺序独立构建。对于不再正确的模块,这不仅对 CMake 本身有影响,而且对下游构建系统也有影响。

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

先决条件

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

  • 忍者版本 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 中当前实验实现的局限性。如果某些内容无法按预期工作,请先检查此处。
    • 熟悉模块的基本功能集和词汇。丹妮拉·恩格特的演讲是一个很好的介绍。
    • 阅读 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_EXPERIMENTAL_CXX_MODULE_CMAKE_API的幻数随每个 CMake 版本而变化,因此如果 CMake 抱怨无法识别与模块相关的命令,请务必仔细检查 CMake 版本的文档。

使用模块

需要使用 CMaketarget_sources命令的FILE_SET功能指定模块源文件。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是可以使用export关键字 C++20 模块的模块源文件。相比之下,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 的文档。

这适用于Linux Manjaro(与Arch相同),但应该适用于任何Unix操作系统。当然,您需要使用新的 clang 进行构建(使用 clang-10 进行测试)。

你好世界.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)

假设您将 gcc 11 与 Makefile 生成器一起使用,即使没有 CMake 对 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和模块
  3. TS
  4. 我们首先创建一个对象库来构建用户模块
  5. 最后,我们创建可执行文件并将其链接到上一步中创建的对象库。

不考虑以下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,您的示例应该可以很好地编译(使用 gcc 1.11.0 在 Ubuntu WSL 中成功测试)。

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

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

原因可能是每个新模块都会尝试构建标准库模块,但我不确定。不幸的是,我没有找到合适的解决方案(如果您想添加新的标准模块,如果gcm.cache目录已经存在,则避免重建是不好的,并且每个模块执行此操作是维护噩梦)。我的Q&D解决方案是删除${CMAKE_BINARY_DIR}/gcm.cache并重建模块。不过,我很高兴有更好的建议。

CMake 附带了对 C++20 模块的实验性支持: https://gitlab.kitware.com/cmake/cmake/-/blob/master/Help/dev/experimental.rst

此问题对此进行了跟踪: https://gitlab.kitware.com/cmake/cmake/-/issues/18355

还有一个CMakeCXXModules存储库,它为CMake添加了对模块的支持。

https://github.com/NTSFka/CMakeCxxModules

在等待 CMake 中适当的 C++20 模块支持时,我发现如果使用 MSVC Windows,现在您可以通过破解构建而不是围绕 CMakeList 来假设它的存在.txt:使用最新的 VS 生成器持续生成,并使用 VS2020 打开/构建.sln。IFC依赖链会自动处理(import <iostream>;正常工作)。还没有尝试过Windows克隆或交叉编译。这并不理想,但到目前为止,今天至少有另一个相当可行的替代方案。

事后重要的想法:使用 .cppm 和 .ixx 扩展名。

对于 C++20 模块,文件编译顺序很重要,这是全新的。这就是为什么实施很复杂并且在 2023 年仍处于实验阶段的原因。请阅读作者的博客文章

我找不到对模块的Cmake支持。下面是如何使用 clang 使用模块的示例。我正在使用Mac,此示例在我的系统上工作正常。我花了相当长的时间才弄清楚这一点,所以不确定这在 Linux 或 Windows 上有多普遍。

文件驱动程序中的源代码.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!

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

CMake 目前不支持 C++20 模块,就像其他人所说的那样。但是,Fortran 的模块支持非常相似,也许这可以很容易地更改为 C++20 中的支持模块。

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

现在,也许我有一个简单的方法来修改它以直接支持 C++20。不确定。如果您解决它,值得探索并执行拉取请求。

添加 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)

最新更新