大家好啊,我是码财同行。今天咱们不聊数据库,换个口味,聊聊 C++ 里的"体力活"------模块初始化。
不知道大家在做中大型项目(尤其是像咱们这种服务器后台)的时候,有没有遇到过这种尴尬:
背景
项目刚起步时,大家干劲十足,加个功能就写个 StaticModuleInit()。主函数 main 里的代码长这样:
cpp
// 刚开始挺清晰的
auto bSuccss = CRoleBaseMgr::ModuleStaticInit();
if (!bSuccss) return bSuccss;
bSuccss = CTaskMgr::ModuleStaticInit();
if (!bSuccss) return bSuccss;
bSuccss = CRole::ModuleStaticInit();
// ... 更多模块注册
看着没啥问题对吧?逻辑清晰,按顺序走。
噩梦的开始
但随着业务膨胀,模块从 3 个变成 30 个,甚至 100 个的时候,问题就接踵而至了:
- "清单"越来越长 :
main函数成了"模块点名大杂烩"。每次新来个同事加个模块,都得去改一遍公共的main文件,冲突不断。 - 隐性 Bug 满地走 :有人删了模块,忘了删
main里的调用,编译报错;有人加了模块,忘了在main里写点名,结果程序跑起来莫名其妙崩了,查半天才发现是模块没初始化。 - 顺序依赖想死人 :比如
CRole必须在CRoleBaseMgr之后初始化。如果哪天谁不小心把顺序调反了,整个服务就原地爆炸。
这种"强耦合"的显式调用,简直就是代码里的定时炸弹。我就在想:能不能像 Go 语言的 init() 那样,让模块自己告诉程序"我要初始化",而不用主函数去一个个点名?
改进版本:注册式初始化
于是,我折腾了一套"命名空间 + 自动注册 + Lambda"的小框架。核心思路很简单:反向控制。
1. 搞个"大管家"
首先,得有个地方存这些初始化函数。
cpp
namespace module_init {
using ModuleInitFunc = std::function<bool()>;
class ModuleInitManager {
public:
// 各模块找我报到
void Register(const std::string& name, ModuleInitFunc func) {
std::lock_guard<std::mutex> lock(mutex_);
init_funcs_.emplace_back(name, std::move(func));
}
// 最后一键执行
bool ExecuteAll() {
for (const auto& [name, func] : init_funcs_) {
if (!func()) {
// 这里可以接项目的日志系统:xxx 模块初始化失败
return false;
}
}
return true;
}
private:
std::vector<std::pair<std::string, ModuleInitFunc>> init_funcs_;
std::mutex mutex_;
};
}
2. 搞个"自动报到机"(辅助类)
利用 C++ 全局对象在 main 执行前构造的特性,咱们搞个辅助类。
cpp
namespace module_init {
class Registrar {
public:
Registrar(const std::string& name, ModuleInitFunc func) {
ModuleInitManager::GetInstance().Register(name, std::move(func));
}
};
}
业务怎么接入?
有了这套东西,业务侧写起来就非常爽了,支持各种姿势:
场景 A:最简单的初始化 直接在模块的 .cpp 里写一行:
cpp
static module_init::Registrar g_reg("MyModule", []() {
// 逻辑写在这,一行搞定
return true;
});
场景 B:要捕获点上下文? Lambda 的强大就体现出来了:
cpp
static int g_config = 2024;
static module_init::Registrar g_reg("ConfigModule", [&]() {
std::cout << "初始化配置:" << g_config << std::endl;
return true;
});
场景 C:老代码不想大改? 套个壳就行:
cpp
static module_init::Registrar g_reg("OldModule", []() {
return COldModule::OldStaticInit();
});
最终效果
现在,咱们回头看看 main 函数:
cpp
int main() {
// 整个世界清静了!
if (!module_init::ModuleInitManager::GetInstance().ExecuteAll()) {
return -1;
}
// 愉快的业务逻辑...
return 0;
}
不管以后是加 100 个模块还是删 50 个模块,main 函数一行代码都不用动! 每个模块的生死存亡,都在它自己的 .cpp 里决定,这才是真正的解耦。
避坑指南
写完之后,我发现还有几个细节得提醒大家:
- 线程安全 :虽然注册通常在
main之前(单线程),但考虑到有些动态库加载(插件化)的情况,我在管理器里加了std::mutex,稳。 - 顺序问题 :目前的方案是按编译链接的顺序执行。如果你的模块间有严格的先后顺序,可以考虑在
Register时加个优先级参数。
好了,今天的分享就到这里。这种"注册式"方案在咱们大型工程里非常实用,既优雅又省心。
大家回去可以试试,要是遇到什么坑,欢迎在评论区跟我一起讨论!
我是码财同行,咱们下期见!