C++ 依赖注入(DI)
1、 什么是依赖注入(DI)
1.1 直观定义
依赖注入 = 不在类内部创建依赖对象,而是由外部提供(注入)依赖。
反例(强耦合)
cpp
class SlamSystem {
public:
SlamSystem() {
imu_ = new ImuDriver(); // 内部创建
lidar_ = new LidarDriver();
}
private:
ImuDriver* imu_;
LidarDriver* lidar_;
};
依赖注入(低耦合)
cpp
class SlamSystem {
public:
SlamSystem(ImuDriver* imu, LidarDriver* lidar)
: imu_(imu), lidar_(lidar) {}
private:
ImuDriver* imu_;
LidarDriver* lidar_;
};
1.2 为什么需要 DI
| 问题 | 不使用 DI | 使用 DI |
|---|---|---|
| 耦合度 | 极高 | 低 |
| 单元测试 | 困难 | 容易 |
| 模块替换 | 需改源码 | 只换注入对象 |
| 架构扩展 | 痛苦 | 自然 |
小结:
DI 是"控制反转(IoC)"的核心实现手段
2、 依赖注入 vs 依赖查找
2.1 依赖查找(Service Locator)
cpp
auto imu = ServiceLocator::get<ImuDriver>();
缺点:
- 隐式依赖
- 难测试
- 全局状态
2.2 依赖注入
cpp
SlamSystem(ImuDriver& imu);
DI 更显式、更安全、更可维护
3、 C++ 中常见的 4 种依赖注入方式
3.1 构造函数注入(最推荐 ⭐⭐⭐⭐⭐)
适用场景
- 依赖 必须存在
- 对象 不可变依赖
cpp
class Backend {
public:
Backend(Optimizer& opt, Map& map)
: optimizer_(opt), map_(map) {}
private:
Optimizer& optimizer_;
Map& map_;
};
优点
- 强制依赖完整
- 线程安全
- 易测试
缺点
- 构造函数参数可能较多
工程首选
3.2 Setter 注入(可选依赖)
cpp
class VisualFrontend {
public:
void setCamera(Camera* cam) {
camera_ = cam;
}
private:
Camera* camera_ = nullptr;
};
优点
- 灵活
- 依赖可后置
缺点
- 生命周期和空指针风险
适合 可选模块 / 插件
3.3 接口 + 多态注入(经典 OOP)
cpp
class Imu {
public:
virtual ~Imu() = default;
virtual ImuData read() = 0;
};
class RealImu : public Imu {};
class SimImu : public Imu {};
class Estimator {
public:
Estimator(Imu& imu) : imu_(imu) {}
private:
Imu& imu_;
};
优点
- 完美支持 mock / fake
- 解耦实现
SLAM / 机器人中非常常见
3.4 模板注入(编译期 DI,零开销)
cpp
template<typename ImuT>
class Estimator {
public:
Estimator(ImuT& imu) : imu_(imu) {}
private:
ImuT& imu_;
};
优点
- 零虚函数开销
- 编译期绑定
缺点
- 编译时间增加
- 接口不稳定
高性能模块(前端、滤波器)推荐
4、 智能指针在 DI 中的使用
4.1 引用 vs 指针 vs 智能指针
| 类型 | 场景 |
|---|---|
T& |
必须存在,生命周期外部管理 |
T* |
可为空 |
std::unique_ptr<T> |
所有权转移 |
std::shared_ptr<T> |
共享依赖 |
推荐原则
cpp
class System {
public:
System(std::shared_ptr<Logger> logger)
: logger_(std::move(logger)) {}
private:
std::shared_ptr<Logger> logger_;
};
不要滥用 shared_ptr
5、 手写一个简单 DI Container
cpp
class Container {
public:
template<typename T>
void registerInstance(std::shared_ptr<T> obj) {
instances_[typeid(T).hash_code()] = obj;
}
template<typename T>
std::shared_ptr<T> resolve() {
return std::static_pointer_cast<T>(
instances_[typeid(T).hash_code()]);
}
private:
std::unordered_map<size_t, std::shared_ptr<void>> instances_;
};
使用:
cpp
Container c;
c.registerInstance<Imu>(std::make_shared<RealImu>());
auto imu = c.resolve<Imu>();
工程中 不建议自己造复杂轮子
6、 现成 C++ DI 框架
| 框架 | 特点 |
|---|---|
| Boost.DI | 纯头文件,现代 C++ |
| Google Fruit | 谷歌内部风格 |
| Poco DI | 工程化 |
| Qt DI | Qt 项目 |
Boost.DI 示例
cpp
auto injector = di::make_injector(
di::bind<Imu>.to<RealImu>()
);
auto estimator = injector.create<Estimator>();
7、 在 SLAM / 机器人系统中的典型 DI 架构
text
Config
↓
Sensor Drivers
↓
Frontend
↓
Backend
↓
Map
cpp
SlamSystem(
Imu& imu,
Lidar& lidar,
Camera& cam,
Backend& backend
);
模块可替换
可单独测试
支持仿真 / 实物
8、 常见反模式(一定要避开)
- 在类内部
new依赖 - 全局单例
- Service Locator
- shared_ptr 滥用
- 构造函数里干重活
9、 总结
依赖注入不是"多写代码",而是"少还技术债"。
核心原则
- 依赖外置
- 接口优先
- 构造函数注入优先
- 能编译期就编译期
- 生命周期清晰
建议要点
前端/滤波器 → 模板 DI
系统/模块 → 构造函数 + 接口 DI
测试 → Mock + 注入
依赖注入(DI) vs 单例设计模式(Singleton)
1、核心结论
Singleton 解决的是"唯一性"问题
DI 解决的是"依赖管理与解耦"问题
它们不是同一层面的工具。
2、概念对比
| 维度 | Singleton | DI(依赖注入) |
|---|---|---|
| 关注点 | 对象唯一性 | 依赖解耦 |
| 控制权 | 类内部 | 外部 |
| 依赖可见性 | ❌ 隐式 | ✅ 显式 |
| 可测试性 | 差 | 好 |
| 并发友好 | 一般 | 好 |
| 架构规模 | 小~中 | 中~大 |
| 面试评价 | 会写不加分 | 会用很加分 |
3、典型 Singleton 示例
3.1 传统写法(隐式依赖)
cpp
class Logger {
public:
static Logger& instance() {
static Logger inst;
return inst;
}
void log(const std::string& msg) {
std::cout << msg << std::endl;
}
};
cpp
void process() {
Logger::instance().log("processing...");
}
存在问题
process()无法感知依赖- 无法 mock
- 强耦合
4、DI 版本(推荐)
4.1 抽象接口
cpp
struct ILogger {
virtual ~ILogger() = default;
virtual void log(const std::string&) = 0;
};
4.2 具体实现(可仍是 Singleton)
cpp
class ConsoleLogger : public ILogger {
public:
void log(const std::string& msg) override {
std::cout << msg << std::endl;
}
};
4.3 依赖注入(构造注入)
cpp
class Processor {
public:
explicit Processor(ILogger& logger)
: logger_(logger) {}
void run() {
logger_.log("processing...");
}
private:
ILogger& logger_;
};
4.4 使用方式
cpp
int main() {
ConsoleLogger logger;
Processor p(logger);
p.run();
}
优点
- 依赖显式
- 可测试
- 可替换
5、Singleton + DI(工程中最常见)
Singleton 不消失,但只存在于"组装层"
5.1 组合方式(最佳实践)
cpp
class LoggerSingleton : public ILogger {
public:
static LoggerSingleton& instance() {
static LoggerSingleton inst;
return inst;
}
void log(const std::string& msg) override {
std::cout << msg << std::endl;
}
};
cpp
Processor p(LoggerSingleton::instance());
关键点
- 业务代码不知道这是 Singleton
- 只依赖接口
6、替代方案 1:Context / Service Locator(折中)
6.1 Context 容器
cpp
struct AppContext {
ILogger* logger;
};
cpp
void process(const AppContext& ctx) {
ctx.logger->log("processing...");
}
特点
| 优点 | 缺点 |
|---|---|
| 集中管理 | 仍是全局感 |
| 明确依赖 | 易膨胀 |
7、替代方案 2:Factory(插件 / 扩展系统)
7.1 Factory + Registry
cpp
class LoggerFactory {
public:
static std::unique_ptr<ILogger> create() {
return std::make_unique<ConsoleLogger>();
}
};
cpp
auto logger = LoggerFactory::create();
Processor p(*logger);
常用于:
- 插件系统
- 动态模块
- 多策略切换
8、替代方案 3:模板注入(算法/高性能)
8.1 编译期 DI(零开销)
cpp
template <typename Logger>
class Processor {
public:
explicit Processor(Logger& logger) : logger_(logger) {}
void run() {
logger_.log("processing...");
}
private:
Logger& logger_;
};
特点
- 零虚函数
- 编译期绑定
- 常见于 SLAM / 数值优化
9、真实项目选型建议(非常重要)
用 Singleton 当:
- Logger
- 配置只读
- GPU / CUDA Context
- 线程池(受控)
用 DI 当:
- 算法模块
- 策略选择
- IO / 存储
- 可测试代码
不要:
- 在算法核心用 Singleton
- 在库代码暴露 Singleton
- 用 Singleton 传状态
10、二者关系
"Singleton 可以是 DI 的一种实现,
但 DI 不是 Singleton。"
11、对比总结
| 目标 | 方案 |
|---|---|
| 唯一资源 | Singleton |
| 解耦依赖 | DI |
| 可测试 | DI |
| 高性能 | 模板 DI |
| 插件系统 | Factory |
| 大型系统 | DI + 少量 Singleton |