C++20引入了模块 (modules),模块本质上就是规范化的高配版预编译头文件。但C++单个文件的编译模型依旧没有改变,同时标准也未指定编译器要以什么样的方式处理模块和搜索依赖。所以徒手使用编译器的场景要用上模块,反而比传统编译更复杂了。
本文不讨论任意源码文件的模块化,只关注理论上最应该先模块化的库——C++ std。
尽管模块在C++20进入C++,C++20标准并未包含统一的std模块,只提供了一种替代办法,允许用户以import的方式使用std头文件(称作Header Unit Import):
import <iostream>;
int main() {
std::cout << "Hello, C++ modules!\n";
}
然而,GCC无法直接编译这个程序。要让编译器能够import某个头文件(即使是std头文件),编译器需要先生成它的PCM (Pre-Compiled Module),即换了个名头的头文件:
# GCC
g++ -fmodules -x c++-system-header -c /usr/include/c++/15.2.0/iostream -std=c++20
GCC编译后会在当前位置生成一个gcm.cache目录,其中包括gcm.cache/usr/include/c++/15.2.0/iostream.gcm文件。
现在有了预编译的iostream,上面的程序终于可以编译了:
# GCC
g++ test.cpp -fmodules -std=c++20
# Clang (libc++)
clang++ test.cpp -fmodules -std=c++20
上面预编译的步骤没有提到Clang,因为Clang多做了一步:如果发现模块缓存位置没有预编译的模块,它会自动编译依赖的头文件并缓存起来,然后参与 test.cpp的编译。所以第一次用Clang执行这个命令的时间会很长,而第二次会快很多。Clang有一个默认的模块缓存位置,在~/.cache/clang/ModuleCache,选项-fmodules-cache-path可以使用其他目录。
Clang这种行为称作Implicit Modules,可以用-fno-implicit-modules关闭。这种情况下,和GCC一样,Clang需要手动编译iostream,并且提供一个modulemap,比GCC更加复杂。
GCC有个著名的bits/stdc++.h,包含了所有的libstdc++头文件,要预编译也很简单:
g++ -fmodules -x c++-system-header -c /usr/include/c++/15.2.0/x86_64-linux-gnu/bits/stdc++.h -std=c++20
C++23标准化了这种将所有std头文件放入一个模块的行为,定义了std和std.compat两个模块:std包含了所有标准库的内容,并把所有C标准库继承来的定义都放在std命名空间中;std.compat会将C标准库的符号导出到全局命名空间。
import std;
int main() {
std::cout << "Hello, C++23 modules!\n";
}
不过GCC依然不能直接编译这个程序,因为std模块依然是以源码而非二进制形式提供的。这也合理,因为无论GCC还是Clang,都不保证预编译模块二进制格式的任何稳定性,编译器版本升级或者任何选项改变都可能导致pcm/gcm文件不匹配而报错。libstdc++的std模块源码在头文件目录的bits/std.cc:
# GCC
g++ -std=c++23 -fmodules -fmodule-only /usr/include/c++/15.2.0/bits/std.cc -c
g++ -std=c++23 -fmodules test.cpp
# Clang
clang++ -std=c++23 /usr/share/libc++/v1/std.cppm --precompile
clang++ -std=c++23 main.cpp -fprebuilt-module-path=.
尽管std模块在同一个项目只需要构建一次,但要组织这么多命令依然非常麻烦。使用CMake可以简化使用标准库模块项目的组织:
cmake_minimum_required(VERSION 3.30)
set(CMAKE_CXX_MODULE_STD ON)
set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444")
project(cpp_mod_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS ON)
add_executable(cpp_mod main.cpp)
这里有几点需要注意的地方:
- CMake从3.30开始实验性支持C++ import std,但随着版本变化,CMAKE_EXPERIMENTAL_CXX_IMPORT_STD的正确值也可能变化,直到功能稳定后才会改为ON。对于不同CMake版本的正确值,可以去对应版本的CMake文档里查阅
- Module相关的两个CMake选项必须放在project声明之前,否则CMake会报错
- import std需要C++23
- 目前CMAKE_CXX_EXTENSIONS需要置为ON,因为CMake编译std模块时会使用-std=gnu++23而不是-std=c++23。如果CMAKE_CXX_EXTENSIONS为OFF,编译其他代码时用的是-std=c++23,模块选项不匹配会导致编译错误
由于GCC、CMake、Clang和各大标准库对模块的支持都算不上特别稳定。GCC从11开始支持module,libstdc++从15开始支持import std;Clang从17开始支持module,libc++从 19 开始支持import std。但在使用模块相关功能时,仍然推荐尽量新的版本。
至于MSVC,最新的Visual Studio 2026对C++ module和std module都已经有了较好的支持:
- 在项目属性里,将C++语言标准设置为C++20(如果要import std,则需C++23)
- 在项目属性中「C/C++」下的「语言」选项,将「生成ISO C++23标准库模块」设置为是;或者在「C/C++」的「常规」中将「扫描源以查找模块依赖关系」设置为是
总的来说,模块对IDE环境的C++项目更加友好(尽管Xcode现在也没有很好的支持)。CMake对模块的支持仍有一些限制,比如不支持Makefile作为生成器,也只支持较新的Ninja(依赖Ninja 1.10开始才有的dyndep功能,动态生成依赖),实际上CMake和libc++/libstdc++的通信也是依赖特定JSON文件(如LLVM实现)。真正丝滑的构建体验,恐怕要呼唤「下一代构建系统」才行。
发表回复