Clang 和 MSVC 已经支持来自未完成的 C++20 标准的 Modules TS。
我可以使用 CMake 或其他构建系统构建我的模块项目吗?如何操作呢?
这反映了CMake 3.27的状态。
随着情况的变化,我会不断更新这个答案。
重要提示:CMake对于C++20模块的支持目前处于功能性阶段,但仍然是实验性的。在某些情况下可能正常工作,但在其他情况下可能会出现问题。请预期版本之间存在错误和破坏性变更! 请参考CMake问题跟踪器中相关问题。
请注意,支持模块需要构建系统提供比插入新的编译器选项更多的支持。它从根本上改变了在构建过程中处理源文件之间依赖关系的方式:在没有模块的世界中,所有cpp源文件可以以任意顺序独立构建。而使用模块后,这种情况不再成立,这不仅对CMake本身有影响,也对下游构建系统有影响。
详细了解CMake Fortran模块论文中的技术细节。从构建系统的角度来看,Fortran的模块行为与C++20模块非常相似。
正确的集成目前仅适用于以下生成器:
模块依赖扫描当前支持以下编译器:
请确保您的编译器和构建系统都足够更新!
注意:要使 Clang 正常工作,至少需要 Clang 版本 16,并且可能还需要对 CMAKE_EXPERIMENTAL_CXX_SCANDEP_SOURCE
进行一些调整。由于这涉及到工具内部的深层操作,我在此回答中将不涉及该内容!如果您想尝试,请查看CMake 的功能指南。这个问题应该会在未来的 CMake 版本中得到解决。
.json
文件,其中包含构建系统用于跟踪模块依赖关系的数据。如果某些功能无法按预期工作,这些文件对于调试非常有用。由于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.cppm
和 b.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 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"
add_compile_options(-fmodules)
,它就会停止报错说找不到模块,但不幸的是,clang-scan-deps
会崩溃(完全崩溃并输出堆栈转储),除非我还添加 add_compile_options(-fno-implicit-module-maps)
...老实说,我真的不太清楚这样做的后果,关于它的信息似乎很少而且晦涩难懂,甚至有时相互矛盾,但至少在这种非常简单的测试中似乎是有效的。 - undefinedexport 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čC++20‘import’仅在启用‘-fmodules-ts’时可用,而‘-std=c++20’尚未启用
此外,-stdlibm、-fmodules和-Xclang都是无法识别的编译器选项。 - Alex Vergara假设您正在使用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})
一些解释:
iostream
。不考虑以下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
并重新构建模块。不过,如果有更好的建议,我也很乐意听取。
-fmodule-header
创建一个大的头文件单元(称其为“std.h”或其他名称),其中包含所有标准库,并导入它,而不是常规的头文件。 - unddoch在等待CMake支持适当的C++20模块时,我发现如果使用MSVC Windows,你可以通过在构建过程中进行一些小改动而不是在CMakeLists.txt中进行修改来假装它已经存在:使用最新的VS生成器进行连续生成,并使用VS2020打开/构建.sln文件。IFC依赖链会被自动处理 (import <iostream>;
就能工作了)。我还没有尝试过Windows clang或交叉编译。虽然这并不理想,但至少可以算是另一个相当可行的替代方案,到目前为止。
重要的后期思考: 使用 .cppm 和 .ixx 扩展名。
C++20的模块化编译需要考虑文件编译顺序,这是全新的特性。因此实现过程较为复杂,目前仍处于实验阶段(2023年)。请参考作者的博客文章。
添加 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)
std.core
)时,才需要使用 STD_MODULES_DIR
。如果使用 #include
指令,则无需添加此目录和 stdIfcDir
编译选项。 - Haroldstd.core
或者其他内容,不需要这些编译选项,因为它们对我没有用。我正在使用2022 MSVC编译器,但是正如我所说的,现在我可以完全编译我的项目。 - Alex VergaraCMake目前不支持C++20模块,正如其他人所述。但是,Fortran的模块支持非常相似,也许可以很容易地改为支持C++20中的模块。
http://fortranwiki.org/fortran/show/Build+tools
也许有一种简单的方法可以直接支持C++20进行修改。不确定。值得探索,如果您解决了这个问题,应该提交拉取请求。
我无法找到适用于模块的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!
希望这能有所帮助,祝一切顺利。
CXX_STANDARD
的组合,以及一个 CMake if 语句来检查 Clang 或 MSVC,并根据你的编译器添加相应的编译器标志。 - Kevin-std=c++2a -fmodules-ts
选项使用Clang编译代码,但是它报错说致命错误:找不到模块“VulkanRender”。我该如何告诉Clang我的模块存储在哪里? - Blaze