聊聊 C++ 模块“注册式”的优雅姿势

大家好啊,我是码财同行。今天咱们不聊数据库,换个口味,聊聊 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 个的时候,问题就接踵而至了:

  1. "清单"越来越长 :main 函数成了"模块点名大杂烩"。每次新来个同事加个模块,都得去改一遍公共的 main 文件,冲突不断。
  2. 隐性 Bug 满地走 :有人删了模块,忘了删 main 里的调用,编译报错;有人加了模块,忘了在 main 里写点名,结果程序跑起来莫名其妙崩了,查半天才发现是模块没初始化。
  3. 顺序依赖想死人 :比如 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 时加个优先级参数。

好了,今天的分享就到这里。这种"注册式"方案在咱们大型工程里非常实用,既优雅又省心。

大家回去可以试试,要是遇到什么坑,欢迎在评论区跟我一起讨论!

我是码财同行,咱们下期见!

相关推荐
小码哥_常13 小时前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
学长毕设16 小时前
基于SpringBoot的实验室资产管理系统
java·spring boot·后端
努力的小雨18 小时前
给 AI 眼镜开发应用有多难?我从零做了一个陪练,还上架了
后端
excel20 小时前
双 Token 与前端使用 Crypto 公私钥鉴权换 Token,哪种方案更好?
前端·后端
AI入职记20 小时前
AI 代码审查召回率实测:20 个骗过单元测试的变异体,抓到 13 个
后端
浮链序20 小时前
别让 AI 客服把用户当猴耍:百来行 Python 把「转人工」做成一条高可用链路
后端
属于自己的天空20 小时前
Claude Code 插件完全指南:官方市场怎么用,怎么自己写(v2.1.269+)
后端
Tokenfree20 小时前
给大模型调用加一个熔断器:60 行代码,三个阈值
后端
Jason_chen20 小时前
Linux 进程调度器深入解析与实时性优化指南(基于 Linux 5.10+)
后端
1360967572320 小时前
三个 Agent 干同一个活,职责边界到底该怎么划
后端