c++20模块需用export显式导出符号,export module必须为首行非注释语句;导入用import实现编译器级链接,非文本替换;构建系统须显式支持.ifc/.pcm生成与引用,否则无法解析符号。

模块怎么写、怎么导出符号
C++20 模块不是“把头文件改个后缀”,它要求显式声明哪些名字对外可见。export 是核心动作,没加它,哪怕定义在模块文件里,别的单元也看不到。
-
export module 必须是文件第一行非空非注释行(否则编译器直接报错 module declaration must appear at global scope)
- 只有被
export 修饰的声明才可被导入:比如 export struct Foo { int x; }; 可以,但 struct Bar { ... }; 不行,哪怕它在同一个 .ixx 文件里
- 模块接口文件通常用
.ixx 后缀(MSVC)或 .mpp(Clang),但后缀本身不决定语义,靠 export module 识别
- 如果要导出多个声明,可以包在
export { ... } 块里,避免重复写 export
// math.ixx
export module math;
export import <cmath>;
<p>export namespace math {
export const double PI = 3.14159;
export int add(int a, int b) { return a + b; }
}</p></cmath>
模块怎么导入、和 #include 有什么区别import 不是文本替换,不触发预处理,也不依赖包含路径;它是编译器级的符号链接。这意味着:
-
import 语句不能放在头文件里(模块不能“嵌套导入”到头文件中再被 #include)
- 导入顺序无关,多次
import math; 不会重复编译,也不需要 #pragma once 或 include guard
- 不能用
import "path/to/file.h" 导入传统头文件(除非用 import <stdio.h></stdio.h> 这种头单位模块,且需编译器支持)
- MSVC 要求模块接口文件先编译成二进制模块接口文件(
.ifc),Clang 使用 .pcm,必须确保构建系统生成并引用了它们
export module 必须是文件第一行非空非注释行(否则编译器直接报错 module declaration must appear at global scope)export 修饰的声明才可被导入:比如 export struct Foo { int x; }; 可以,但 struct Bar { ... }; 不行,哪怕它在同一个 .ixx 文件里.ixx 后缀(MSVC)或 .mpp(Clang),但后缀本身不决定语义,靠 export module 识别export { ... } 块里,避免重复写 export
import 不是文本替换,不触发预处理,也不依赖包含路径;它是编译器级的符号链接。这意味着:
-
import语句不能放在头文件里(模块不能“嵌套导入”到头文件中再被#include) - 导入顺序无关,多次
import math;不会重复编译,也不需要#pragma once或 include guard - 不能用
import "path/to/file.h"导入传统头文件(除非用import <stdio.h></stdio.h>这种头单位模块,且需编译器支持) - MSVC 要求模块接口文件先编译成二进制模块接口文件(
.ifc),Clang 使用.pcm,必须确保构建系统生成并引用了它们
常见错误现象:error: 'add' was not declared in this scope,往往是因为忘了 export,或者模块文件根本没参与编译(构建系统没配置模块编译规则)。
构建系统怎么配才跑得起来
C++20 模块不是“开个编译器开关就自动支持”的功能。它依赖编译器对模块接口的生成与消费链路,而主流构建系统原生支持仍较弱。
- CMake 3.28+ 才有实验性
cmake_language(DEFER) 和 add_module() 支持,但实际项目中更稳妥的是用 add_library(... INTERFACE) + 自定义命令调用编译器生成 .ifc
- Clang 需要分两步:先用
clang++ -x c++-system-header -std=c++20 -fmodules -emit-module-interface -o math.pcm math.ixx 生成模块接口,再在主文件里 import math; 并用 -fprebuilt-module-path=. 指向它
- MSVC 要求
/interface 编译选项生成 .ifc,并用 /reference math.ifc 导入;且模块文件必须在源文件列表中显式列出,不能靠 glob 匹配
- 混用模块和头文件时,
#include 的头文件无法看到模块导出的符号(反之亦然),二者符号域隔离——这不是 bug,是设计使然
什么时候不该急着切模块
模块解决的是头文件带来的 ODR、宏污染、编译依赖爆炸问题,但现阶段落地成本高:
- 第三方库几乎全是头文件形式,你写了模块,但依然得
#include <json.hpp></json.hpp>,没法统一
- 模块接口文件不能含宏定义(
export 不作用于宏),所有跨模块的配置宏还得靠传统方式传递
- 调试信息支持弱:GDB/LLDB 对模块符号的展开、断点设置不如头文件稳定
- Windows 上 MSVC 的模块二进制格式不兼容 Clang,跨工具链协作基本不可行
cmake_language(DEFER) 和 add_module() 支持,但实际项目中更稳妥的是用 add_library(... INTERFACE) + 自定义命令调用编译器生成 .ifc
clang++ -x c++-system-header -std=c++20 -fmodules -emit-module-interface -o math.pcm math.ixx 生成模块接口,再在主文件里 import math; 并用 -fprebuilt-module-path=. 指向它/interface 编译选项生成 .ifc,并用 /reference math.ifc 导入;且模块文件必须在源文件列表中显式列出,不能靠 glob 匹配#include 的头文件无法看到模块导出的符号(反之亦然),二者符号域隔离——这不是 bug,是设计使然- 第三方库几乎全是头文件形式,你写了模块,但依然得
#include <json.hpp></json.hpp>,没法统一 - 模块接口文件不能含宏定义(
export不作用于宏),所有跨模块的配置宏还得靠传统方式传递 - 调试信息支持弱:GDB/LLDB 对模块符号的展开、断点设置不如头文件稳定
- Windows 上 MSVC 的模块二进制格式不兼容 Clang,跨工具链协作基本不可行
真正适合模块的场景其实是内部基础库重构:比如把公司自研的容器、日志、序列化模块化,控制导出粒度,切断隐式依赖。别一上来就想替代 vector 或 string。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
模块不是语法糖,是编译模型的重写。你得先让构建系统认得清 .ifc 和 .pcm,再谈怎么组织接口。否则连 import 都报找不到模块,不是代码写错了,是构建漏了一环。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!









