聊聊 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 时加个优先级参数。

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

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

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

相关推荐
余槐i6 分钟前
无慢查询 RT 却飙升:cProfile 定位 FastAPI 事件循环阻塞
后端·python·性能优化·fastapi·asyncio
AINative软件工程14 分钟前
LLM 应用的 Adaptive Batching 工程实践:动态合批把吞吐提升 3 倍,但延迟的坑你踩过吗
后端·llm·ai编程
parser25 分钟前
Python 装饰器:从语法糖、闭包到 @wraps(上篇)
后端
高频因子挖掘机25 分钟前
股票池一大就请求缓慢?量化系统批量获取行情的设计与优化
后端·github·api
郑州光合科技余经理26 分钟前
本地生活平台搭建:跨业态用户标识怎么贯通
java·开发语言·前端·后端·uni-app·php·ai编程
Thneonl33 分钟前
故意弄坏生产:混沌工程不是乱砸,是实验设计
后端·程序员
Thneonl42 分钟前
别上来就 strace:60 秒十条命令看清一台病机
后端·程序员
马剑威(威哥爱编程)1 小时前
【AI全栈后端12-11】Spring Boot 把 AI 接口真正上线扛量:限流 / 降级 / 可观测
人工智能·spring boot·后端
tachibana21 小时前
Golang 中数组和切片的区别?
开发语言·后端·golang·数组·切片
我是人✓1 小时前
Spring IOC注解全解析和Spring整合测试单元
java·后端·spring