前言
C++20 引入了模块功能,用于优化大型项目编译。模块可以加快编译速度、避免宏污染、解决部分ODR问题。 而 C++23 引入了标准库模块,但截止至文章发布/修改时(2025年8月)依然是实验性支持。
本文章将介绍如何在CMake项目中使用标准库功能,以及一些使用模块时的注意事项。
项目结构
现在创建一个目录,内容如下所示:
项目根目录/
│
├── CMakeLists.txt
│
└── main.cpp
然后编写一个最简单的 main.cpp 代码:
import std;
int main() {
std::println("Hello, C++23 STD Module!");
}
CMake 配置
C++23 模块支持需要较新的 CMake 版本,建议 4.0.0 及以上。
相关事项可以参考 CMake 关于模块支持的 文档 。简而言之,我们需要设置至少四个参数:
- 生成器设为
Ninja(生成配置时指定) - C++ 版本设为 23
- 设置 STD Module 的实验性 UUID
- 设置
CXX_MODULE_STD或CMAKE_CXX_MODULE_STD变量
所以我们的 CMakeLists.txt 可以这样写:
# 最小版本号,自行调整
cmake_minimum_required(VERSION 4.0.3)
# 实验性 UUID,需与 CMake 版本对应,请前往 Gitlab 或 Github 仓库查看。
# 例如,对于 CMake v4.1.0,对应的文档网址如下:
# https://gitlab.kitware.com/cmake/cmake/-/blob/v4.1.0/Help/dev/experimental.rst
set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD ".............")
project(hello_std_module)
# 设置 C++ 版本为 23
set(CMAKE_CXX_STANDARD 23)
# 启用标准库模块
set(CMAKE_CXX_MODULE_STD 1)
add_executable(main main.cpp)
# 如果不喜欢全局变量,也可以使用目标参数设置
# set_target_properties(main PROPERTIES
# CXX_MODULE_STD 1
# CXX_STANDARD 23
# )
注意事项
一、对于实验性 UUID 和 CMAKE_CXX_STANDARD 等变量的具体含义,请访问上方给出的官方
链接,
在 “C++ import std support” 子栏目中有相关说明。
二、实验性 UUID 与 CMake 版本挂钩,不要直接复制 main 分支下的 UUID 。
请选择与 CMake 版本匹配的 Tag 下的 UUID 。上面给出的链接是 v4.1 的,大概率与你的 CMake 不匹配。
你可以在本地通过 cmake --version 命令查看本地 CMake 版本。
三、CMAKE_EXPERIMENTAL_CXX_IMPORT_STD 变量必须在 project() 函数之前设置。
若不想硬编码在 CMake 配置中,也可以使用 CMake 预设或者在命令行使用 -D 附加参数。
四、截止本文的最新一次修改(2025年8月),各编译期的支持性依然不佳,因此有如下建议:
- 对于 Windows,推荐使用 MSVC 工具链,或 LLVM+MinGW 。独立的 MinGW 和 LLVM+MSVC 无法正常工作。
- 对于 Linux,可以直接使用 GNU/GCC 。如果使用 LLVM,可能需要
set(CMAKE_CXX_FLAGS "-stdlib=libc++")指令显式使用 Clang 自身的标准库。
编译与运行
完成上面的准备工作后,就可以生成 CMake 配置了。需要再生成时指定使用 Ninja 生成器。
cmake -B build -G "Ninja"
你可能看到类似下面的内容:
CMake Warning (dev) at ......
CMake's support for import std; in C++23 and newer 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.
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
虽然它发出了警告,表示模块是实验性的,但构建已经成功。
然后可以生成目标文件:
cmake --build build
应该没有任何报错,最后运行:
# Windows
./build/main.exe
# Linux/MacOS
./build/main
你应该能看到正常的打印输出:
Hello, C++23 STD Module!
使用 CMake 预设
CMake 预设可以与 VSCode / Visual Studio / CLion 进行很好的集成,从而无需手动输入命令进行配置和编译。
如果需要 CMake 预设模块,请参考 这篇文章 。
此预设提供了 GCC、MSVC、Clang 的 CMake 基础配置,包括 Clang 可能需要的 -stdlib=libc++ 参数。
关于编译工具
最新编译器与相关工具通常无法通过包管理器安装,下面给出部分安装参考。
Ninja
Ninja 更新频率不高,Linux 中直接使用 apt 等包管理器安装即可。 Window 可以去 官网 下载编译好的文件然后设置环境变量。
CMake
建议使用高版本 CMake,本文档使用 4.0.3 ,建议 官网 或仓库拉取编译好的文件。
Windows 直接下载 MSI 安装程序,或下载压缩包并设置环境变量。
Linux 同理,最简单的方式是命令拉取 xxx.sh 安装脚本并执行。
MSVC 工具链
使用 Visual Studio Installer , 直接安装社区预览版的 Visual Studio,勾选C++模块相关支持。
Clang
Clang 在 Windows 上默认使用 MSVC 的标准库,但 Clang 启用标准库模块要求 libc++ 或 libstdc++ ,因此无法在 Windows 上正常工作(2025年8月)。
libc++ 是 Clang 自己的标准库,不支持 Windows 。如果要在 Windows 上使用 Clang,需使用 LLVM-MinGW 。
Clang 在 Linux 下可以正常工作,推荐使用官方脚本安装,下载 llvm.sh :
wget https://apt.llvm.org/llvm.sh
# 或者
curl -O https://apt.llvm.org/llvm.sh
然后给予文件可执行权限并运行脚本,此处指定安装 Clang 20 的全部内容,你可以选择更高版本:
chmod +x llvm.sh
sudo ./llvm.sh 20 all
然后可以检查 clang 版本,验证是否安装成功:
clang++ --version
如果提示找不到 clang 和 clang++ ,可能需要设置软链接:
sudo ln -s /usr/lib/llvm-20/bin/clang-20 /usr/bin/clang
sudo ln -s /usr/lib/llvm-20/bin/clang++ /usr/bin/clang++
GCC
截止文章更新时间(2025年8月),Windows 的 MinGW 对标准库的支持并不好,不要使用 MinGW 。
Linux 上的安装请参考 [xim+]: 最新gcc15.1.0发布, 一键从源码构建 — c++23 import std启动。
关于编辑器
Visual Studio 或 CLion 都可对 C++ 23 以及 CMake 预设提供了较好的支持,但二者都较为笨重。
如果使用 MSVC 工具链,可以选择 Visual Studio 。其他情况更推荐 CLion 。
VSCode 对 CMake 的支持良好,但 C++ 插件的智能感知对 C++ 模块的支持并不好。 需要让 CMake 导出编译命令,让 VSCode 的 C++ 插件读取,才能正常进行语法检查。 首先在 CMakeLists.txt 中增加代码:
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
这样会在构建目录中生成一个文件,包含编译相关的所有命令。
然后在项目中创建 .vscode/settings.json 文件,并添加内容:
{
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json"
}
然后重新生成配置文件并编译即可(可能需要重启 vscode )。 遗憾的是,这很可能只对Debug模式生效,Release模式可能依然无法正确进行智能感知。
作者没有尝试过使用 clangd 作为 VSCode 的智能感知插件,你可以自行尝试。
混用模块与头文件
模块文件与头文件在项目中同时出现时很容易有一些问题,尤其是 C++23 后同时具有标准库模块和标准库头文件。 很多时候我们不得不用第三方库,这些库往往没有提供模块化的版本,我们必须兼容这些头文件和我们的模块文件同时使用。
后续“模块文件”特指“模块接口文件”,即 .cppm/.ixx 文件。而“模块实现文件”属于 .cpp 文件。
作者建议你的代码满足如下要求,这可以很大程度上避免头文件与模块的冲突问题:
普通源文件
在 .cpp 文件中,请保证先导入(所有)头文件、后导入(所有)模块:
#include <iostream>
#include ...... // 先导入各种头文件
import std; // 最后导入标准库模块和其他模块
// 可通过编译
如果先导入标准库模块再导入头文件,就会编译失败:
import std;
#include <iostream> // 编译失败,存在重定义问题
模块接口文件与实现文件
对于 .cppm/.ixx 模块接口文件,请将所有头文件都放在全局模块片段中,将模块导入放在正常的声明部分:
module;
// 全局模块片段 导入任何需要的头文件,包括第三方库的头文件
#include <iostream>
export module MyModule;
import std; // 导入其他需要的模块
...... // 正常模块内容
模块实现文件通常也是 .cpp 文件,但它也有全局模块片段,因此导入方式与接口类似:
module;
#include <......> // 先在全局模块片段导入各种头文件
module XXX; // 模块实现文件的模块声明
import std;// 最后导入标准库模块和其他模块
import ...;
/* 模块中函数的实现代码 */
// ......
分离声明与实现
虽然模块优化了头文件机制,让你可以把实现和声明代码全部放在一个模块文件中,但依然推荐你将二者分离。
作者推荐的做法是:将模板、声明和少量内联函数放在 .cppm/.ixx 模块文件中,将函数实现放在 .cpp 文件中,将宏定义放在头文件中。
原因是 .cppm/.ixx 模块接口文件需要按照依赖顺序编译,一个模块文件被修改后,所有依赖它的文件都需要被重新编译一遍,顺序编译又导致整体的编译耗时极高。
.cpp 文件内容的修改不影响模块接口文件,如果你将实现写在 .cpp 文件中,修改了实现代码而未修改声明,则仅需要重新编译此 .cpp 文件(且 .cpp 文件可以轻松并行编译),不需要重新编译大量的模块文件。
举例:
// A.cppm
export module A;
import std;
export struct A{
void say const { std::println("Hello, A."); }
};
// B.cppm
export module B;
export struct B: public A{ };
// main.cpp
import B;
int main() {
B b{};
b.say();
}
如果你像上面一样将函数实现直接写在了模块文件中,功能没有任何问题。
但如果你修改了 say 函数的实现,那么需要先重编译 A.cppm 、然后重编译 B.cppm 、最后重编译 main.cpp ,且必须串行编译,因为后者依赖前者。
而且,在此代码风格中,实现代码常常位于模块接口文件中,代码量大导致编译耗时较长,又必须顺序编译,每次修改后都需要花费很长时间重编译项目。
如果你像下面这样分离了声明与实现:
// A.cppm
export module A;
import std; // optional
export struct A{
void say const;
};
// A.cpp
module A;
import std;
void A::say const { std::println("Hello, A."); }
// B.cppm
export module B;
export struct B: public A{ };
// main.cpp
import B;
int main() {
B b{};
b.say();
}
此时修改 say 函数,它位于 .cpp 文件中,不影响 A.cppm 的模块内容,所以只需要重编译 A.cpp 而无需重编译其他文件。
如果你按照这种方式编写代码,后期修改了某个模块接口文件导致大量链式编译,由于它们只含声明和少量内联代码,编译速度会比较快;而受影响的 .cpp 文件们虽然含大量实现代码、却可以轻松的并行编译,整体编译耗时较短。
注意一点,私有模块片段虽然在设计上属于独立翻译单元,但目前的编译参考文件自身的修改,所以修改私有模块片段等于修改模块接口文件自身,也会造成链式编译。