C++依赖注入应采用接口抽象+构造函数传参+工厂或组合根,避免滥用std::shared_ptr导致析构不可控、循环引用及测试困难;优先用const接口指针或引用传递,长生命周期对象才考虑unique_ptr;std::function或策略模板可替代运行时DI容器,更轻量安全。

直接说结论:C++ 里做依赖注入(DI)不靠框架,靠接口抽象 + 构造函数传参 + 工厂或组合根(Composition Root)——标准库够用,强行套用 Java 风格的“容器”反而增加复杂度和生命周期风险。
为什么不用 std::shared_ptr 自动管理所有依赖?
常见错误是把所有服务都塞进 std::shared_ptr,以为能自动解耦。但问题立刻浮现:
-
shared_ptr延迟析构,导致资源释放时机不可控(比如日志服务在 main 结束后才析构,写日志失败) - 循环引用真实存在:A 持有 B 的
shared_ptr,B 又持有 A 的shared_ptr→ 内存泄漏 - 测试时无法替换实现:mock 对象必须也用
shared_ptr,但 mock 生命周期常需手动控制
更稳妥的做法是:接口指针用 const IService* 或 IService& 传入,所有权明确归调用方;只有跨模块、长生命周期对象才考虑 unique_ptr 管理。
std::function 和模板策略替代运行时 DI 容器
很多场景根本不需要运行时注册/解析——比如算法策略、回调钩子、配置驱动行为。这时 std::function 或策略模板更轻、更类型安全:
立即学习“C++免费学习笔记(深入)”;
class Processor {
public:
using Policy = std::function;
Processor(Policy policy) : policy_(std::move(policy)) {}
void handle(const Data& d) { policy_(d); }
private:
Policy policy_;
};
// 使用时直接传 lambda,无反射、无字符串查找、无 RTTI 开销
Processor p{[](const Data& d) { std::cout << d.id << "\n"; }};
对比“容器注册 "data_handler" 字符串再 resolve”,这种写法编译期绑定、零成本抽象、调试友好。
模块边界如何定义接口与实现分离?
真正的解耦不在“能不能换实现”,而在“换实现是否需要重编译其他模块”。关键点:
- 接口头文件(
IRepository.h)只含纯虚函数、noexcept、值语义参数,不 include 实现细节头文件 - 实现类(
SqlRepository)定义在 cpp 文件里,头文件不暴露sqlite3_*类型 - 工厂函数放在模块边界(如
create_repository()),返回std::unique_ptr,调用方不感知 new 了什么 - 避免在接口中使用
std::vector等模板类型 —— ABI 兼容性风险高;改用std::span或自定义 view 类
这样,更换数据库实现时,只需重新编译 SqlRepository.cpp 和工厂,其余模块完全不 rebuild。
构造函数注入不是万能的 —— 生命周期冲突怎么办?
当两个服务互相依赖,或某服务需在特定阶段初始化(如网络服务必须等配置加载完),硬塞构造函数会逼你写 init() 方法,破坏 RAII:
- 优先拆分职责:把“配置加载”抽成独立
ConfigLoader,让网络服务在构造时接收已解析的Endpoint值,而非整个 loader - 若真需延迟创建,用
std::optional+ 显式emplace(),比裸指针 +new更安全 - 全局服务(如日志器)用局部静态变量 + 函数内联初始化,避免静态初始化顺序问题:
Logger& get_logger() { static Logger instance; return instance; }
最易被忽略的是:DI 不解决线程安全。接口本身不带线程契约,IRepository::save() 是线程安全还是调用方保证?必须在接口注释里写死,不能靠“容器帮我们管”。










