CppCon 2015 学习:Large Scale C++ With Modules
先搞一下环境再说
下面是一些例子因为gcc14 很多不支持懒得折腾 用clang学习
关于 Clang 对 C++20 模块支持的介绍文档:
引言(Introduction)
在 Clang 中,“module”(模块)这个词具有多重含义,可能指:
- Objective-C 模块
- Clang 模块(Clang Header Module)
- C++20 模块(标准模块)
尽管它们内部实现共享了很多代码,但对用户来说,它们的行为、语义和命令行接口是不同的。
本文聚焦于 C++20 模块(也称为“标准模块”),文中所提的 module 均指这个概念。Clang module 另作区分。
C++ 标准中,模块包括两部分:
- 命名模块(Named Modules)
- 头文件单元(Header Units)
本文将两者都涵盖。
标准 C++ 命名模块(Standard C++ Named Modules)
为了理解 Clang 如何处理模块,我们需要先理解一些术语。这部分不是 C++ 教程,而是对模块语义的背景说明。
术语与定义(Background and Terminology)
Module vs Module Unit
- 一个 模块(Module) 是由一个或多个 模块单元(Module Unit) 组成。
- 模块单元是 C++ 特殊的翻译单元,通常要以
module
声明开始。
模块声明语法:
[export] module 模块名[:分区名];
export
是可选的模块名
和分区名
像普通标识符,可以带.
,但.
没有语义意义
模块单元的分类
类型 | 声明语法 | 含义 |
---|---|---|
主模块接口单元 | export module M; | 每个模块只能有一个,用于模块的公开接口 |
模块实现单元 | module M; | 可以有多个,存储实现细节 |
模块分区接口单元 | export module M:part; | 用于拆分模块接口,便于组织 |
内部模块分区单元 | module M:part; | 用于模块内部结构分割,不导出 |
更多术语定义
- 模块接口单元:主接口单元 + 分区接口单元
- 可导入模块单元:模块接口单元 + 内部模块分区
- 模块分区单元:分区接口单元 + 内部模块分区单元
Built Module Interface (BMI)
一个 BMI 是对“可导入模块单元”的预编译结果。Clang 通常生成 .pcm
文件(Precompiled Module)。
Global Module Fragment (GMF)
指位于 module;
与模块声明之间的代码块。这是模块外部代码的区域,常用于包含头文件等。
如何使用模块构建项目(How to Build Projects Using Modules)
快速示例(Hello World)
模块声明(Hello.cppm)
module;
#include <iostream>
export module Hello;
export void hello() {std::cout << "Hello World!\n";
}
使用模块(use.cpp)
import Hello;
int main() {hello();return 0;
}
编译命令:
clang++ -std=c++23 Hello.cppm --precompile -o Hello.pcm
clang++ -std=c++23 use.cpp -fmodule-file=Hello=Hello.pcm Hello.pcm -o Hello.out
./Hello.out
# 输出:Hello World!
说明:
- 使用
--precompile
生成.pcm
(BMI 文件) - 使用
-fmodule-file
指定模块文件供import
使用
复杂示例:使用四种模块单元
这个例子展示一个模块 M
拆分为不同功能单元,分别封装接口和实现。
主接口单元(M.cppm)
export module M;
export import :interface_part;
import :impl_part;
export void Hello();
export import
表示“导出一个子模块”import
表示“仅内部使用”
接口分区单元(interface_part.cppm)
export module M:interface_part;
export void World();
内部分区单元(impl_part.cppm)
module;
#include <iostream>
#include <string>
module M:impl_part;
import :interface_part;
std::string W = "World.";
void World() {std::cout << W << std::endl;
}
实现单元(Impl.cpp)
module;
#include <iostream>
module M;
void Hello() {std::cout << "Hello ";
}
使用者(User.cpp)
import M;
int main() {Hello();World();return 0;
}
编译流程总结
Step 1: 预编译各模块单元(生成 .pcm)
clang++ -std=c++23 interface_part.cppm --precompile -o M-interface_part.pcm
clang++ -std=c++23 impl_part.cppm --precompile -fprebuilt-module-path=. -o M-impl_part.pcm
clang++ -std=c++23 M.cppm --precompile -fprebuilt-module-path=. -o M.pcm
clang++ -std=c++23 Impl.cpp -fprebuilt-module-path=. -c -o Impl.o
Step 2: 编译用户代码
clang++ -std=c++23 User.cpp -fprebuilt-module-path=. -c -o User.o
Step 3: 编译模块 .pcm
为 .o
,并链接
clang++ -std=c++23 M-interface_part.pcm -fprebuilt-module-path=. -c -o M-interface_part.o
clang++ -std=c++23 M-impl_part.pcm -fprebuilt-module-path=. -c -o M-impl_part.o
clang++ -std=c++23 M.pcm -fprebuilt-module-path=. -c -o M.o
clang++ User.o M-interface_part.o M-impl_part.o M.o Impl.o -o a.out
总结
你现在应该理解以下概念:
- C++20 模块语法及其分类(主接口、实现、接口分区、内部分区)
- Clang 如何使用
--precompile
和.pcm
构建模块系统 - 模块的依赖需要通过
-fprebuilt-module-path
来查找 - 使用
import
替代#include
可以实现模块化、提升编译速度
如果你需要将这类项目集成进 CMake,或者自动生成.pcm
和.o
文件的流程,也可以继续问我。
如何启用标准 C++ 模块
只要你使用 -std=c++23
(或更新版本)编译选项,Clang 就会自动启用标准模块功能。
如何生成 BMI(Built Module Interface,构建模块接口)
BMI 是模块接口单元的预编译产物,有两种生成方式:
1. 两阶段编译(--precompile
)
- 第一步:将模块接口编译为
.pcm
文件。 - 第二步:使用
.pcm
文件编译和链接生成可执行文件。
示例:
clang++ -std=c++23 Hello.cppm --precompile -o Hello.pcm
clang++ -std=c++23 use.cpp -fprebuilt-module-path=. Hello.pcm -o Hello.out
2. 单阶段编译(-fmodule-output
)
- 在编译源文件时自动生成
.pcm
文件(BMI)。
示例:
clang++ -std=c++23 -fmodule-output Hello.cppm -c -o Hello.o
单阶段编译更适合构建系统;两阶段编译可以并行处理,编译速度更快。
文件命名约定(非常重要)
类型 | 建议扩展名 |
---|---|
模块接口单元(可导入) | .cppm (或 .ccm , .cxxm ) |
模块实现单元(不可导入) | .cpp (或 .cc , .cxx ) |
BMI 文件 | .pcm |
- 主模块接口 BMI:例如
Hello.pcm
- 模块分区接口 BMI:例如
M-interface_part.pcm
如果你使用了错误的扩展名(如.cpp
而不是.cppm
),Clang 无法识别为模块接口,除非你显式指定语言类型:
clang++ -std=c++23 -x c++-module Hello.cpp --precompile -o Hello.pcm
模块命名限制
根据 C++ 标准,以下模块名称是保留的,不能使用:
std
std1
std.anything
__test
- 等含
std
前缀或以__
开头的名称
如你仍想使用这些名字并忽略警告:
-Wno-reserved-module-identifier
如何指定 BMI 依赖
你需要在编译时显式指定模块依赖的 BMI 文件。方式有三种:
推荐方式:
-fprebuilt-module-path=目录路径
其他方式:
-fmodule-file=模块名=路径 # 推荐,惰性加载
-fmodule-file=路径 # 不推荐,已弃用,将被移除
多个选项的优先级是:
-fmodule-file=路径 > -fmodule-file=模块名=路径 > -fprebuilt-module-path=路径
编译和链接流程图示(传统 vs 模块)
传统头文件:
src1.cpp -+> clang++ src1.cpp --> src1.o ---,
hdr1.h --' +-> 链接 -> a.out
hdr2.h --, |
src2.cpp -+> clang++ src2.cpp --> src2.o ---'
使用模块后:
mod1.cppm -> mod1.pcm --++--> clang++ mod1.pcm -> mod1.o
src1.cpp --------------+--> clang++ src1.cpp -> src1.o
最终链接:
clang++ mod1.o src1.o -o a.out
注意:BMI 文件(.pcm
)不能直接链接,必须先编译为 .o
对象文件,再参与链接。
关于归档库(.a 文件)
你不能把 .pcm
直接打包进归档库(.a)。你应该将 .pcm
编译成 .o
文件,然后打包 .o
文件。
下面是对你提供的那段关于 Clang 支持 C++20 模块和 Header Units 使用方式:
目标概述
本文主要讲述如何使用 Clang 编译器来编译 C++20 的模块(Named Modules)与 Header Units,包括:
- 如何生成模块接口文件(BMI / PCM)
- 如何导入模块
- 如何分析模块依赖
- 使用
clang-scan-deps
获取依赖信息 - 模块对编译性能的影响分析
- 与 Clang Modules 的互操作性
Header Unit 的编译方式
示例一:标准库 Header Unit
// main.cpp
import <iostream>;
int main() {std::cout << "Hello World.\n";
}
编译步骤如下:
clang++ -std=c++23 -xc++-system-header --precompile iostream -o iostream.pcm
clang++ -std=c++23 -fmodule-file=iostream.pcm main.cpp
--precompile
生成预编译模块(PCM)-xc++-system-header
指定为系统头文件-fmodule-file
导入该预编译模块
用户自定义 Header Unit 示例
// foo.h
#include <iostream>
void Hello() {std::cout << "Hello World.\n";
}
// use.cpp
import "foo.h";
int main() {Hello();
}
编译流程:
clang++ -std=c++23 -fmodule-header foo.h -o foo.pcm
clang++ -std=c++23 -fmodule-file=foo.pcm use.cpp
如果 .h
没有扩展名,可用:
clang++ -std=c++23 -fmodule-header=system -xc++-header iostream -o iostream.pcm
模块依赖指定
- 使用
-fmodule-file=xxx.pcm
指定依赖模块文件 - 当前 Clang 尚不支持通过
-fprebuilt-module-path
自动查找 header unit(因其为匿名模块)
Header Unit 无法生成 .o
不能将 header unit 编译成 .o
,例如:
clang++ -std=c++23 -xc++-system-header --precompile iostream -o iostream.pcm
clang++ iostream.pcm -c -o iostream.o # 不被允许
Header unit 仅能用于预编译和导入。
包含翻译(#include 自动转 import)
Clang 在某些情况下可以将 #include
转换为 import
,尤其是在模块 global module fragment 中。例如:
module;
#include <iostream>
export module M;
export void Hello() {std::cout << "Hello.\n";
}
可被自动视为:
module;
import <iostream>;
export module M;
export void Hello() {std::cout << "Hello.\n";
}
Clang Modules vs 标准 Header Units
虽然 Header Units 与 Clang Modules 行为相似,但二者是不同机制:
- Header Units:符合 C++20 标准,按单一头文件构建
- Clang Modules:支持多个 header,语义更复杂
- Clang 不打算用 modulemap 模拟 Header Units,以防混淆
使用 clang-scan-deps
获取模块依赖
模块引入了依赖顺序问题(必须按拓扑顺序编译),可以使用 clang-scan-deps
自动生成依赖关系:
clang-scan-deps -format=p1689 -compilation-database compile_commands.json
输出为 P1689 格式 JSON,包括:
- 模块提供者(provides)
- 模块依赖(requires)
- 源文件与输出对应关系
支持更精细粒度调用方式,例如:
clang-scan-deps -format=p1689 -- ./clang++ -std=c++23 impl_part.cppm -c -o impl_part.o
常见问题
找不到系统头文件
如报错 fatal error: 'stddef.h' file not found
,可能是:
- 使用的是 clang++ 的符号链接
- 解决方案:
- 使用真实路径的 clang++
- 加上
-resource-dir
指定资源目录 - 使用
-print-resource-dir
获取资源路径
性能分析
模块理论上加速编译:O(n*m) => O(n + m)
编译器流程(-O0 时):
源文件:
├ 解析(Parsing)
├ 语义分析(Sema)
└ 前端生成(Codegen)
导入模块:
├ 名称查找
├ 重载决议
└ 模板实例化
编译器流程(优化开启 -O2/O3):
为了进行 跨模块优化(IPO),模块的定义会被重复使用于优化流程中,因此优化阶段的编译时间提升空间较小。
Clang-Repl 中导入模块
// M.cppm
export module M;
export const char* Hello() {return "Hello Interpreter for Modules!";
}
构建步骤:
clang++ -std=c++23 M.cppm --precompile -o M.pcm
clang++ M.pcm -c -o M.o
clang++ -shared M.o -o libM.so
然后:
clang-repl -Xcc=-std=c++23 -Xcc=-fprebuilt-module-path=.
%lib libM.so
import M;
extern "C" int printf(const char *, ...);
printf("%s\n", Hello());
输出:
Hello Interpreter for Modules!
安装clangd-19 就可以运行下面的例子
1:
main.cpp
import <iostream>;
int main() {//std::cout << "Hello World.\n";
}
cmake:
cmake_minimum_required(VERSION 3.28)
project(IostreamModuleExample LANGUAGES CXX)
# 设置 C++ 标准为 C++23(使用模块支持)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 必须使用 Clang 编译器
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")message(FATAL_ERROR "此示例需要使用支持模块的 Clang 编译器")
endif()
# 预编译 <iostream> 为模块接口文件 iostream.pcm
add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/iostream.pcmCOMMAND ${CMAKE_CXX_COMPILER}-std=c++23-xc++-system-header # 指定预编译的是系统头文件--precompile iostream # 编译 <iostream> 成 PCM-o ${CMAKE_BINARY_DIR}/iostream.pcmCOMMENT "预编译标准库头文件 <iostream> 为模块 iostream.pcm"
)
# 添加 main 可执行文件,明确依赖 iostream.pcm
add_executable(main main.cpp ${CMAKE_BINARY_DIR}/iostream.pcm)
# 编译时指定使用 iostream 的模块文件(保持选项一致)
target_compile_options(main PRIVATE-std=c++23-fmodule-file=${CMAKE_BINARY_DIR}/iostream.pcm
)
# 链接时也保持一致(有时不是必须)
target_link_options(main PRIVATE-fmodule-file=${CMAKE_BINARY_DIR}/iostream.pcm
)
target_compile_options(main PRIVATE -Wno-experimental-header-units)
2:
// M.cppm
export module M;
export import :interface_part;
import :impl_part;
export void Hello();
// interface_part.cppm
export module M:interface_part;
export void World();
// impl_part.cppm
module;
#include <iostream>
#include <string>
module M:impl_part;
import :interface_part;
std::string W = "World.";
void World() {std::cout << W << std::endl;
}
// Impl.cpp
module;
#include <iostream>
module M;
void Hello() {std::cout << "Hello ";
}
// User.cpp
import M;
int main() {Hello();World();return 0;
}
cmake:
cmake_minimum_required(VERSION 3.28)
project(ComplexHelloModules LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")message(FATAL_ERROR "只支持 Clang 编译器")
endif()
set(MOD_DIR ${CMAKE_BINARY_DIR}/modules)
file(MAKE_DIRECTORY ${MOD_DIR})
set(SOURCE_DIR ${CMAKE_SOURCE_DIR}/CppCon/day82/code)
# 预编译模块命令
add_custom_command(OUTPUT ${MOD_DIR}/M-interface_part.pcmCOMMAND ${CMAKE_CXX_COMPILER} -std=c++23 --precompile ${SOURCE_DIR}/interface_part.cppm -o ${MOD_DIR}/M-interface_part.pcmDEPENDS ${SOURCE_DIR}/interface_part.cppm
)
add_custom_target(precompile_interface_part DEPENDS ${MOD_DIR}/M-interface_part.pcm)
add_custom_command(OUTPUT ${MOD_DIR}/M-impl_part.pcmCOMMAND ${CMAKE_CXX_COMPILER} -std=c++23 --precompile -fprebuilt-module-path=${MOD_DIR} ${SOURCE_DIR}/impl_part.cppm -o ${MOD_DIR}/M-impl_part.pcmDEPENDS ${SOURCE_DIR}/impl_part.cppm ${MOD_DIR}/M-interface_part.pcm
)
add_custom_target(precompile_impl_part DEPENDS ${MOD_DIR}/M-impl_part.pcm)
add_custom_command(OUTPUT ${MOD_DIR}/M.pcmCOMMAND ${CMAKE_CXX_COMPILER} -std=c++23 --precompile -fprebuilt-module-path=${MOD_DIR} ${SOURCE_DIR}/M.cppm -o ${MOD_DIR}/M.pcmDEPENDS ${SOURCE_DIR}/M.cppm ${MOD_DIR}/M-interface_part.pcm ${MOD_DIR}/M-impl_part.pcm
)
add_custom_target(precompile_M DEPENDS ${MOD_DIR}/M.pcm)
# 编译模块实现对象
add_library(modules_objs OBJECT${SOURCE_DIR}/impl_part.cppm${SOURCE_DIR}/M.cppm
)
target_compile_options(modules_objs PRIVATE -std=c++23 -fprebuilt-module-path=${MOD_DIR})
add_dependencies(modules_objs precompile_interface_part precompile_impl_part precompile_M)
# 编译用户代码和链接
add_executable(hello_modular${SOURCE_DIR}/Impl.cpp${SOURCE_DIR}/User.cpp
)
target_compile_options(hello_modular PRIVATE -std=c++23 -fprebuilt-module-path=${MOD_DIR})
target_link_libraries(hello_modular PRIVATE modules_objs)
add_dependencies(hello_modular modules_objs)
3:
// Hello.cpp
module;
#include <iostream>
export module Hello;
export void hello() {std::cout << "Hello World!\n";
}
// use.cpp
import Hello;
int main() {hello();return 0;
}
cmake:
cmake_minimum_required(VERSION 3.25)
project(HelloModuleDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(MODULE_OUTPUT_DIR ${CMAKE_BINARY_DIR}/modules)
file(MAKE_DIRECTORY ${MODULE_OUTPUT_DIR})
set(HELLO_PCM ${MODULE_OUTPUT_DIR}/Hello.pcm)
set(HELLO_OBJ ${MODULE_OUTPUT_DIR}/Hello.o)
set(SRC_DIR ${CMAKE_SOURCE_DIR}/CppCon/day82/code)
# 1) 生成 Hello.pcm
add_custom_command(OUTPUT ${HELLO_PCM}COMMAND ${CMAKE_CXX_COMPILER}-std=c++23-x c++-module${SRC_DIR}/Hello.cpp--precompile-o ${HELLO_PCM}DEPENDS ${SRC_DIR}/Hello.cppCOMMENT "Precompiling Hello.cpp to Hello.pcm"
)
add_custom_target(precompile_Hello DEPENDS ${HELLO_PCM})
# 2) 用 Hello.pcm 编译生成 Hello.o
add_custom_command(OUTPUT ${HELLO_OBJ}COMMAND ${CMAKE_CXX_COMPILER}-std=c++23-fprebuilt-module-path=${MODULE_OUTPUT_DIR}-c ${SRC_DIR}/Hello.cpp-o ${HELLO_OBJ}DEPENDS ${HELLO_PCM} ${SRC_DIR}/Hello.cppCOMMENT "Compiling Hello.cpp to Hello.o using Hello.pcm"
)
add_custom_target(compile_Hello_obj DEPENDS ${HELLO_OBJ})
add_dependencies(compile_Hello_obj precompile_Hello)
# 编译 User.cpp 并链接所有目标
add_executable(hello_use ${SRC_DIR}/User.cpp)
target_compile_options(hello_use PRIVATE-std=c++23-fprebuilt-module-path=${MODULE_OUTPUT_DIR}
)
target_link_options(hello_use PRIVATE-fprebuilt-module-path=${MODULE_OUTPUT_DIR}
)
# 把 Hello.o 显式加入链接
target_link_libraries(hello_use PRIVATE ${HELLO_OBJ})
# User.cpp 依赖 Hello 模块
add_dependencies(hello_use compile_Hello_obj)
上面是几个例子完整的可以去llvm看
https://clang.llvm.org/docs/StandardCPlusPlusModules.html
下面C++ Modules 的示例代码,它展示了 C++ 模块(module
)与传统头文件(#include
)的对比,并阐述了模块的优势:语义清晰、依赖明确、编译快、结构好维护。
你给出的有三个版本:
下面是你请求的 完整可编译代码,使用的是传统 #include
风格的方式(非 C++20 modules),包括 main()
文件、Date
头文件与实现文件、Month
定义等。修正了原代码中几个拼写错误(如 Int
→ int
,Std::string
→ std::string
):
文件结构建议(建议放在对应目录):
project-root/
├── main.cpp
├── Calendar/
│ ├── date.h
│ ├── date.cpp
│ └── Month.h
main.cpp
(即你说的 use-date.cxx
)
#include <iostream>
#include "calendar/date.h"
int main() {using namespace Chrono;Date date { 22, Month::Sep, 2015 };std::cout << "Today is " << date << std::endl;
}
calendar/date.h
#ifndef CHRONO_DATE_INCLUDED
#define CHRONO_DATE_INCLUDED
#include <iosfwd>
#include <string>
#include "calendar/Month.h"
namespace Chrono {
struct Date {Date(int dd, Month mm, int yy);int day() const { return d; }Month month() const { return m; }int year() const { return y; }
private:int d;Month m;int y;
};
std::ostream& operator<<(std::ostream&, const Date&);
std::string to_string(const Date&);
} // namespace Chrono
#endif // CHRONO_DATE_INCLUDED
calendar/date.cpp
#include "date.h"
#include <iostream>
namespace Chrono {
Date::Date(int dd, Month mm, int yy): d(dd), m(mm), y(yy) {}
std::ostream& operator<<(std::ostream& os, const Date& date) {return os << date.day() << "-" << static_cast<int>(date.month()) << "-" << date.year();
}
std::string to_string(const Date& date) {return std::to_string(date.day()) + "-" +std::to_string(static_cast<int>(date.month())) + "-" +std::to_string(date.year());
}
} // namespace Chrono
calendar/Month.h
#ifndef CHRONO_MONTH_INCLUDED
#define CHRONO_MONTH_INCLUDED
namespace Chrono {
enum class Month {Jan = 1, Feb, Mar, Apr, May, Jun,Jul, Aug, Sep, Oct, Nov, Dec
};
} // namespace Chrono
#endif // CHRONO_MONTH_INCLUDED
编译命令(使用 g++ 或 clang++):
cmake_minimum_required(VERSION 3.15)
project(UseDate LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(SRC_DIR ${CMAKE_SOURCE_DIR}/CppCon/day82/code)
# 添加 source files
add_executable(use_date${SRC_DIR}/main.cpp${SRC_DIR}/calendar/date.cpp
)
# 添加包含头文件的路径
target_include_directories(use_date PRIVATE${SRC_DIR}/calendar
)
输出示例:
Today is 22-9-2015
下面学习怎么把这个文件分成module 的形式
main.cxx:
#include <iostream>
#include <string>
enum class Month { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec };
namespace Chrono {
struct Date {Date(int dd, Month mm, int yy) : d(dd), m(mm), y(yy) {}int day() const { return d; }Month month() const { return m; }int year() const { return y; }
private:int d;Month m;int y;
};
} // namespace Chrono
std::ostream& operator<<(std::ostream& os, const Chrono::Date& date) {return os << date.day() << "-" << static_cast<int>(date.month()) << "-" << date.year();
}
std::string to_string(const Chrono::Date& date) {return std::to_string(date.day()) + "-" + std::to_string(static_cast<int>(date.month())) + "-" +std::to_string(date.year());
}
int main() {using namespace Chrono;Date date{22, Month::Sep, 2015};std::cout << "Today is " << date << std::endl;
}
目录结构
xiaqiu@xz:~/test/CppCon/day82/code$ tree
.
├── CMakeLists.txt
└── main.cxx
1 directory, 2 files
xiaqiu@xz:~/test/CppCon/day82/code$
cmake:
cmake_minimum_required(VERSION 3.28)
project(CalendarWithModules LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(SOURCE_DIR ${CMAKE_SOURCE_DIR}/CppCon/day82/code)
set(MOD_DIR ${CMAKE_BINARY_DIR}/modules)
file(MAKE_DIRECTORY ${MOD_DIR})
# 编译主程序
add_executable(calendar_main ${SOURCE_DIR}/main.cxx)
add_custom_target(update-timestampCOMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_SOURCE_DIR}/CMakeLists.txtCOMMENT "Updating game/CMakeLists.txt timestamp" # 跟新CMakeLists.txt时间戳
)
# 3. 让 main 依赖于 clean_modules
add_dependencies(calendar_main update-timestamp)
修改头文件导入为import
import <iostream>
import <string>
enum class Month { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec };
namespace Chrono {
struct Date {Date(int dd, Month mm, int yy) : d(dd), m(mm), y(yy) {}int day() const { return d; }Month month() const { return m; }int year() const { return y; }
private:int d;Month m;int y;
};
} // namespace Chrono
std::ostream& operator<<(std::ostream& os, const Chrono::Date& date) {return os << date.day() << "-" << static_cast<int>(date.month()) << "-" << date.year();
}
std::string to_string(const Chrono::Date& date) {return std::to_string(date.day()) + "-" + std::to_string(static_cast<int>(date.month())) + "-" +std::to_string(date.year());
}
int main() {using namespace Chrono;Date date{22, Month::Sep, 2015};std::cout << "Today is " << date << std::endl;
}
修改生成iostream的pcm stream的pcm
cmake_minimum_required(VERSION 3.28)
project(CalendarWithModules LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(SOURCE_DIR ${CMAKE_SOURCE_DIR}/CppCon/day82/code)
set(MOD_DIR ${CMAKE_BINARY_DIR}/modules)
file(MAKE_DIRECTORY ${MOD_DIR})
# 预编译 <iostream> 为模块接口文件 iostream.pcm
add_custom_command(OUTPUT ${MOD_DIR}/iostream.pcmCOMMAND ${CMAKE_CXX_COMPILER}-std=c++23-xc++-system-header # 指定预编译的是系统头文件--precompile iostream # 编译 <iostream> 成 PCM-o ${MOD_DIR}/iostream.pcmCOMMENT "预编译标准库头文件 <iostream> 为模块 iostream.pcm"
)
add_custom_target(iostream_pcm DEPENDS ${MOD_DIR}/iostream.pcm)
# 预编译 <string> 为模块接口文件 string.pcm
add_custom_command(OUTPUT ${MOD_DIR}/string.pcmCOMMAND ${CMAKE_CXX_COMPILER}-std=c++23-xc++-system-header # 指定预编译的是系统头文件--precompile string # 编译 <string> 成 PCM-o ${MOD_DIR}/string.pcmCOMMENT "预编译标准库头文件 <string> 为模块 string.pcm"
)
add_custom_target(string_pcm DEPENDS ${MOD_DIR}/string.pcm)
# 编译主程序
add_executable(calendar_main ${SOURCE_DIR}/main.cxx ${MOD_DIR}/iostream.pcm ${MOD_DIR}/string.pcm)
add_custom_target(update-timestampCOMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_SOURCE_DIR}/CMakeLists.txtCOMMENT "Updating game/CMakeLists.txt timestamp" # 跟新CMakeLists.txt时间戳
)
# 3. 让 main 依赖于 clean_modules
add_dependencies(calendar_main update-timestamp iostream_pcm string_pcm)
# 设置 fmodule-file 参数,指向预编译模块
target_compile_options(calendar_main PRIVATE -fmodule-file=${MOD_DIR}/iostream.pcm)
target_compile_options(calendar_main PRIVATE -fmodule-file=${MOD_DIR}/string.pcm)
xiaqiu@xz:~/test/build/modules$ tree
.
├── iostream.pcm
└── string.pcm
1 directory, 2 files
xiaqiu@xz:~/test/build/modules$
手动预编译 C++ 标准库头文件为模块接口单元(PCM 文件),然后
在主程序编译时通过 -fmodule-file=xxx.pcm
使用这些模块。
一步一步解释:
1. 手动预编译 <iostream>
为模块接口文件
add_custom_command(OUTPUT ${MOD_DIR}/iostream.pcmCOMMAND ${CMAKE_CXX_COMPILER}-std=c++23-xc++-system-header--precompile iostream-o ${MOD_DIR}/iostream.pcmCOMMENT "预编译标准库头文件 <iostream> 为模块 iostream.pcm"
)
add_custom_target(iostream_pcm DEPENDS ${MOD_DIR}/iostream.pcm)
解释:
-xc++-system-header
: 告诉 Clang 这是预编译 系统头文件,不是普通用户源文件。--precompile iostream
: 表示将<iostream>
编译为模块接口单元(PCM 文件)。-o ${MOD_DIR}/iostream.pcm
: 输出到目标目录。add_custom_target(...)
: 创建一个 phony 目标(叫iostream_pcm
),目的是触发上面的命令。
你做同样的处理对<string>
:
--precompile string → ${MOD_DIR}/string.pcm
2. 把生成的模块 pcm 文件链接到主程序中
target_compile_options(calendar_main PRIVATE -fmodule-file=${MOD_DIR}/iostream.pcm)
target_compile_options(calendar_main PRIVATE -fmodule-file=${MOD_DIR}/string.pcm)
解释:
这些语句意思是:当编译 calendar_main
可执行文件时,告诉编译器使用提前预编译好的模块单元文件:
-fmodule-file=xxx.pcm
是 Clang 的选项,用于 加载现成模块,而不是重新编译。calendar_main
的编译器参数里会带上:-fmodule-file=/some/path/modules/iostream.pcm -fmodule-file=/some/path/modules/string.pcm
整体流程图理解:
Step 1: 预编译模块头<iostream> ──Clang─ --precompile→ iostream.pcm<string> ──Clang─ --precompile→ string.pcm
Step 2: 编译 main 程序main.cpp + -fmodule-file=iostream.pcm + -fmodule-file=string.pcm↓使用预编译模块加快编译、避免重复分析 headers
运行输出:
Today is 22-9-2015
-Xclang -fretain-comments-from-system-headers
是一个 Clang 编译器的命令行选项,用于控制是否在抽象语法树(AST)中保留系统头文件中的文档注释(例如 ///
或 /** */
样式的注释,通常用于 Doxygen 等文档生成工具)。
- 作用:这个选项指示 Clang 在解析系统头文件(例如
<iostream>
、<string>
等标准库头文件)时,将其中的文档注释保留在生成的 AST 中,而不是忽略它们。 - 为什么需要:
- 默认情况下,Clang 可能会忽略系统头文件中的注释,以减少 AST 的大小和解析开销。
- 某些工具(例如 Clangd,用于提供代码补全、诊断等 LSP 功能的语言服务器)依赖于这些注释来提供更准确的代码分析或文档提示。
- 如果你在生成预编译模块(PCM,例如
iostream.pcm
)或预编译头(PCH)时没有启用这个选项,但编译主程序或 Clangd 启用了它,就会导致配置不匹配的错误(如你遇到的Retain documentation comments from system headers in the AST was disabled in PCH file but is currently enabled
)。
-Xclang
的作用:-fretain-comments-from-system-headers
是一个 Clang 前端(-cc1
)的选项,而不是驱动程序的直接选项。-Xclang
用于将后续的选项传递给 Clang 的前端。
具体用途:
- 解决模块/PCH 错误:在你的项目中,添加
-Xclang -fretain-comments-from-system-headers
到 PCM 文件生成和主程序编译命令中,可以确保系统头文件的文档注释处理方式一致,从而避免pch_langopt_mismatch
或module-file-config-mismatch
错误。 - 支持 Clangd:Clangd 默认可能启用此选项以解析文档注释。如果你的预编译模块没有启用它,Clangd 可能会报错或无法正确提供代码补全、跳转等功能。
示例:
在你的 CMakeLists.txt
中,添加这个选项到 PCM 生成和编译命令:
COMMAND ${CMAKE_CXX_COMPILER}-std=c++23-Xclang -fretain-comments-from-system-headers # 保留系统头文件中的文档注释-xc++-system-header--precompile iostream-o ${MOD_DIR}/iostream.pcm
Month 提取单独的ixx中
calendar/month.ixx
export module calendar.month;
export enum class Month { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec };
code
├── CMakeLists.txt
├── calendar
│ └── month.ixx
└── main.cxx
cmake_minimum_required(VERSION 3.28)
project(CalendarWithModules LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(SOURCE_DIR ${CMAKE_SOURCE_DIR}/CppCon/day82/code)
set(MOD_DIR ${CMAKE_BINARY_DIR}/modules)
file(MAKE_DIRECTORY ${MOD_DIR})
# 预编译 <iostream> 为模块接口文件 iostream.pcm
add_custom_command(OUTPUT ${MOD_DIR}/iostream.pcmCOMMAND ${CMAKE_CXX_COMPILER}-std=c++23-Xclang -fretain-comments-from-system-headers # Add this flag-xc++-system-header # 指定预编译的是系统头文件--precompile iostream # 编译 <iostream> 成 PCM-o ${MOD_DIR}/iostream.pcmCOMMENT "预编译标准库头文件 <iostream> 为模块 iostream.pcm"
)
add_custom_target(iostream_pcm DEPENDS ${MOD_DIR}/iostream.pcm)
# 预编译 <string> 为模块接口文件 string.pcm
add_custom_command(OUTPUT ${MOD_DIR}/string.pcmCOMMAND ${CMAKE_CXX_COMPILER}-std=c++23-Xclang -fretain-comments-from-system-headers # Add this flag-xc++-system-header # 指定预编译的是系统头文件--precompile string # 编译 <string> 成 PCM-o ${MOD_DIR}/string.pcmCOMMENT "预编译标准库头文件 <string> 为模块 string.pcm"
)
add_custom_target(string_pcm DEPENDS ${MOD_DIR}/string.pcm)
add_custom_command(OUTPUT ${MOD_DIR}/month.pcmCOMMAND ${CMAKE_CXX_COMPILER} -std=c++23 -x c++-module--precompile -Xclang -fretain-comments-from-system-headers-fprebuilt-module-path=${MOD_DIR} ${SOURCE_DIR}/calendar/month.ixx -o ${MOD_DIR}/month.pcm
)
add_custom_target(month_pcm DEPENDS ${MOD_DIR}/month.pcm)
# 编译主程序
add_executable(calendar_main ${SOURCE_DIR}/main.cxx ${MOD_DIR}/iostream.pcm ${MOD_DIR}/string.pcm ${MOD_DIR}/month.pcm)
add_custom_target(update-timestampCOMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_SOURCE_DIR}/CMakeLists.txtCOMMENT "Updating game/CMakeLists.txt timestamp" # 跟新CMakeLists.txt时间戳
)
# 让 main 依赖于 update-timestamp 和 PCM 文件
add_dependencies(calendar_main update-timestamp iostream_pcm string_pcm)
# 设置 fmodule-file 参数,指向预编译模块
target_compile_options(calendar_main PRIVATE-fmodule-file=${MOD_DIR}/iostream.pcm-fmodule-file=${MOD_DIR}/string.pcm-fmodule-file=${MOD_DIR}/month.pcm-Xclang -fretain-comments-from-system-headers # Add this flag-Wno-experimental-header-units
)
输出:
Today is 22-9-2015
把date的内容移动到calendar/date.cxx中
date.cxx:
module calendar.date; // 实现模块 calendar.date
// 导入标准模块(可细化为 iostream 和 string)
import <iostream>;
import <string>;
import calendar.month; // 导入你自己的 Month 枚举模块
namespace Chrono {
export struct Date {Date(int day_, Month month_, int year_) : d(day_), m(month_), y(year_) {}int day() const { return d; }Month month() const { return m; }int year() const { return y; }
private:int d;Month m;int y;
};
export std::ostream& operator<<(std::ostream& os, const Date& date) {os << static_cast<int>(date.month()) << "/" << date.day() << "/" << date.year();return os;
}
export std::string to_string(const Date& date) {return std::to_string(static_cast<int>(date.month())) + "/" + std::to_string(date.day()) + "/" +std::to_string(date.year());
}
} // namespace Chrono
xiaqiu@xz:~/test/CppCon/day82/code$ tree
.
├── CMakeLists.txt
├── calendar
│ ├── date.cxx
│ └── month.ixx
└── main.cxx
2 directories, 4 files
xiaqiu@xz:~/test/CppCon/day82/code$
add_custom_command(OUTPUT ${MOD_DIR}/date.pcmCOMMAND ${CMAKE_CXX_COMPILER} -std=c++23 -x c++-module--precompile -Xclang -fretain-comments-from-system-headers-fprebuilt-module-path=${MOD_DIR} ${SOURCE_DIR}/calendar/date.cxx -o ${MOD_DIR}/date.pcm
)
add_custom_target(date_pcm DEPENDS ${MOD_DIR}/date.pcm)
# 编译主程序
add_executable(calendar_main ${SOURCE_DIR}/main.cxx ${MOD_DIR}/iostream.pcm ${MOD_DIR}/string.pcm ${MOD_DIR}/month.pcm ${MOD_DIR}/date.pcm)
出现未定义
看来只能ixx 接口export 和实现cxx 分开
项目模块结构
源文件(假设位于 ${SOURCE_DIR}
):
M.cppm # 主模块接口 (export module M;)
interface_part.cppm # 模块接口分区 (export module M:interface_part;)
impl_part.cppm # 模块实现分区 (module M:impl_part;)
Impl.cpp # 使用模块 M 的实现代码
User.cpp # 使用模块 M 的用户代码
模块分区说明
M.cppm
(主接口模块)
export module M;
export import :interface_part;
export import :impl_part;
- 这是模块
M
的顶层导出接口; - 它把
interface_part
和impl_part
都包含进来。
interface_part.cppm
(接口分区)
export module M:interface_part;
export void greet(); // 声明接口
- 是
M
模块的接口部分(export module M:interface_part;
); - 定义了可导出的声明;
- 会生成
M-interface_part.pcm
,可以被其他模块 import。
impl_part.cppm
(实现分区)
module M:impl_part;
#include <iostream>
void greet() {std::cout << "Hello from module M!\n";
}
- 是
M
模块的实现部分(不能 export); - 必须被
M
的主模块显式 import 才能生效。
构建逻辑
分阶段编译 .pcm
(模块编译单元)
M-interface_part.pcm
← 编译interface_part.cppm
M-impl_part.pcm
← 编译impl_part.cppm
(依赖 interface)M.pcm
← 编译M.cppm
(导入 interface + impl)
这些.pcm
是 模块头部信息的预编译形式,供编译器了解模块结构。
OBJECT
库编译 modules_objs
:
add_library(modules_objs OBJECT${SOURCE_DIR}/impl_part.cppm${SOURCE_DIR}/M.cppm
)
- 这一步是将
impl_part.cppm
与M.cppm
编译成.o
对象代码; - 这些
.o
会参与链接,提供模块的函数定义实现(如greet()
);
连接用户代码
add_executable(hello_modular${SOURCE_DIR}/Impl.cpp${SOURCE_DIR}/User.cpp
)
User.cpp
和Impl.cpp
中可以import M;
;- 编译器通过
-fprebuilt-module-path=${MOD_DIR}
找到.pcm
; - 链接器通过
modules_objs
找到.o
实现代码。
总结接口与实现结构
文件名 | 类型 | 内容用途 |
---|---|---|
interface_part.cppm | 接口分区 M:interface_part | 声明可供导出函数(如 greet() ) |
impl_part.cppm | 实现分区 M:impl_part | 实现接口函数 greet() |
M.cppm | 主模块接口 M | 汇总并导出接口和实现分区 |
Impl.cpp , User.cpp | 用户代码 | 通过 import M; 使用模块中定义的函数 |
整体构建流程图(简化)
interface_part.cppm ──┐├─> M-interface_part.pcm
impl_part.cppm ─────────────┐├─> M-impl_part.pcm
M.cppm ─────────────────────┘──> M.pcm
impl_part.cppm + M.cppm ──→ modules_objs (.o) ──┐├─> linked into hello_modular
Impl.cpp + User.cpp ────────→ hello_modular ───┘ (import M)
使用 date.ixx
表示接口、date.cxx
表示实现,是一种清晰且标准兼容的做法。下面是按你的设想重新组织模块接口/实现的写法:
文件结构(推荐方式)
date.ixx
:导出模块接口date.cxx
:实现模块的内部逻辑(编译时需导入接口模块)
date.ixx
(模块接口)
export module calendar.date;
import <iostream>;
import <string>;
import calendar.month;
namespace Chrono {
export struct Date {Date(int day_, Month month_, int year_);int day() const;Month month() const;int year() const;
private:int d;Month m;int y;
};
export std::ostream& operator<<(std::ostream& os, const Date& date);
export std::string to_string(const Date& date);
} // namespace Chrono
date.cxx
(模块实现)
module calendar.date;
import <iostream>;
import <string>;
import calendar.month;
namespace Chrono {
Date::Date(int day_, Month month_, int year_) : d(day_), m(month_), y(year_) {}
int Date::day() const { return d; }
Month Date::month() const { return m; }
int Date::year() const { return y; }
std::ostream& operator<<(std::ostream& os, const Date& date) {os << static_cast<int>(date.month()) << "/" << date.day() << "/" << date.year();return os;
}
std::string to_string(const Date& date) {return std::to_string(static_cast<int>(date.month())) + "/" +std::to_string(date.day()) + "/" +std::to_string(date.year());
}
}
在用户代码中使用
import calendar.date;
using Chrono::Date;
int main() {Date d{8, Chrono::Month::Jun, 2025};std::cout << d << '\n';
}
xiaqiu@xz:~/test/CppCon/day82/code$ tree
.
├── CMakeLists.txt
├── calendar
│ ├── date.cxx
│ ├── date.ixx
│ └── month.ixx
└── main.cxx
2 directories, 5 files
xiaqiu@xz:~/test/CppCon/day82/code$
main.cxx:
import <iostream>;
import <string>;
import calendar.month;
import calendar.date;
int main() {using namespace Chrono;Date date{22, Month::Sep, 2015};std::cout << "Today is " << date << std::endl;
}
date.cxx:
module calendar.date;
import <iostream>;
import <string>;
import calendar.month;
namespace Chrono {
Date::Date(int day_, Month month_, int year_) : d(day_), m(month_), y(year_) {}
int Date::day() const { return d; }
Month Date::month() const { return m; }
int Date::year() const { return y; }
std::ostream& operator<<(std::ostream& os, const Date& date) {os << static_cast<int>(date.month()) << "/" << date.day() << "/" << date.year();return os;
}
std::string to_string(const Date& date) {return std::to_string(static_cast<int>(date.month())) + "/" + std::to_string(date.day()) + "/" +std::to_string(date.year());
}
} // namespace Chrono
date.ixx:
export module calendar.date;
import <iostream>;
import <string>;
import calendar.month;
namespace Chrono {
export struct Date {Date(int day_, Month month_, int year_);int day() const;Month month() const;int year() const;
private:int d;Month m;int y;
};
export std::ostream& operator<<(std::ostream& os, const Date& date);
export std::string to_string(const Date& date);
} // namespace Chrono
month.ixx:
export module calendar.month;
export enum class Month { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec };
cmake太复杂了:
cmake_minimum_required(VERSION 3.28)
project(CalendarWithModules LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(SOURCE_DIR ${CMAKE_SOURCE_DIR}/CppCon/day82/code)
set(MOD_DIR ${CMAKE_BINARY_DIR}/modules)
file(MAKE_DIRECTORY ${MOD_DIR})
# 预编译 <iostream>
add_custom_command(OUTPUT ${MOD_DIR}/iostream.pcmCOMMAND ${CMAKE_CXX_COMPILER}-std=c++23-Xclang -fretain-comments-from-system-headers-xc++-system-header--precompile iostream-o ${MOD_DIR}/iostream.pcmCOMMENT "预编译 <iostream>"
)
add_custom_target(iostream_pcm DEPENDS ${MOD_DIR}/iostream.pcm)
# 预编译 <string>
add_custom_command(OUTPUT ${MOD_DIR}/string.pcmCOMMAND ${CMAKE_CXX_COMPILER}-std=c++23-Xclang -fretain-comments-from-system-headers-xc++-system-header--precompile string-o ${MOD_DIR}/string.pcmCOMMENT "预编译 <string>"
)
add_custom_target(string_pcm DEPENDS ${MOD_DIR}/string.pcm)
# 编译 calendar.month 模块
add_custom_command(OUTPUT ${MOD_DIR}/month.pcmCOMMAND ${CMAKE_CXX_COMPILER} -std=c++23 -x c++-module --precompile-Xclang -fretain-comments-from-system-headers-fprebuilt-module-path=${MOD_DIR}${SOURCE_DIR}/calendar/month.ixx -o ${MOD_DIR}/month.pcm
)
add_custom_target(month_pcm DEPENDS ${MOD_DIR}/month.pcm)
# 编译 calendar.date 接口模块
add_custom_command(OUTPUT ${MOD_DIR}/date.pcmCOMMAND ${CMAKE_CXX_COMPILER} -std=c++23 -x c++-module --precompile-Xclang -fretain-comments-from-system-headers-fprebuilt-module-path=${MOD_DIR}-fmodule-file=${MOD_DIR}/iostream.pcm-fmodule-file=${MOD_DIR}/string.pcm-fmodule-file=calendar.month=${MOD_DIR}/month.pcm${SOURCE_DIR}/calendar/date.ixx -o ${MOD_DIR}/date.pcm
)
add_custom_target(date_pcm DEPENDS ${MOD_DIR}/date.pcm)
# 编译 date.cxx 对象
add_library(date_obj OBJECT ${SOURCE_DIR}/calendar/date.cxx)
target_compile_options(date_obj PRIVATE-std=c++23-Xclang -fretain-comments-from-system-headers-fprebuilt-module-path=${MOD_DIR}-fmodule-file=${MOD_DIR}/iostream.pcm-fmodule-file=${MOD_DIR}/string.pcm-fmodule-file=calendar.month=${MOD_DIR}/month.pcm-fmodule-file=calendar.date=${MOD_DIR}/date.pcm
)
# 编译 date.ixx 对象
add_library(date_iface_obj OBJECT ${SOURCE_DIR}/calendar/date.ixx)
target_compile_options(date_iface_obj PRIVATE-std=c++23-x c++-module-Xclang -fretain-comments-from-system-headers-fprebuilt-module-path=${MOD_DIR}-fmodule-file=${MOD_DIR}/iostream.pcm-fmodule-file=${MOD_DIR}/string.pcm-fmodule-file=calendar.month=${MOD_DIR}/month.pcm
)
# 编译 date.cxx 实现对象
add_library(date_impl_obj OBJECT ${SOURCE_DIR}/calendar/date.cxx)
target_compile_options(date_impl_obj PRIVATE-std=c++23-Xclang -fretain-comments-from-system-headers-fprebuilt-module-path=${MOD_DIR}-fmodule-file=${MOD_DIR}/iostream.pcm-fmodule-file=${MOD_DIR}/string.pcm-fmodule-file=calendar.month=${MOD_DIR}/month.pcm-fmodule-file=${MOD_DIR}/date.pcm
)
# 生成动态库 date
add_library(date SHARED$<TARGET_OBJECTS:date_iface_obj>$<TARGET_OBJECTS:date_impl_obj>
)
# 编译主程序
add_executable(calendar_main ${SOURCE_DIR}/main.cxx)
target_compile_options(calendar_main PRIVATE-std=c++23-Xclang -fretain-comments-from-system-headers-fprebuilt-module-path=${MOD_DIR}-fmodule-file=${MOD_DIR}/iostream.pcm-fmodule-file=${MOD_DIR}/string.pcm-fmodule-file=calendar.month=${MOD_DIR}/month.pcm-fmodule-file=${MOD_DIR}/date.pcm
)
target_link_libraries(calendar_main PRIVATE date)
# 设置依赖关系
add_dependencies(month_pcm iostream_pcm string_pcm)
add_dependencies(date_pcm month_pcm)
add_dependencies(date_iface_obj date_pcm)
add_dependencies(date_impl_obj date_pcm)
add_dependencies(date date_iface_obj date_impl_obj)
add_dependencies(calendar_main date)
这段描述是来自于一本关于 C++模块化或编译模型 的讨论资料,它解释的是:
同一段小小的用户代码(176 字节),在不同编译器下经过预处理、头文件展开、宏展开后,最终传给编译器的内容可能是几十万字节。
所描述的是:头文件膨胀(header bloat)
#include <iostream>
#include "Calendar/date.h"
int main() {using namespace Chrono;Date date { 18, Month::Sep, 2015 };std::cout << "Today is " << date << std::endl;
}
这段代码只有 176 bytes,也就是:
- 你写的源代码字节数非常少
- 但是因为
#include <iostream>
和#include "Calendar/date.h"
会引入大量 STL 头文件、模板定义和函数声明 - 所以编译器看到的是:展开后高达数百 KB 到数 MB 的代码
实际编译器展开体积举例:
编译器版本 | 展开后代码体积(approx) |
---|---|
GCC 5.2.0 | 约 412,326 bytes(约 400 KB) |
Clang 3.6.1 | 约 1,203,953 bytes(超 1 MB) |
MSVC Dev14 | 约 1,083,255 bytes(也超 1 MB) |
这些数字表明: |
- 你写的只是 “176 字节”,但编译器实际需要处理的是 几百 KB 到几 MB 的代码
- 越老的编译器越难优化这些头文件展开
- 这就是为什么 模块(modules)系统 被提出:为了解决头文件重复编译、编译速度慢等问题
模块能带来什么?
使用 C++20 的模块(import
),就可以做到:
- 不需要每次都重复展开 iostream、vector、string 等头文件
- 模块编译一次就可以缓存为
.pcm
(预编译模块)文件 - 之后的编译只加载
.pcm
,速度和内存都更优
总结
你这段描述来自编译器讲解材料,比如 CppCon 或模块设计指南。它说明的是:
一段小小的用户代码,其实际对编译器造成的负担非常大 —— 这是头文件膨胀的问题。
引出背景是为了强调 使用 C++ 模块的必要性。
如果你正在学习模块(import
/ module
),我可以提供一个示例:
- 使用
module; export module date;
定义模块 - 使用
import date;
在 main 中使用 - 对比含有
#include
与模块的编译时间差异
为什么 #include
是 C++ 中构建效率低的“元凶”,并说明了为什么 模块(Modules)是大势所趋。
内容逐句解释
#include <iostream>
#include "Calendar/date.h"
你在源代码中写的只是两行 #include
,但它们实际上引发了非常重的编译负担。
为什么 #include
是个问题?
“Preprocessor directive
#include
is textual copy and paste”
翻译:
#include
本质上是文本复制粘贴。
举个例子:
#include <vector>
等同于:
// 复制整个 <vector> 文件的全部代码粘贴到当前位置
这会带来连锁反应:
- 引入
<vector>
⇒ 它又引入<initializer_list>
,<memory>
,<algorithm>
… - 每个
.cpp
文件都要 重新展开、重新编译 这些头文件 - 每个模板定义(如
std::vector
))都要重新解析和实例化
Compiler works hard to process the same entity multiple times
即使你在多个 .cpp
文件中都用了 #include <vector>
:
- 编译器每次都要重新编译这个头文件(虽然内容一样)
- 即使你用了
#pragma once
或 include guards,它只阻止多次嵌套,不阻止多个.cpp
文件之间的重复工作
Linker works hard to throw all of them away, except one
- 所有
.cpp
文件最终会生成.o
(目标文件) - 如果头文件中有
inline
函数、模板函数等,多个.o
中会有重复定义 - 链接器(linker)最后要“清理”重复项,只保留一个版本
这就是链接器中的ODR
(One Definition Rule)问题所在,容易出错也浪费资源。
“Miserable build throughput”(可怜的构建吞吐量)
最终的后果:
- 编译时间长(头文件重复处理)
- 链接时间长(要合并并去除冗余)
- 构建系统复杂(靠 precompiled headers、unity builds 缓解问题)
C++ Modules 的优点
C++20 模块(module
/ import
)的设计目的就是为了终结这些问题:
特性 | 模块的行为 |
---|---|
引入代码方式 | import <module> (非文本展开) |
编译器是否重复解析? | 否,只编译一次成 .pcm 文件 |
链接器是否需要清理? | 否,模块不会产生重复定义 |
构建速度 | 快得多,特别是大型项目或大量模板代码 |
总结理解句式
原句 | 中文解释 |
---|---|
#include is textual copy and paste | 预处理器的文本替换机制,会复制头文件内容到每个 .cpp |
Compiler works hard to process the same entity… | 编译器多次重复编译相同头文件 |
Linker works hard to throw all of them away… | 链接器必须识别并去除所有重复定义 |
Miserable build throughput | 构建过程慢,开发体验差 |
继续深入讲解了为什么传统 C/C++ 中用 #include
复制粘贴代码是 危险的做法,不仅导致构建慢,更引发很多难以追踪的 bug 和架构脆弱性。我们逐句拆解来理解。
Copy: No consistency guarantee
复制没有一致性保证
使用 #include
,你相当于把同一份代码 复制进多个不同的 .cpp
文件 中。
- 如果头文件改动,所有依赖的
.cpp
都要重新编译; - 但你没有任何机制来验证这些文件之间的一致性
- 易出错,依赖太松散
Hard to track bugs (famous “ODR” violation)
难以追踪的 bug(臭名昭著的“ODR违反”)
ODR = One Definition Rule(一个定义规则)
C++ 要求:每个函数/类/变量在程序中只能有一个定义
但你用 #include
,等于偷偷地把定义复制到了多个地方:
// my_class.h
struct MyClass {void foo() {}
};
如果这个头被多个 .cpp
文件包含,编译器在每个 .cpp
文件中都会看到一份 foo()
的定义,最终链接器会尝试合并它们。如果你有微小差异,就会出现:
ODR violation: multiple definitions of MyClass::foo
非常难查的问题,因为编译器在多个步骤处理(预处理 → 编译 → 链接),而问题要到链接阶段才爆发。
No component boundaries, brittle enforcement
没有组件边界,结构极脆弱
#include
不能明确表达 “我只想使用这个模块的接口”。
- 它让你暴露内部实现细节给用户
- 用户可以“滥用”这些细节(违背封装)
- 修改一个头文件可能导致下游成百上千个文件重新编译
- 系统缺乏清晰的模块边界,架构容易崩塌
模块(module foo; export ...
)则恰好解决这一点。
C Preprocessor technology: Impossible to correctly parse/analyze a component
C 的预处理器机制,无法正确分析组件结构
原因是:
#include
是文本级替换,没有语义意识(不知道你在 include 什么、是否是模板等)- 编译器不能构建模块化抽象图(依赖图混乱)
- 很难做静态分析、模块化优化、语义检查
而 C++ Modules 是 编译器级别的机制,支持:
语义依赖
明确导出接口
构建系统能准确追踪依赖关系
支持增量编译和并行构建
总结对比
传统 #include | C++20 Modules |
---|---|
文本替换,复制定义 | 编译好的接口,按需导入 |
多次编译相同实体,构建慢 | 编译一次 .pcm ,重用 |
没有组件边界,容易破坏封装 | 明确的接口导出和依赖 |
ODR 问题严重,bug 难查 | 编译器自动控制唯一定义 |
构建系统难分析依赖 | 可以静态追踪模块依赖 |
工具无法理解语义(只看文本) | 工具可以理解模块结构,支持 IDE/静态分析更好 |
对 C++ 在大规模代码工程(数十亿行级别)下的一些 架构与工具链问题的批判,并对比了 C++ 与现代语言(如 C#、Java)在模块化和构建效率方面的劣势。我们逐句深入理解:
逐句解析
Source Code Organization at Large
大型项目中的源码组织问题
Scaling beyond billions of lines of code
面对十亿行级别代码,C++源码结构难以扩展
- 传统
#include
会带来指数级的编译开销,在大项目中变得不可接受 - 缺乏真正的“组件化”,让项目结构混乱、不稳、难维护
Producing, composing, consuming components with well-defined semantics boundaries
难以生产、组合、消费具备良好语义边界的组件
- C++ 的模块化能力(在传统语法下)很弱
- 很难建立“清晰的接口”和“稳定的内部实现”之间的边界
- 更难让编译器和工具链理解和利用这些边界
Paucity of Semantics-Aware Developer Tools
缺乏语义感知的开发工具
- “paucity” = 极度匮乏
- 大部分 C++ 工具链(编译器、IDE、静态分析器)只知道 文本结构,不理解语义结构
例如:
#include "engine/core.h"
IDE 看到的是文本插入,它不知道 core.h 里导出了什么函数/类、依赖了什么模块
对比:
import engine.core;
IDE 和编译器可以准确理解这个模块导出了什么,依赖了谁,是否被改变……
Serious impediment to programmer productivity
严重影响程序员的生产效率
例如:
- 头文件改了 -> 所有依赖文件都重新编译(浪费时间)
- 工具无法精准分析依赖关系(静态检查、重构都困难)
- 多人协作时,很容易破坏结构而不自知
Great disadvantage vis-à-vis contemporary languages (C#, Java, Ada, etc.)
相较于现代语言(如 C#、Java、Ada)极大劣势
- Java 有
package
,C# 有namespace + assembly
,都是真正的模块系统 - C++ 传统上靠“约定俗成”加
#include
构建组件,没有语义级隔离
Reason not to adopt C++ / Reason to migrate away from C++
这是许多公司不采用 C++ / 甚至迁移出 C++ 的理由
你可以理解为:
“传统的 include + 链接 构建体系” 已无法支撑大规模工程的开发体验和效率
Build time Scalability of Idiomatic C++
“惯用C++”的构建时间扩展性很差
- 比如模板用得多就编译慢(因为每个
.cpp
文件都重复实例化模板) - 缺少模块缓存机制,构建不具增量性
Distributed build, cloud build, etc.
所以大项目往往要靠:
- 分布式构建系统(如 Bazel、distcc)
- 云端缓存与增量构建(如 Google’s remote exec, Microsoft’s cloud cache)
但这些只是缓解手段,不是根本解决问题
Use semantics difference to accelerate build
模块化构建系统(使用“语义差异”)可以极大加速编译
- 如果模块的导出接口没变,那么依赖它的模块不需要重新编译
- 支持增量构建、缓存复用、并行编译
C++20 Modules 就是解决这一痛点的关键尝试。
总结:这段话主旨
问题 | 原因 | 结果 |
---|---|---|
C++ 不擅长组件化 | #include 是文本复制,工具无法感知语义 | 构建慢、封装差、结构脆弱 |
工具无法理解组件关系 | 没有模块边界,工具只能基于文本做“猜测” | IDE 无法智能重构、查错,CI/CD 成本高 |
构建无法增量/并行 | 没有模块缓存机制,#include 重复编译 | 大型项目构建时间爆炸 |
与 C#/Java 相比缺乏现代工具支持 | 它们用的是语义模块系统(assembly、package) | 现代团队更倾向用 Java/C#/Go/Rust 等语言替代 C++ |
如果你是开发者或架构师,核心 takeaway 是:
C++20 Modules 不是语法糖,它是未来高效、大规模源码组织的唯一出路之一。
对 C++ 模块系统(C++20 modules)的设计初衷的总结,核心在于 为何要引入 module system,它的作用、目标、非目标。我们一一解读:
Give C++ a module system — 为什么 C++ 需要模块系统
C++ 长期以来使用的是 #include
+ 头文件的方式组织程序,但这种机制存在严重的扩展性、封装性和构建效率问题。
模块系统(module
)的引入旨在从根本上解决以下问题:
Module System 改进的四大核心点:
1. Componentization – 模块化(组件化)
模块让你可以像 Java 的
package
、C# 的assembly
一样,构建清晰、隔离的 组件边界。
- 避免“一改头文件,全项目重编译”
- 定义清晰的 API 和 implementation 隔离
- 支持更清晰的代码 ownership 与 reuse 模式
2. Isolation (from macros) – 与宏的隔离
宏(
#define
)是 preprocessor 的产物,具有全局污染性 —— 模块可以 阻止宏的跨模块传播。
- 宏定义不再像 include 那样“扩散到全世界”
- 模块边界是语义隔离的,不受宏污染影响
- 极大提升代码的可维护性和可靠性
3. Build Throughput – 构建速度大幅提升
使用模块后:
- 编译器可对模块生成中间产物
.pcm
(预编译模块文件) - 其他文件只需引用模块接口,不需重新编译头文件内容
- 可并行构建多个模块,提升大项目的构建效率
实测中,模块系统在大型项目中可带来 3~10倍编译加速
4. Support for modern semantics-aware developer tools – 支持语义感知工具链
#include
是“纯文本插入”,工具无法准确知道:
- 哪些符号被导入
- 哪些依赖可以重用或优化
模块提供明确的语义边界,IDE、LSP、静态分析器等工具可以: - 快速导航与索引符号
- 自动补全与跳转
- 精确重构和静态分析
这让 C++ 开发体验 接近 C#/Java 的现代 IDE 支持
“Deliver now, use for decades to come”
模块系统是为了解决未来几十年 C++ 构建问题设计的。
虽然是在 C++20 正式加入,但设计目标是长期可用 —— “长期投资”,未来构建系统、包管理、IDE 生态都将围绕它演化。
Target: C++17 (yes, it can be done)
虽然标准是从 C++20 起支持模块,但部分编译器(如 Clang、MSVC、GCC)允许用模块语法在 C++17 模式下启用,只要:
- 编译器支持
-fmodules-ts
或等效标志 - 使用实验性模块接口文件
.ixx
或.mpp
这意味着你 现在就可以在 C++17 项目中实验模块化设计
Non-Goals: Improve or remove the preprocessor
引入模块的目标不是:
- 移除 preprocessor
- 改善宏系统
而是:
在你需要时继续用
#define
,但在新代码中尽可能使用module
进行隔离
模块和宏可以共存。模块是构建新项目的新选项,而不是强迫你抛弃旧代码。
总结一句话:
C++ 模块系统的目标是:赋予语言现代组件化能力,隔离污染源,加速构建,支撑现代开发工具,保证未来几十年依然可扩展。
早期模块系统实现的历史和演进
MS VC 2015 Update 1 时间点的模块系统相关信息
- MS VC 2015 Update 1:
- 微软 Visual C++ 编译器在这个版本开始,尝试性地实现了 C++ 模块提案(modules proposal)。
- 当时还处于实验阶段,属于早期原型,主要是收集用户反馈和验证模块系统的可行性。
- 目标是为未来的 C++17 标准做准备,尝试将模块系统引入到语言里。
反馈和可行性验证
- 这个实验帮助微软团队了解模块系统在现实中的使用场景和问题。
- 收集到的反馈推动了模块设计和实现的完善。
- 证明模块系统在技术层面是可行的,即使当时仍有不少挑战。
Clang 的模块实现
- Clang 编译器早期实现模块系统是基于“module maps”的技术。
- module maps是一种描述源码如何被模块化的文件格式,帮助Clang理解传统代码库如何映射到模块。
- 这个实现方式侧重于向后兼容旧有代码,同时支持逐步模块化。
综述理解
- 微软和 Clang 两大主流编译器都早早着手模块支持。
- 尽管最初实现形式不同,但都为 C++20 模块标准的最终落地做了铺垫。
- 这是模块系统从提案走向现实的关键里程碑。
传统 C++ 构建模型中“翻译单元(Translation Unit,简称 TU)”的特点及其固有问题:
核心点总结
- 程序 = 一堆独立翻译单元(TUs)的集合
- 每个翻译单元独立编译,彼此之间没有直接了解对方的内部细节。
- 编译器只能看到本翻译单元内的代码,其他翻译单元的代码只通过声明(declaration)来“猜测”外部符号的存在。
- 翻译单元间的通信依赖声明(declarations)
- 外部符号(函数、变量、类型等)通过声明告诉编译器“某处存在定义”。
- 编译器并不关心这些声明对应的定义在哪个 TU 中,甚至不核对其准确性。
- 缺乏显式的依赖关系管理
- 每个 TU 不知道它实际依赖的其他 TU 或组件的具体实现细节。
- 因此编译过程不能有效验证不同 TU 之间是否保持一致性。
- 链接器解决外部符号
- 链接器阶段将不同 TU 中的外部符号绑定到对应的定义上。
- 这个过程依赖于符号名匹配(“某种方式”),并不保证类型安全或者定义唯一。
- 问题:类型安全和 ODR 违规
- 类型安全的链接问题,即链接时类型不匹配的情况难以发现。
- One Definition Rule(ODR)违规:程序中同一实体有多重不同定义,导致未定义行为,但编译时难以发现。
总体理解
传统编译模型基于“文本复制和独立编译”,导致:
- 缺乏对整体程序结构和依赖的准确掌控。
- 编译器和构建工具只能在链接阶段拼凑各个翻译单元,难以保证一致性。
- 这导致调试和维护变得复杂,代码规模大时尤为明显。
C++ 模块系统提出之前的核心问题。我们来一步步剖析这段代码和概念:
代码分解
你给出的代码可以拆分为 3 个不同源文件内容,意在演示 C++ 的传统编译模型中 ODR(One Definition Rule) 的问题。
文件划分示意:
1.cc:
int quant(int x, int y) {return x * x + y * y;
}
2.cc:
extern int quant(int, int);
int main() {return quant(3, 4);
}
3.cc:
#include <stdlib.h>
int quant(int x, int y) {return abs(x) + abs(y);
}
合法的程序组合:
(a) 1.cc
+ 2.cc
:
quant
有定义(平方和),main()
调用它。- 没有冲突,一切正常
(b) 2.cc
+ 3.cc
:
quant
有另一种定义(绝对值和),main()
调用它。- 一样也合法
问题出现:如果你链接 1.cc
和 3.cc
:
就会出现 ODR 违反(One Definition Rule violation):
quant
被定义了两次,行为未定义。- 有的编译器可能报错,有的可能静默接受,但结果不可预测。
问题核心总结:
“Useful, effective, but low-level and brittle”
- 有效:传统 C++ 编译+链接模型确实能工作几十年。
- 低层次:源文件和头文件之间的依赖是通过文本
#include
拼贴而成,编译器并不真正“理解”代码的含义。 - 易碎:一不小心就 ODR 违例,难以自动诊断。
进一步理解
“Leak implementation details to language specification”
- 所谓 泄露实现细节,指的是你无法在接口中 仅公开你想暴露的东西。
- 由于
#include
是文本拷贝,往往你必须暴露过多定义(比如内联函数、模板函数、宏等),否则无法编译。 - 这让大型系统中模块边界不清晰、耦合度高、难以重构或替换组件。
为什么这说明 C++ 模块的重要性
C++ Modules 的设计初衷就是要解决这类问题:
- 不再通过
#include
文本复制来导入定义; - 每个模块有清晰的导出接口;
- 编译器能感知模块间的真实依赖;
- 可以有效避免 ODR 问题,提高构建性能。
总结
你看到的这个例子是经典的教学案例,用于说明:
- C++ 编译模型的脆弱性;
- ODR 的难以管理;
- 模块化的必要性;
- 传统
#include
所带来的问题。
你提到的内容涉及一些与 C++ 语言设计相关的核心概念,尤其是 ODR(单一定义规则)和模块系统的缺失。我们来逐一解释:
- ODR(单一定义规则):
在 C++ 中,ODR 是指程序中的每个实体(如函数、变量或类型)应该只有 一个定义。这个规则是为了避免符号冲突,确保链接器可以正确解析符号。如果同一个函数或者类型有多个定义,程序会变得不确定,可能导致未定义行为。ODR 规则是确保 C++ 程序的行为一致和正确的重要部分。 - Bjarne Stroustrup 的引用:
Stroustrup 是 C++ 的创建者,他在这段话中提到,C++ 标准中有很多复杂的规则(比如“标记比较”),这些规则的存在是因为 C++ 缺少一个真正的模块系统。模块系统的缺失使得 C++ 语言需要通过一些复杂的、甚至是“临时的”机制来处理代码中的命名查找、重载解析和模板实例化等问题。 - “标记逐个比较”与重载解析:
在 C++ 中,如果同一个函数有多个重载(即函数名相同,但参数不同),编译器需要根据传入的参数来确定调用哪个重载版本。这个过程就涉及到“标记逐个比较”,即编译器通过比较标记(如变量名、类型等)来判断哪个版本的函数或者模板是正确的。 - 缺乏模块系统:
现代编程语言(如 Python、Rust 或 JavaScript)通常会有完善的模块或包系统,允许开发者把代码组织成不同的模块或命名空间,从而更好地管理依赖关系。而在 C++ 中,早期缺乏这样的模块系统(虽然 C++20 引入了模块的概念,但仍不如其他语言方便)。因此,Stroustrup 提到,C++ 只能依靠复杂的方式来实现这些功能,从而导致了标准中复杂的规则(如标记比较)。
总结:
Stroustrup 认为,由于 C++ 没有一个真正的模块系统,语言的设计不得不依赖一些复杂的方式来处理函数重载、命名查找和模板实例化等问题,这些问题本可以通过模块系统更简洁地解决。
这段代码看起来像是一个示例程序,旨在演示如何使用一些假设的模块和类库来处理日期和时间。虽然这段代码并没有直接符合 C++ 标准库的语法,它看起来更像是某个语言或库的设计理念,尤其是与日期操作相关的模块化处理。我们逐步解析它:
代码解释
import std.io;
import calendar.date;
int main() {using namespace Chrono; // Chrono 是一个时间/日期处理命名空间Date date { 18, Month::Sep, 2015 }; // 创建一个日期对象,表示2015年9月18日std::cout << "Today is " << date << std::endl; // 输出该日期
}
1. 模块导入 (import
)
import std.io;
import calendar.date;
- 这两个
import
语句表示导入两个模块。模块是一种更现代的代码组织方式,通常用于提高代码的可读性和可维护性。import std.io;
可能导入了标准输入输出功能,允许你进行如打印输出到控制台的操作。import calendar.date;
导入了与日期相关的功能,可能包含了定义Date
类型、月份枚举等。
需要注意的是,import
是 C++20 引入的一个新的语言特性,类似于 Python 或 JavaScript 中的模块系统。这使得 C++ 代码可以更清晰地组织,并避免了传统的头文件包含(#include
)方式所带来的重复和管理问题。
2. using namespace Chrono;
using namespace Chrono;
Chrono
很可能是一个处理时间和日期的命名空间,它封装了与时间相关的功能。using namespace Chrono;
表示使用Chrono
命名空间中的所有内容,不需要每次都写Chrono::
来引用其中的函数或类。通常这会用于代码中,来方便访问日期时间类和操作。
3. 创建日期对象
Date date { 18, Month::Sep, 2015 };
- 这行代码创建了一个
Date
类型的对象date
,它代表了 2015 年 9 月 18 日。Date
是一个自定义类,应该负责表示一个日期。它可能接受日、月、年作为参数进行初始化。Month::Sep
表示 9 月,Month
应该是一个枚举类型,列出了所有月份(Jan
,Feb
,Mar
, 等等)。
4. 输出日期
std::cout << "Today is " << date << std::endl;
- 这行代码输出字符串
"Today is "
,然后输出date
对象。为了能直接输出date
对象,这说明Date
类应该重载了输出流操作符<<
,这样就能直接使用std::cout
来打印Date
对象的内容(如18 Sep 2015
)。
5. 注释掉的代码
// #include <iostream>
// #include “Calendar/Date.h”
- 这两行注释掉的代码是传统的 C++ 中用来引入头文件的方式。它们分别是:
#include <iostream>
: 引入 C++ 标准输入输出库,用于处理std::cout
等。#include “Calendar/Date.h”
: 引入一个自定义的头文件,可能定义了Date
类及相关功能。
如果我们使用现代的模块系统(即import
),那么传统的#include
就不再需要了。
关键概念总结
- 模块化 (
import
):这段代码使用了import
关键字来加载模块,而不是传统的#include
。模块使得代码结构更加清晰,避免了头文件的冗余和重复。 - 命名空间 (
using namespace
):using namespace Chrono;
让你能够直接使用Chrono
命名空间中的功能,如Date
类、月份枚举等,而不需要每次都写Chrono::
前缀。 - 自定义类与重载操作符:
Date
类应该有相应的构造函数和<<
输出操作符重载,使得可以方便地创建日期对象并将其输出。
总结
这段代码展示了使用现代 C++ 模块系统和命名空间来简化日期处理的方式。它提供了一种更简洁、更易维护的方式来组织代码,避免了传统 C++ 代码中常见的繁琐头文件管理问题。
模块化编程(特别是在 C++ 中的模块化)和如何定义和组织相关的翻译单元(translation units, TUs)。模块化的主要目的是使代码更易于组织、可重用,并且减少编译时的依赖管理问题。
1. 模块(Module)
模块是一个包含相关翻译单元(translation units)的集合,通常具有一个明确的入口点集合。它将代码分成多个部分,从而提高了代码的组织性、重用性以及编译性能。
- 翻译单元(Translation Unit, TU):在 C++ 中,翻译单元是源文件及其包含的所有头文件经过预处理后的最终输出。在模块化的情况下,模块由多个这样的翻译单元组成。
- 模块入口点(Entry Points):模块的入口点是外部代码可以访问的声明(如函数或类)。这些声明组成了 模块接口,即暴露给外部代码的接口。
2. 模块接口(Module Interface)
模块接口包含了对外可见的声明集合。换句话说,模块接口是其他代码在使用该模块时所能访问到的部分。这些声明是模块的“公共 API”,它们定义了模块中外部消费者可以调用的函数、类、类型等。
示例:
module My.Module; // 定义模块接口
export void foo(); // foo 函数作为模块接口暴露给外部代码
module My.Module;
:这一行表示定义了一个名为My.Module
的模块。export
:用于声明哪些函数、类或类型暴露给外部使用。
3. 模块单元(Module Unit)
模块单元是模块的组成部分。它们是模块的 实现,包括模块的具体实现代码。模块单元通过包含在模块中,来定义模块的行为,但它们在外部代码中通常不可直接访问(除非通过模块接口暴露)。
每个模块单元都对应一个翻译单元(TU)。这意味着每个模块单元都是一个源文件,包含该模块的实际实现。
示例:
module My.Module; // 模块接口
export void foo(); // 公开接口声明
// 模块单元实现
void foo() {// 函数实现
}
在这个例子中,foo
函数的声明出现在模块接口中,而它的实现则在模块单元中。
4. 模块名(Module Name)
模块名是模块的符号化引用,通常用作外部代码引用该模块的标识符。例如,在 import
或 export
声明中,模块名是你用来引用模块的名称。
示例:
import My.Module; // 引入名为 "My.Module" 的模块
5. 模块的组织结构
- 模块接口:模块的公共 API,定义了外部可以访问的声明。
- 模块单元(实现):模块内部的实现,包含了模块具体的代码逻辑。
例如,一个模块可能包含多个模块单元,每个模块单元都负责模块的一部分功能:
My.Module||-- Module Unit (implementation)|-- Module Unit (implementation)|-- Module Unit (implementation)|-- Module Unit (implementation)
6. 总结
在现代 C++ 中,模块化编程旨在通过将代码分解为模块来提高代码的组织性和效率。模块化的主要特点包括:
- 模块接口:暴露给外部的公共声明。
- 模块单元:包含实现的源文件或翻译单元。
- 模块名:模块的符号化引用,用来标识该模块。
模块化可以减少头文件的依赖,改进编译性能,并使代码的结构更清晰。C++20 引入了模块功能,使得这个特性成为可能,虽然目前许多编译器和工具链的支持仍在不断完善中。
这段代码是一个 使用 C++20 模块语法 编写的日期模块(calendar.date
)的示例,来自 CppCon 2015 Gabriel Dos Reis 的演讲。他是 C++ 模块设计的重要推动者之一。
import std.io;
import std.string;
import calendar.month;
module calendar.date;
namespace Chrono {
export struct Date {Date(int, Month, int);int day() const { return d; }Month month() const { return m; }Int year() const { return y; }
private:int d;Month m;int y;
};
export std::ostream& operator<<(std::ostream& os, const Date& d) {// …
}
// …
export std::string to_string(const Date& d) {// …
}
} // namespace Chrono
下面我们逐行用中文解释代码的意义,帮助你理解模块结构和写法。
总体结构简介
这是一个名为 calendar.date
的模块定义,它:
- 导入了标准 I/O、字符串和月份模块
- 定义了一个
Date
结构体,并将其导出 - 定义了几个与
Date
相关的函数(如输出流重载、字符串转换等),也将它们导出 - 所有定义都在
Chrono
命名空间下
逐部分解析
模块头部
import std.io;
import std.string;
import calendar.month;
module calendar.date;
意义:
import
:引入其他模块,这些模块提供了必要的类型和功能:std.io
:标准输入输出(如std::ostream
)std.string
:字符串处理(如std::string
)calendar.month
:自定义的月份枚举Month
module calendar.date;
:声明这是模块calendar.date
的接口部分
命名空间 & 导出
namespace Chrono {
Chrono
是一个时间相关的命名空间,用于组织日期/时间功能,避免命名冲突
Date 结构体定义
export struct Date {Date(int, Month, int); // 构造函数:日、月、年int day() const { return d; }Month month() const { return m; }Int year() const { return y; } // 注意:Int 应为 int,可能是笔误
private:int d;Month m;int y;
};
说明:
export
:表示Date
是 模块接口的一部分,可以被外部使用- 成员函数提供访问年、月、日
- 构造函数接受 (day, month, year)
- 内部数据成员是私有的(
private
)
注意: Int year() const { ... }
中的Int
应该是int
,应该是拼写错误。
输出运算符重载
export std::ostream& operator<<(std::ostream& os, const Date& d)
{// ...
}
说明:
- 允许你使用
std::cout << date;
来输出Date
对象 export
说明这个操作符重载函数也对模块使用者可见
字符串转换函数
export std::string to_string(const Date& d)
{// ...
}
说明:
- 定义了
to_string()
,将Date
转换为std::string
- 这在调试或日志记录时非常有用
小结:模块接口结构
这段代码实现了一个清晰的模块化设计,具备以下特点:
组件 | 功能 |
---|---|
module calendar.date; | 声明模块接口 |
export struct Date | 日期类型,暴露给外部模块 |
export operator<< | 重载输出操作符,便于打印 |
export to_string() | 转为字符串,便于展示 |
namespace Chrono | 将所有内容封装在 Chrono 命名空间中 |
对比传统方式(非模块化)
传统 C++ 写法可能是这样:
// Date.h
namespace Chrono {struct Date { ... };std::ostream& operator<<(std::ostream&, const Date&);std::string to_string(const Date&);
}
// Date.cpp
#include "Date.h"
用模块替代 .h
+ .cpp
的方式:
- 避免了头文件保护(
#ifndef
等) - 加快编译速度
- 更清晰的接口/实现分离
- 更好地支持 IDE、依赖分析等工具
如果你想,我还可以: - 补上
Date
构造函数、to_string
和输出运算符的实现 - 展示如何从其他文件导入
calendar.date
模块并使用它
模块机制 的一个非常简洁的示例。它旨在展示用 模块接口(module interface)来组织代码的简洁性与清晰性。
import std.io;
import std.string;
import calendar.month;
module calendar.date;
export namespace Chrono {
struct Date {Date(int, Month, int);int day() const { return d; }Month month() const { return m; }Int year() const { return y; }
private:int d;Month m;int y;
};
std::ostream& operator<<(std::ostream& os, const Date& d) {// …
}
// …
std::string to_string(const Date& d) {// …
}
} // namespace Chrono
整体目标
创建一个名为 calendar.date
的模块,导出 Chrono
命名空间,其中包含一个日期类 Date
,及两个相关函数:
operator<<
:输出Date
to_string()
:将Date
转为字符串
模块头部导入
import std.io;
import std.string;
import calendar.month;
module calendar.date;
说明:
import std.io
:导入输入输出支持,如std::cout
、std::ostream
import std.string
:导入字符串处理支持,如std::string
import calendar.month
:导入自定义模块,定义了Month
枚举或类型module calendar.date
:声明当前模块名为calendar.date
相比传统
#include
,模块import
更快、更安全、避免重复包含。
模块接口导出(模块公开 API)
export namespace Chrono {
说明:
export namespace
:将Chrono
命名空间中的内容整体导出- 外部使用该模块后,即可访问
Chrono::Date
,Chrono::to_string
等
Date
类型定义
struct Date {Date(int, Month, int);int day() const { return d; }Month month() const { return m; }Int year() const { return y; }
private:int d;Month m;int y;
};
说明:
成员 | 作用 |
---|---|
Date(int, Month, int) | 构造函数:创建一个具体日期(如 2024年6月8日) |
day() , month() , year() | 提供对私有成员的只读访问 |
int d 、Month m 、int y | 日期的日、月、年数据 |
注意: | |
Int year() const 应为 int year() const ,Int 是拼写错误。 |
输出流重载函数
std::ostream& operator<<(std::ostream& os, const Date& d)
{// …
}
说明:
- 重载
<<
,支持std::cout << date;
- 通常实现为:
return os << d.day() << " " << d.month() << " " << d.year();
字符串转换函数
std::string to_string(const Date& d)
{// …
}
说明:
- 将
Date
转为字符串 - 类似于标准库中已有的
std::to_string()
- 用法:
std::string s = Chrono::to_string(date);
总结:代码结构分析表
模块部分 | 内容或功能 |
---|---|
import ... | 引入所需模块(标准库 & 自定义) |
module calendar.date | 声明模块名称 |
export namespace Chrono | 模块接口部分,导出整个命名空间 |
struct Date | 日期类,提供构造和访问函数 |
operator<< | 支持打印日期对象 |
to_string() | 支持将日期对象转为字符串 |
模块 vs 传统头文件
特性 | 模块(C++20) | 传统头文件 (#include ) |
---|---|---|
代码清晰 | 明确导出接口 | 接口/实现混杂 |
编译性能 | 编译一次,可缓存 | 每次都要重新处理头文件 |
重复包含问题 | 自动避免 | 需要手动写 include guard |
工具支持(IDE 等) | 更好 | 依赖分析困难 |
结尾说明
这个示例正是要表达 “模块接口写法就该像这样简洁自然” 的理念 —— 就像 Python/Java 的模块系统那样,让你只关注功能,而不是头文件、宏、预处理器指令等。
如果你想:
- 完善构造函数/输出/to_string 实现
- 学习如何在其他文件中
import calendar.date
并使用Date
- 学习模块的实现文件(module implementation unit)怎么写
使用了 #include <iostream>
而不是 import std.io
下面我们从结构、模块语法、设计意图几方面逐条深入分析,并用中文解释其意义。
文件头部
#include <iostream> // 使用传统方式包含 iostream
import std.string; // 模块化导入 std::string
import calendar.month; // 导入自定义模块 calendar.month
module calendar.date; // 声明当前模块名称
分析:
语句 | 含义 |
---|---|
#include <iostream> | 使用传统的头文件方式引入输入输出库(如 std::ostream ) |
import std.string | 模块化方式引入字符串类型 |
import calendar.month | 引入自定义模块,提供 Month 枚举 |
module calendar.date | 声明当前是 calendar.date 模块的接口文件 |
为什么混用
#include
和import
?
命名空间与模块导出
namespace Chrono {
含义:
- 将所有功能放在
Chrono
命名空间中(类似 STL 中的std::chrono
) - 结构更清晰,防止名字冲突
导出结构体 Date
export struct Date {Date(int, Month, int);int day() const { return d; }Month month() const { return m; }Int year() const { return y; } // 拼写错误,应该是 int
private:int d;Month m;int y;
};
分析:
项目 | 说明 |
---|---|
export struct Date | 将 Date 结构体作为模块接口导出 |
构造函数 Date(int, Month, int) | 初始化一个日期对象(年/月/日) |
成员函数 | 访问 day() 、month() 、year() |
私有成员 | d (日)、m (月)、y (年) |
注意拼写错误:Int 应该为 int 。 |
输出运算符重载
export std::ostream& operator<<(std::ostream& os, const Date& d)
{// …
}
含义:
- 定义
<<
操作符,以便std::cout << Date{...};
能工作 - 使用
export
使外部模块可用
字符串转换函数
export std::string to_string(const Date& d)
{// …
}
含义:
- 将
Date
转换为字符串,例如"2024-06-08"
- 导出函数供其他模块调用
总结:结构分析表
部分 | 内容 | 模块导出? |
---|---|---|
#include <iostream> | 引入传统头文件(未模块化) | 否 |
import std.string | 字符串模块 | 否 |
import calendar.month | 自定义枚举类型模块 | 否 |
module calendar.date | 当前模块名称 | — |
export struct Date | 日期类 | |
export operator<< | 打印重载 | |
export to_string() | 转为字符串 |
关键点归纳
- 模块接口可用
export
导出类型、函数 - 模块内部仍可使用传统的
#include
,尤其在过渡期或模块未完全支持时 Chrono::Date
是最终暴露给其他模块的核心类型- 该模块简洁清晰,避免了传统
.h + .cpp
的冗余和依赖问题
如果你想深入:
我可以继续帮你:
- 完善
Date
的构造函数与函数体 - 展示如何在其他模块中
import calendar.date
并使用 - 比较模块接口与实现单元的写法(如
module;
分隔线)
C++ 模块(modules)机制 的设计动机和行为规则的说明,核心思想是:
模块是对传统头文件系统的现代化替代,旨在提升代码隔离性、构建速度与可维护性。
下面我将这段内容逐条用中文解释,并附上详细的理解分析。
1. Modules are isolated from macros
模块与宏是隔离的。
解释:
- 模块的接口是一个编译过的导出实体集合(compiled set of exported entities),不是简单的文本替换。
- 它不会受到导入它的翻译单元(TU)中的宏的影响。
- 同样,模块内部定义的宏也不会“泄漏”到导入它的代码中。
意义: - 避免宏引发的命名冲突和不可预测行为。
- 提升模块的可预测性和安全性。
- 模块不再是“文本拷贝”,而是语义隔离的编译单元。
2. A unique place where exported entities are declared
所有导出实体都有唯一定义处。
解释:
- 一个模块的接口必须在某个模块单元(Translation Unit, TU)中唯一地声明。
- 一个模块可以只包含一个 TU,也可以由多个 TU 组成,但必须有一个 TU 专门负责导出(interface TU)。
意义: - 避免重复定义。
- 明确模块 API 来自哪里,便于维护与查找。
3. Every entity is defined at exactly one place, and processed only once
每个实体只在一个地方定义、只被处理一次。
解释:
- 模块中的函数、类、变量只会被编译器处理一次。
- 实体属于定义它的模块。
- 唯一例外是:模板的完整语义分析(template instantiation)可能会在多个地方进行。
意义: - 大幅减少重复解析(与传统头文件中
#include
多次处理不同)。 - 提升构建效率。
- 模板除外,因为它们是“延迟编译”的语言特性。
4. Exception is made for “global module”
“全局模块” 是个特例,用于兼容旧代码。
解释:
- 全局模块(global module)允许在非模块化代码中使用模块定义的实体。
- 它是为实现“平滑过渡”设计的。
意义: - 使模块机制能渐进式引入到大型老代码库中。
- 保持与非模块代码的互操作性。
5. No new name lookup rules
不会引入新的名称查找规则。
解释:
- C++ 名称查找(name lookup)机制已经非常复杂。
- 模块不会改变它,只是以更清晰的方式管理可见性与作用域。
意义: - 避免语言规则进一步复杂化。
- 保持对现有编译器行为的兼容性。
6. Modules do not replace header files
模块不会完全取代头文件。
解释:
- 如果接口 heavily 使用宏(比如
<windows.h>
),那么仍然需要保留传统头文件。 - 但可以将内部组件模块化,例如:
// Still macro-heavy public header #include <legacy_api.h> // Internals use modularized components import mymodule.core;
意义:
- 模块是新特性,但允许与旧系统共存。
- 允许项目逐步模块化,而非一刀切。
7. Build time is faster (goal)
模块的目标之一是:加快编译速度。
解释:
- 因为模块是一次性编译、缓存的二进制接口,不像头文件那样重复解析。
- 在大型工程中,模块能显著缩短构建时间。
成果预期: - 更快的全量编译(特别是在大型项目中)
- 更少的冗余处理(特别是对大型头文件)
总结图表
特性 | 说明 | 传统头文件 vs 模块 |
---|---|---|
宏隔离 | 宏不会互相污染 | 常有副作用 → 模块隔离 |
唯一定义 | 每个实体定义一次 | 重复处理 → 一次编译 |
名字查找 | 没有新增规则 | 保持兼容 |
与旧代码兼容 | 通过全局模块 | 渐进迁移 |
宏接口 | 宏仍需要头文件支持 | 可共存 |
构建性能 | 编译更快 | 头文件慢 → 模块快 |
如果你希望,我还可以为你: |
- 举例展示宏污染在传统头文件中的问题
- 演示如何将一个宏-heavy 接口逐步模块化
- 分析实际项目中使用模块带来的构建性能提升
C++ 模块设计中的几个核心原则和特性,我帮你详细解释和理解一下:
1. Module owns entities in its purview
模块“拥有”它所定义的实体(函数、类型、变量等)
- 这意味着模块内部定义的每个实体都归该模块管理,明确归属,避免重复定义和冲突。
- 体现了模块的封装性和独立性。
2. ODR: every entity is defined exactly once
符合 ODR(One Definition Rule,唯一定义规则)
- 每个实体(函数、类、变量)在程序中必须且只能定义一次。
- 模块机制保证在整个程序中只编译和定义一次对应实体,避免头文件重复包含带来的多重定义问题。
3. Order of consecutive import declarations is irrelevant
多个连续的 import
声明顺序无关紧要
- 无论你怎样排列
import A; import B;
,不会影响程序行为。 - 这提高了代码的可维护性和模块之间的独立性。
4. Modules are isolated from macros
模块与宏相互隔离
- 模块内部的宏不会影响外部,外部宏也不会影响模块内部。
- 避免宏污染和潜在的编译问题。
5. Import declarations only makes name available– You don’t pay for what you don’t use
导入模块只带来名字可见性 —— 你不会为未使用的部分付出代价
- 模块导入不会自动引入所有实体的编译或链接开销。
- 只有真正用到的实体才会被编译和链接。
- 类似于“按需加载”,提升编译效率。
6. Module metadata suitable for use by packaging systems
模块的元数据适合被打包系统使用
- 模块编译后带有结构化的元数据(如依赖关系、版本等)。
- 有助于包管理器自动管理模块依赖和版本控制。
- 这为构建复杂系统和跨模块协作提供基础。
7. Modules provide ownership
模块提供“所有权”的概念
- 每个模块负责它定义的实体,避免命名冲突、重复定义。
- 明确代码归属,方便维护和重用。
总结
C++ 模块机制从根本上解决了传统头文件系统的痛点,做到:
- 唯一定义,避免重复
- 模块间独立,顺序无关
- 隔离宏污染
- 只为使用付费
- 方便包管理
- 清晰所有权归属
这为大型项目的构建速度和代码管理带来革命性改善。
这段代码展示了一个 C++20 模块 Calendar.Month
的接口定义,聚焦于定义一个导出的枚举 Month
及其相关输出操作符。你提到“Module purview”想了解模块视域(即模块对实体的拥有与管理),我帮你详细拆解并解释。
代码结构与核心内容解析
#include <iostream> // 传统头文件,提供 std::ostream
import Enum.Utils; // 导入自定义模块,推测提供 bits::rep() 等工具
module Calendar.Month; // 声明模块名 Calendar.Month
namespace Chrono {export enum class Month { Jan = 1, Feb, Mar, Apr, May, Jun, /*…*/ };constexpr const char* month_name_table[] = {"January", "February", /* … */};export std::ostream& operator<<(std::ostream& os, Month m){assert(m >= Month::Jan and m <= Month::Dec);return os << month_name_table[bits::rep(m) - 1];}
}
详细理解
1. 模块所有权(Module Purview)
- 这个模块
Calendar.Month
拥有(own)了Chrono::Month
枚举类型和相关的输出运算符。 - 任何导入该模块的代码,都可以直接访问
Chrono::Month
及其operator<<
,模块负责它们的定义和实现。 - 这符合模块“唯一定义”和“封装所有权”的原则。
2. export
关键字
export
用于声明模块对外公开的接口。export enum class Month
表明Month
是模块的公共 API。- 同样
export operator<<
表明这个重载函数也对外可见。
3. 模块隔离和依赖
- 模块通过
import Enum.Utils
获得了bits::rep()
功能,这样模块内部实现细节(如bits::rep()
)被抽象出来,保持清晰职责划分。 - 同时,模块内部可以自由使用包含的头文件
<iostream>
来实现需要的功能。
4. 模块接口的意义
- 模块的视域(purview)即它所“拥有”的实体范围,这里指枚举和操作符。
- 模块保证这些实体只在这里定义一次,且外部通过导入访问,不会重复定义。
- 提高编译效率,避免宏和文本替换带来的脆弱性。
5. month_name_table
的用法
month_name_table
是模块内部的constexpr
字符串数组,用于将枚举值映射到月份名字符串。- 通过
bits::rep(m)
得到Month
枚举的整数值(推测bits::rep
是返回枚举底层整数的工具),并减 1 作为数组索引。 assert
保护防止传入无效枚举值。
总结
特点 | 说明 |
---|---|
模块拥有权 | 该模块定义和管理 Month 及相关功能。 |
导出接口 | Month 枚举和 operator<< 通过 export 公开。 |
实现依赖模块 | 依赖 Enum.Utils 模块来实现辅助功能。 |
模块隔离宏与重复定义 | 避免宏污染和多重定义问题。 |
代码组织清晰 | 枚举与打印逻辑在一个模块内,方便维护和重用。 |
如果你想,我可以帮你: |
- 详细讲解
bits::rep()
可能的实现 - 演示如何在另一个文件或模块里
import Calendar.Month;
并使用Month
- 展示模块对编译时间优化的效果
链接(linkage)和符号命名的现状及挑战,尤其是名称“修饰”(name mangling)在现代编译器和语言规范中的作用和问题。
1. Strings and bytes – Name “mangling” or name “decoration”
- 编译器会对函数名、变量名等符号进行“修饰”(mangling),把它们转换成带有额外信息的唯一字符串(比如函数参数类型、命名空间等)。
- 这个过程是为了支持 函数重载、命名空间 等 C++ 特性。
- 但这种“修饰名”实际上是一种对语言内部实现细节的泄露,也就是说,名称修饰规则成了语言规范的一部分,这本不该是程序员直接关心的。
2. Unfortunate leakage to language specification
- 这种名字修饰规则的细节必须被语言标准部分涵盖,或者被编译器规范化支持。
- 但名字修饰本质是编译器实现细节,标准暴露了这些细节,造成语言层和实现层的耦合。
3. Standard “linkage” far behind the practice and needs of our time
- 标准定义的“链接方式”(linkage)不足以满足现代软件开发的实际需求。
- 现代编译器提供了更多灵活的符号可见性控制,但标准没跟上。
4. Examples: GCC and Clang support linkage “visibility”
现代编译器对符号可见性的支持:
- default:默认可见,符号对所有模块可见。
- hidden:隐藏符号,不对外暴露,减少符号冲突。
- internal:符号只在当前编译单元可见。
- protected:符号对外可见但不能被重定义(符号保护)。
这些机制用于优化符号导出,减少符号冲突,提高链接效率。
5. VC++ supports: dllimport and dllexport
微软编译器用来管理动态链接库(DLL)符号导入和导出的关键字:
- dllimport:标记符号为从 DLL 导入。
- dllexport:标记符号为导出到 DLL。
这是 Windows 平台动态库机制的一部分,用于控制符号的可见性和链接。
总结理解
- 名称修饰(name mangling)是为支持 C++ 特性而引入的符号编码方式,但它暴露了语言实现细节,造成语言规范的复杂性。
- 标准的链接规则滞后于现代编译器和实际开发需求,现代编译器提供了更细粒度的符号可见性控制(visibility),帮助管理符号导入导出,优化链接过程。
- 不同平台(如 GCC/Clang 和 VC++)有不同的符号可见性和导出机制。
C++20 模块的编译过程和生成物 相关,我帮你理清它们的含义和关系:
1. src.ixx
.ixx
是模块接口单元(module interface unit)的常用扩展名,表示这是一个模块接口源文件。- 这个文件里通常写有
module My.Module;
和export
声明,定义模块的对外接口。
2. cl –c /module
- 这是用微软的编译器(
cl.exe
)进行模块编译的命令行参数示例。 /module
表示启用模块编译模式,告诉编译器这是在处理模块接口单元。-c
表示只编译,不链接。
3. src.obj
- 编译
src.ixx
后生成的目标文件(object file)。 - 包含模块接口单元的机器码和符号信息。
4. Module metadata
- 编译器会生成一个模块元数据文件,通常后缀是
.ifc
(interface file)。 - 这个文件是模块接口的二进制表示,包含模块中导出的实体的描述、接口信息等。
- 其他编译单元通过这个
.ifc
文件来导入模块。
5. My.Module.ifc
- 这是编译
My.Module
模块接口单元时生成的接口文件。 - 这个文件用来给后续的翻译单元提供模块接口描述,实现模块之间的编译单元隔离和高效重用。
总结理解
文件/命令 | 作用 |
---|---|
src.ixx | 模块接口源文件 |
cl -c /module | 用微软编译器编译模块接口单元 |
src.obj | 编译产出的目标文件 |
My.Module.ifc | 模块接口的二进制元数据文件 |
这个流程体现了模块编译的关键优势: |
- 模块接口只需编译一次,其他单元通过
.ifc
文件复用,减少重复编译。 - 模块元数据清晰描述接口,保证模块间的隔离与高效。
这段内容描述的是用微软编译器(cl.exe)编译 C++20 模块接口单元的具体命令和生成的文件:
1. src.cxx
- 这是模块接口单元的源代码文件,通常包含
module My.Module;
语句和导出接口。 .cxx
是 C++ 源文件扩展名,有时也用.cpp
。
2. cl –c /module /module:interface
cl
是微软命令行编译器。-c
表示只编译不链接,生成目标文件。/module
让编译器开启模块支持。/module:interface
明确告诉编译器这是一个模块接口单元(Module Interface Unit)。
这个参数的作用是告诉编译器,它正在编译模块的接口部分,需要生成相应的模块接口文件。
3. src.obj
- 编译后生成的目标文件,包含模块接口单元的机器码等信息。
- 这个文件用于后续链接。
4. My.Module.ifc
- 编译时自动生成的模块接口文件(Interface File)。
.ifc
文件是模块的元数据,存储模块接口的二进制表示。- 其他编译单元导入该模块时会使用这个
.ifc
文件。 - 它使得模块的接口信息可复用且只需编译一次。
总结:
元素 | 说明 |
---|---|
src.cxx | 模块接口源代码文件 |
cl -c /module /module:interface | 编译模块接口单元的命令,生成 obj 和 ifc 文件 |
src.obj | 编译产物,目标文件 |
My.Module.ifc | 模块接口文件,模块的元数据,供导入使用 |
进一步说明:
- 模块接口单元必须生成
.ifc
文件供其他编译单元使用,替代了传统头文件的文本包含机制。 .ifc
文件提高编译效率和模块接口的稳定性。/module:interface
是区分接口单元和实现单元的关键参数。
这段说明的是用微软编译器编译一个模块的使用(导入)单元的流程,我帮你详细拆解:
1. src.cxx
- 这是一个模块导入单元的源文件,不是模块接口单元。
- 里面可能有
import My.Module;
语句,用来使用之前定义好的模块。
2. /module:reference My.Module.ifc
- 这个编译选项告诉编译器:
- “我这次编译需要用到模块
My.Module
。” - 并且模块接口信息在
My.Module.ifc
文件里。
- “我这次编译需要用到模块
- 编译器通过读取这个
.ifc
文件知道模块导入单元可以访问哪些实体。
3. cl –c /module
cl
编译器执行编译操作,带上/module
以启用模块支持。-c
表示只编译不链接。
4. src.obj
- 编译完成后生成目标文件。
- 这个目标文件链接时会使用
My.Module
中的定义。
整体流程理解
步骤 | 说明 |
---|---|
src.cxx | 模块导入单元(使用模块) |
/module:reference My.Module.ifc | 指定要引用的模块接口文件,告诉编译器模块定义在哪里 |
cl –c /module | 编译时启用模块支持,只生成目标文件 |
src.obj | 编译输出的目标文件,包含模块导入单元的代码 |
总结
- 模块接口单元先编译生成
.ifc
文件(模块接口文件)。 - 模块导入单元通过
/module:reference My.Module.ifc
指定引用模块接口文件。 - 编译器利用
.ifc
了解模块导入的接口,完成编译。
这段描述的是编译一个引用模块的代码单元,但是 .ifc
文件名不一定和模块名一致,可以用任意文件名,只要它包含正确的模块接口信息。
详细解释
1. src.cxx
- 你的源代码文件,里面包含了
import My.Module;
或其他模块导入语句。
2. /module:reference AnyFilename
- 告诉编译器:“模块接口信息在
AnyFilename
文件中”,这个文件是模块接口文件.ifc
的别名或路径。 - 这里的
AnyFilename
不必和模块名字完全匹配,只要文件内容是正确的模块接口文件。 - 这种灵活性允许模块接口文件使用自定义的文件名或路径。
3. cl –c /module
- 使用
cl
编译器开启模块支持,进行编译,不链接。
4. src.obj
- 目标文件,编译结果。
总结
内容 | 说明 |
---|---|
src.cxx | 引用了某个模块的源代码 |
/module:reference AnyFilename | 指定模块接口文件的文件名,灵活不固定 |
cl –c /module | 编译命令,启用模块支持 |
src.obj | 生成的目标文件 |
这说明: |
- 编译模块导入单元时,模块接口文件名可以自由指定,不一定要和模块名相同。
- 编译器只要拿到正确的模块接口文件就能正确编译。
你这段是微软编译器(cl.exe)关于模块相关命令行选项的说明,我帮你总结理解:
/module
- 开启模块支持,告诉编译器启用 C++20 模块的相关功能。
- 启用后,新增关键字:
module
、import
、export
可以被识别。
/module:interface
- 强制将当前源代码当作**模块接口单元(Module Interface Unit)**来编译。
- 这是你定义模块接口的地方,比如
module My.Module;
和export
的声明。
/module:reference <filename>
- 指定一个**已编译好的模块接口文件(.ifc 文件)**的路径。
- 编译器会在这个文件中查找模块接口信息,以支持**模块导入单元(Module Implementation or Import Unit)**的编译。
- 相当于告诉编译器“这里是模块接口描述,用来导入模块”。
/module:search <directory>
- 指定一个目录,供编译器搜索模块接口文件(.ifc)。
- 当导入模块时,如果没有显式指定
.ifc
文件,编译器会去这个目录寻找对应模块的接口文件。 - 方便组织模块文件结构,类似传统的头文件搜索路径。
总结
选项 | 作用 |
---|---|
/module | 开启模块支持,识别模块相关新关键字 |
/module:interface | 将当前文件当作模块接口单元编译 |
/module:reference | 指定具体模块接口文件(.ifc),用于导入模块 |
/module:search | 指定目录,编译器查找模块接口文件 |
1. 能否把头文件当作模块来用?
- 答案是**“可以,但条件是这个头文件必须‘表现良好’(well-behaved)”**。
- 意思是,头文件里面不能有破坏模块机制的宏、重复定义等,结构要清晰,符合模块的要求。
2. /module:export vec.cxx /module:name std.vector
- 这个命令示例说明了可以用编译器参数把一个源文件(比如
vec.cxx
)当作模块接口单元导出, - 并且显式给模块指定名字,比如
std.vector
。 - 实际上,这样就相当于把原本的传统实现(或头文件)“包装”成模块。
3. 选择性导出“表现良好”的宏
/module:exportMacro <macroName>
- 允许模块导出特定的宏,而不是默认全部隐藏宏。
- 这对兼容旧代码或需要导出宏的情况很有用,但必须保证宏不会破坏模块的隔离。
总结理解
- 传统头文件可以在一定条件下被转成模块接口单元,方便迁移和复用。
- 编译器提供了参数支持这种“包装式”模块导出。
- 宏的导出被限制,只能显式指定,以保持模块的整洁和安全。
C++模块特性的开发过程中的一些困惑和期待:
“Can I get it in C++17?” — We are trying.
- 有人问能不能在C++17标准里用上模块,回答是“我们正在努力”。
- 说明模块功能在C++17里没正式支持,但社区和标准化组织在为后续标准努力实现它。
“Pretty please, give me modules now” — We are trying
- 有人急切希望立刻用上模块,回应还是“我们正在努力中”,强调模块的开发和推广不是一蹴而就。
“What about the IFC format” — After modules.
- 有人关心模块接口文件(IFC,Interface File)的格式,回应是这在模块实现之后会处理。
- IFC格式是模块实现的关键,但需要先实现模块本身。
“Really??? Are you kidding?” — No, but I can use some help
- 对某些方案或计划感到惊讶,表示不是开玩笑,但确实需要更多人参与协助。
“What about inaccessible members? Are they exported too?” — Yes, but I hope we find a good solution
- 有人问模块里非公开成员(private、protected)是否也会被导出,回答是“会导出,但希望能找到更好的办法”。
- 这是模块设计中遇到的复杂问题,如何处理隐藏成员的导出还在探讨。
“Come on!”
- 表达对进展缓慢或问题复杂的无奈和催促。
总结
这段话生动反映了模块作为C++新特性在标准化和实现过程中遇到的挑战和大家的期待,也表现了开发者社区希望尽快成熟这项技术的心情。