编译期依赖注入通过构造函数签名和模板推导在编译时锁定依赖关系,杜绝运行时查找、循环依赖仅编译报错;不用std::shared_ptr统一管理因延迟析构、循环引用、mock困难;推荐接口传参用const指针或引用,仅长生命周期对象用unique_ptr或显式shared_ptr;make_service<T>递归推导T构造函数依赖,要求依赖类型可模板推导、禁用裸指针/unique_ptr自动解析、工厂返回可make_shared类型;策略模板替代运行时DI,零开销、易调试、无RTTI;组合根须集中于main或单一入口,否则链接错误难定位。

编译期依赖注入在 C++ 里不是“配个容器 resolve”,而是靠构造函数签名 + 模板推导把依赖关系锁死在编译时——没有运行时查找、不依赖 std::type_index、不隐式延长生命周期,连循环依赖都在编译时报错。
为什么不用 std::shared_ptr 做统一依赖管理
很多人一上来就用 std::shared_ptr 包裹所有服务,结果很快遇到三类硬伤:
-
std::shared_ptr延迟析构:日志器、配置服务等在main()返回后才释放,写日志失败或配置未刷盘 - 循环引用真实存在:A 持有
std::shared_ptr<B>,B 又持有std::shared_ptr<A>→ 引用计数永远不为 0 - 测试时 mock 难控制:mock 对象也得套
std::shared_ptr,但单元测试常需手动控制其生命周期
更稳妥的做法是:接口传参优先用 const IService* 或 const IService&,所有权明确归上层;只有跨模块、长生命周期对象(如数据库连接池)才考虑 std::unique_ptr 或显式 std::shared_ptr。
make_service<T>() 如何自动推导依赖链
核心不是“注册类型”,而是让编译器根据 T 的构造函数参数列表,递归实例化其所有依赖。比如:
立即学习“C++免费学习笔记(深入)”;
class UserService {
public:
UserService(std::shared_ptr<ILogger>, std::shared_ptr<IDatabase>);
};
那么 make_service<UserService>(root) 就必须能推导出 ILogger 和 IDatabase 的具体实现类型。这要求:
- 所有依赖类型必须是模板参数可推导的(不能是
int port这种非类型参数,得提前绑定,如bind_constant<int>(8080)) - 不支持自动解析
std::unique_ptr<T>或裸指针——所有权语义模糊,统一用std::shared_ptr或 const 引用 - 工厂函数必须返回可被
std::make_shared接收的类型,否则推导中断
接口注入 + 模板策略比运行时 DI 更轻量安全
很多所谓“需要 DI”的场景,其实根本不需要运行时注册/解析。比如重试策略、序列化格式、HTTP 客户端行为切换:
template<typename Policy>
class HttpClient {
Policy policy_;
public:
explicit HttpClient(Policy p) : policy_(std::move(p)) {}
void send(Request r) { policy_.execute(r); }
};
使用时直接传 lambda 或策略类:HttpClient{[](auto& r) { return curl_send(r); }}。对比运行时靠字符串 "curl_client" 再 resolve,这种写法:
- 编译期绑定,零运行时开销
- 调试时 Ctrl+Click 直达策略实现,不经过虚表跳转
- 无 RTTI、无类型擦除、无字符串哈希查找
真正容易被忽略的是:编译期 DI 的“组合根”必须集中写在 main() 或一个明确的入口文件里。一旦依赖构造逻辑分散在多个 .cpp,就退化成隐式耦合——你不会在编译时报错,而是在链接时报 undefined reference to make_service<Database>,定位成本远高于一行模板错误提示。


















