如何用C++模块构建存在循环依赖的项目?
针对你遇到的循环依赖问题,不需要合并文件或者退回到头文件包含,C++20模块的**模块分区(Module Partitions)**就是专门为这种场景设计的——既能保持文件分离的结构,又能解决循环依赖,还能实现内联成员函数的定义。
方案1:使用模块分区拆分模块
把你的JSON库做成一个顶层模块json,然后将JsonValue和JsonArray分别作为该模块的两个分区:value和:array。同模块的分区之间可以互相导入,编译时会被视为整体的一部分,不会出现独立模块的循环依赖问题。
具体实现步骤:
- 创建顶层模块接口文件(比如
json.cppm,扩展名依编译器习惯,也可用.ixx):
export module json; // 导出两个分区的内容,外部使用时只需导入json模块 export import :value; export import :array;
- JsonValue的分区文件(
json_value.cppm):
export module json:value; // 直接导入同模块的array分区,获取JsonArray的完整定义 import :array; export class JsonValue { std::variant<double, JsonArray> data; // 成员函数可以直接内联实现,无需分离到源文件 public: bool is_array() const { return std::holds_alternative<JsonArray>(data); } };
- JsonArray的分区文件(
json_array.cppm):
export module json:array; // 导入同模块的value分区,获取JsonValue的完整定义 import :value; export class JsonArray { std::vector<JsonValue> array; // 同样支持内联实现成员函数 public: size_t size() const { return array.size(); } void push_back(JsonValue val) { array.push_back(std::move(val)); } };
为什么这个方案可行?
模块分区属于同一个顶层模块的组成部分,编译时编译器会处理分区之间的依赖关系,不会像独立模块那样要求完全无循环导入。同时,外部代码只需导入json模块就能使用两个类,保持了原有的使用体验。
方案2:前置声明配合模块实现单元(仅适用于部分场景)
如果你的类成员不需要直接持有对方的实例(比如用指针/引用),可以在模块接口中前置声明类,然后在模块的实现单元(非接口部分)导入依赖模块。但你的例子中JsonArray用了std::vector<JsonValue>,需要JsonValue的完整定义,所以这个方案不适用你的场景,但可以作为补充思路:
比如json:array的接口文件:
export module json:array; // 前置声明JsonValue export class JsonValue; export class JsonArray { // 只能用指针/引用,不能直接持有实例 std::vector<std::unique_ptr<JsonValue>> array; // ... };
然后在实现单元json_array_impl.cpp中导入json:value来实现成员函数:
module json:array; import json:value; // 实现JsonArray的成员函数 size_t JsonArray::size() const { return array.size(); }
但这种方式需要修改原有代码结构,不如模块分区优雅。
总结
模块分区是最贴合你需求的方案:既保留了每个类对应一个文件的结构,解决了循环依赖,还能让你直接在类定义中内联实现成员函数,无需分离到源文件。目前主流编译器(GCC 11+、Clang 15+、MSVC)都已支持该特性。
内容的提问来源于stack exchange,提问作者oisyn

