我有几个与游戏相关的类,例如
StateManager
、Window
、EventManager
等等。
这些类位于不同的模块中。模块
Events
包含 EventManager
类,模块 States
包含 StateManager
类和相关类,Window
类位于单独的 Window
模块中,依此类推。
在某些情况下,这些系统之一需要访问其他系统之一的某些功能或状态(例如,
StateManager
可能会通知 EventManager
,当前 StateType
已更改)。为了允许所有这些不同系统之间的访问和交换,在“前模块”时代,我使用了一个轻量级的SharedContext
对象,它只保存一些指向所有相关类的指针:
#pragma once
class Window;
class EventManager;
class TextureManager;
class GUI_Manager;
struct SharedContext{
Window* m_window = nullptr;
EventManager* m_eventManager = nullptr;
TextureManager* m_textureManager = nullptr;
GUI_Manager* m_gui_manager = nullptr;
};
这个
SharedContext
对象创建一次,将对预先存在的对象的所有引用加载到其成员变量中,然后进行传递,因此所有接收 SharedContext
的对象都可以访问附加的类,例如 Window
、EventManager
、TextureManager
、GUI_Manager
等
这种方法与模块根本不兼容。主要问题是,一旦在模块中声明了类名,我就不允许声明该类名。因此,这里的前向声明格式不正确,代码将无法编译,因为所有前向声明的类已经存在于模块中。
SharedContext
在逻辑上不属于任何模块,但将其设为模块(这样我可以导出前向声明的类)也不起作用。
以下最小示例抱怨使用 GCC 14.2 时出现错误:
“错误:对‘EventManager’的引用不明确”;注意:候选者是:'class EventManager@SharedContext' ...注意:'class EventManager@Events'
SharedContext.cppm
export module SharedContext;
export class EventManager;
export struct SharedContext{
EventManager* m_eventManager = nullptr;
};
事件.cppm
export module Events;
export class EventManager{
};
主.cpp
import Events;
import SharedContext;
int main() {
EventManager manager;
SharedContext context;
context.m_eventManager = &manager;
return 0;
}
导入所有模块而不是在
SharedContext
文件中向前声明所需的类可能会引入循环依赖关系,因为当我导入模块 States
以便能够创建指向 StateManager
对象的指针时,许多其他类也被导入,它们是 States
模块的一部分。因此,当这些类中只有一个需要 SharedContext
时,我会创建一个循环依赖项(SharedContext
导入模块 States
,该模块在其模块分区之一中可能会导入 SharedContext
)。
那么,我是不是运气不好,这只是在使用模块时不起作用?
使用 C++20 模块,有几种方法可以打破循环依赖关系,或减轻它们带来的问题。
C++ 模块不等同于标头。与标头相反,模块名称及其导出的内容不是您向程序其他部分公开的 API 的一部分。一般来说,模块比标头大得多,导入更大但更少的模块比导入许多小模块更快。
第一个解决方案是将所有紧密耦合的类型放在同一模块中。想一想,有没有办法可以只用
SharedContext
而不用States
呢?是否存在导入其中一个而不导入另一个的用例?如果答案是否定的,那么您的程序的该部分应该只导出一个模块。
它给我们带来了哪些优势?您现在可以使用分区,它们共享导出符号的所有权。因此,这允许前向声明。
export module SharedState:SharedContext;
struct Window;
struct EventManager;
struct TextureManager;
struct GUI_Manager;
struct SharedContext{
Window* m_window = nullptr;
EventManager* m_eventManager = nullptr;
TextureManager* m_textureManager = nullptr;
GUI_Manager* m_gui_manager = nullptr;
};
然后在其他分区:
export module SharedState:Window;
export struct Window {
// ...
};
export module SharedState:TextureManager;
export struct TextureManager {
// ...
};
最后,在主界面单元中:
export module SharedState;
export import :SharedContext;
export import :Window;
export import :TextureManager;
// ...
更好的解决方案可能只是以不需要生命周期的所有类型的包的方式设计代码,同时还使这些类型需要该包(包是 SharedState)。
如果在共享状态中声明的类型均不使用共享状态,则不存在循环依赖关系。
你会怎么做?只需使用依赖注入,或将特定状态作为参数传递给您的函数。这样,您就不需要像
SharedState
这样的类型,因为只有您需要的类型才会通过参数传递。这是一个优秀的解决方案,使代码更易于阅读。
只需传递你需要的参数即可!!
我不推荐这样做,但使用全局模块确实允许前向声明。
这种方法的问题在于,
SharedContext
中使用的所有类型都必须意识到它们需要导出全局模块符号,并允许 ODR 违规。基本上,您打开了不安全的逃生舱口。
extern "C++" {
class Window;
class EventManager;
class TextureManager;
class GUI_Manager;
}
struct SharedContext {
Window* m_window = nullptr;
EventManager* m_eventManager = nullptr;
TextureManager* m_textureManager = nullptr;
GUI_Manager* m_gui_manager = nullptr;
};
export module Window;
extern "C++" {
export class Window { /* ... */ };
}
并使用
extern "C++"
逃生舱口声明全局模块中的所有内容。
这样做您将失去使用模块的大部分好处。您不再具有适当的组件化,并且您的代码不再是 ODR 安全的。您可能仍然会获得稍微更好的编译时间,但您失去了模块带来的大部分好处。