C++17+纯模板自动依赖注入可行但非开箱即用,需编译期类型图构建与运行时生命周期管理结合,必须处理循环依赖、对象销毁顺序、多例/单例混用等问题,并支持实例/工厂/类型三层注册及线程安全。

纯模板实现的自动派发依赖注入,在 C++17+ 中可行,但不等于“开箱即用”——它本质是编译期类型图构建 + 运行时对象生命周期管理的混合体,直接套用 std::function 或裸指针会立刻掉进对象销毁顺序、循环依赖、多例/单例混用等坑里。
为什么不能只靠 std::make_shared 和模板递归推导?
模板可以推导构造参数类型,但无法自动判断:该类型是否已注册、是否需要延迟构造、是否应复用已有实例。比如 ServiceA 依赖 ServiceB,而 ServiceB 又依赖 ServiceA,单纯递归实例化会导致无限模板展开或栈溢出。
- 编译期无法检测循环依赖,只能靠运行时拓扑排序或弱引用破环
-
std::is_constructible_v只能验证能否构造,不能验证“当前上下文是否允许构造” - 若某依赖需从配置加载(如
DatabaseConfig),模板无法内联读取 JSON/YAML —— 必须预留运行时 hook
injector 类必须支持三种注册模式
硬编码模板推导只适用于无参或全编译期常量参数的场景。真实服务往往需要运行时参数(如端口号、连接串),因此注册接口必须分层:
-
register_instance<t>(T&& obj)</t>:接管已有对象生命周期(常见于全局 logger、event loop) -
register_factory<t>(std::function<:unique_ptr>()> factory)</:unique_ptr></t>:延迟构造,可捕获运行时变量 -
register_type<t impl>()</t>:仅声明绑定,由 injector 在首次请求时按需调用Impl默认构造函数
漏掉任意一种,都会导致某些服务无法注入(例如带 int port 参数的 HttpServer 就不能走 register_type)。
立即学习“C++免费学习笔记(深入)”;
自动派发的核心是 resolve 函数的两阶段策略
第一次调用 resolve<t>()</t> 时,不能直接构造,必须先检查是否已在缓存中;若未命中,再解析其构造函数参数类型,逐个递归 resolve —— 但这个过程必须加正在解析中(in_resolving_)标记,否则循环依赖直接崩溃。
- 使用
std::unordered_map<:type_index std::any></:type_index>缓存已构建实例(注意std::any不管理多态删除,需配合std::shared_ptr存储) - 用
std::unordered_set<:type_index></:type_index>记录当前调用栈中的类型,每次进入resolve前插入、退出时擦除 - 若在
in_resolving_中再次遇到同一类型,抛出"circular dependency detected"而非 segfault
示例关键片段:
template <typename T>
std::shared_ptr<T> resolve() {
auto idx = std::type_index(typeid(T));
if (instances_.count(idx)) {
return std::any_cast<std::shared_ptr<T>>(instances_.at(idx));
}
if (in_resolving_.count(idx)) {
throw std::runtime_error("circular dependency detected");
}
in_resolving_.insert(idx);
auto ptr = create_instance<T>(); // 递归 resolve 参数
in_resolving_.erase(idx);
instances_[idx] = ptr;
return ptr;
}
模板特化与类型擦除的边界在哪?
你无法对 std::vector<t></t> 或 std::function<void></void> 做通用注入 —— 它们不是具体服务类型,而是工具类型。所有被 resolve 的类型必须显式注册,且满足:有公开构造函数、非 final(若需多态)、析构函数非 deleted。
-
std::shared_ptr<t></t>是安全返回类型,但std::unique_ptr<t></t>会破坏复用语义(每次 resolve 都新造) - 若服务含虚基类,确保其虚析构函数为 public,否则
std::shared_ptr释放时 UB - 避免在构造函数里调用
injector::resolve—— 此时 injector 可能尚未完全初始化,缓存为空
最易忽略的是线程安全:上述 instances_ 和 in_resolving_ 若用于多线程环境,必须加 std::shared_mutex(读多写少)或拆成 per-thread injector 实例。没有默认线程安全,别假设它存在。


















