C++export导出模块接口声明
C++ export 不是“导出DLL”的开关,而是模块系统的语法钥匙
刚接触 C++20 模块(Modules)时,我翻文档看到 export 关键字,下意识以为它和 .dll 里的 __declspec(dllexport) 是一回事——毕竟都叫“导出”。结果写完 export module math;,再 import math;,编译器报错:“module interface unit not found”。折腾半小时才发现,export 从不生成二进制符号,它只决定‘谁能在模块外被看到’——就像公寓楼的门禁卡,刷得过不代表你搬进了隔壁单元,只是允许邻居来串门。
C++20 的 export 是模块接口的“可见性闸门”,不是链接器的“打包指令”。它解决的根本问题,是传统头文件机制里那团剪不断理还乱的依赖纠缠:宏污染、重复解析、隐式依赖、ODR(One Definition Rule)踩雷……而 export 的真实作用,是让开发者亲手划定模块的契约边界——哪些声明是“对外服务协议”,哪些是“内部施工日志”。
一个模块接口单元(.ixx 或 .cppm)里,export 必须出现在模块声明之后、首个非导出声明之前。比如:
export module utils;
export namespace io {
export void log(const char* msg); // ✅ 导出函数声明
}
export class Config { // ✅ 导出类定义(含所有成员声明)
public:
export Config(); // ⚠️ 成员函数若在类内定义,默认随类一起导出;若在类外定义,必须显式加 export
int value() const;
};
// ❌ 这里不能再出现非 export 声明(如 static int x;),否则编译失败
注意这个细节:类定义本身带 export,就等于把整个类的接口契约公开了;但类外定义的成员函数,必须单独加 export 才能被导入者调用。我第一次栽在这儿——写了 export class Parser,却忘了给 Parser::parse() 加 export,结果导入方调用时报 “undefined reference”。不是链接失败,是根本没导出符号声明,编译器压根不把它放进模块接口视图里。
更易忽略的是命名空间导出。export namespace ns { ... } 并非导出整个命名空间体,而是导出其内部所有 export 标记的实体。你可以混用:
export namespace net {
export struct Endpoint { /* ... */ };
void helper(); // ❌ 不导出,仅模块内部可用
}
这时候 helper() 对模块使用者完全不可见,哪怕它和 Endpoint 在同一命名空间里。模块的可见性不继承、不传播,只认 export 这一个印章——这点比头文件 #include 严格得多,也干净得多。
实际项目中,我习惯把模块拆成三层:
- 顶层
export module xxx;—— 模块身份证; - 中间
export namespace xxx::detail { ... }—— 把真正需要暴露的工具类型/函数放这里,避免污染全局; - 底层
namespace impl { ... }(无 export) —— 实现细节、临时模板特化、调试辅助类,统统锁死在模块内。
这样既满足封装,又避免用户误用内部实现。比如 json 模块里,json::value 是导出的公共类型,但 json::impl::parser_state 绝不导出——改它不影响 ABI,也不用担心用户代码悄悄依赖它。
最后提醒一个硬约束:export 不能修饰变量定义(除非是 constinit constexpr 变量),也不能修饰模板特化声明(特化本身必须在模块内完成)。想导出常量?用 export inline constexpr int max_size = 1024;;想导出模板?直接 export template<typename T> struct vector { ... }; 即可——模板定义本身就是接口,无需额外标记成员。
export 不是魔法开关,它是模块时代里一次清醒的“减法”:删掉头文件里那些本不该暴露的宏、内联实现、条件编译分支,只留下干净、明确、可验证的契约。当你开始为每个 export 问一句“这个真的需要被别人看见吗?”,模块设计才算真正落地。
合上编辑器前,不妨检查一遍:所有 export 后面跟着的,是不是都经得起“删除后用户代码是否还能编译通过”的拷问?如果是,那你的模块接口,已经比大多数头文件更诚实了。


还没有评论,来说两句吧...