如何将 c++20 模块与 CMake 一起使用
How to use c++20 modules with 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.cppm
和b.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})
一些解释:
添加- 自定义目标以构建标准库模块,以防您想要包含标准库标头单元(在此处搜索"标准库标头单元")。为简单起见,我只是在这里添加了
iostream
。 - 接下来,添加一个功能,以方便地为目标启用C++20和模块 TS
- 我们首先创建一个对象库来构建用户模块
- 最后,我们创建可执行文件并将其链接到上一步中创建的对象库。
不考虑以下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)
- 将--whole archive链接器选项与CMake和具有其他库依赖项的库一起使用
- 如何将GTest与CMake一起使用?遵循谷歌指南时的链接问题
- 如何将动态链接库与CMake一起使用
- SSE 标志应该如何与现代 CMake 一起添加?
- 将外部C++库与 CMake 一起使用时出错
- 如何将 Pybind11 与 CMAKE 一起使用以链接 2 个模块
- 如何将 c++20 模块与 CMake 一起使用
- 如何将编译的 wxwidgets 与 cmake 一起使用
- 当它们不与GCC和CMAKE一起使用时,如何解析未定义的引用
- 如何将库与CMAKE一起包含在OSX捆绑包中
- 当将QT创建者与CMAKE一起使用时,为什么我不必手动链接MSVC库
- 将特征与cmake一起使用
- 在将QT与Cmake一起使用时解决链接错误
- 如何将外部库与 CMake 一起使用
- 如何与CMAKE一起使用VS 2017编译的Boost Filesystem库
- 如何将GCOV与Cmake一起使用
- 如何将OpenCV的测试框架与CMake一起使用?
- 如何将PJSIP库与CMake一起使用
- 如何将该工具与 CMake 一起使用'Include What You Use'来检测未使用的标头?
- 将预编译的头与CMake一起使用