于 C++ 的虚函数多态 和 模板方法模式 的结合
要理解 LaneChangePath
如何 “调用父类 Execute
却返回自己的 Process
”,核心在于 C++ 的虚函数多态 和 模板方法模式 的结合 —— 父类 PathGeneration
定义了固定的执行流程(Execute
),子类 LaneChangePath
只需重写流程中的 “可变步骤”(Process
),即可在复用父类流程的同时,定制自己的核心逻辑。以下从 “类继承关系”“语法细节”“调用流程” 三方面拆解:
一、先理清类的继承关系
首先明确代码中 3 个类的层级(核心是 “父类定义框架,子类定制细节”):
类名 | 继承关系 | 核心作用 |
---|---|---|
Task | 最顶层基类 | 定义规划任务的抽象接口(如 Execute 方法),是所有规划任务的统一基类。 |
PathGeneration | 继承 Task | 作为 “路径生成类” 的中间基类,封装固定的执行流程(Execute 方法),并将 “可变的核心逻辑” 交给虚函数 Process 。 |
LaneChangePath | 继承 PathGeneration | 具体的 “换道路径生成类”,不重写 Execute (复用父类流程),只重写 Process (实现换道路径的核心生成逻辑)。 |
二、关键语法:虚函数(virtual
)与方法重写(override
)
这是实现 “父类 Execute
调用子类 Process
” 的核心语法,先看父类和子类的关键代码设计:
1. 父类 PathGeneration
的 “模板方法” 设计
PathGeneration
作为中间基类,用 模板方法模式 定义了 “固定流程 + 可变步骤”:
- 固定流程:
Execute
方法(非虚函数,不可被子类重写),负责流程的 “框架”(如前置检查、日志记录、调用Process
); - 可变步骤:
Process
方法(声明为virtual
虚函数,允许子类重写),负责流程的 “核心逻辑”(如换道路径生成、直线路径生成)。
父类核心代码回顾:
cpp
运行
class PathGeneration : public Task {public:// 1. 重写父类 Task 的 Execute 方法(固定流程,非虚函数)apollo::common::Status Execute(Frame* frame, ReferenceLineInfo* reference_line_info) override {Task::Execute(frame, reference_line_info); // 调用顶层父类 Task 的 Execute(做基础初始化)return Process(frame, reference_line_info); // 调用虚函数 Process(核心逻辑交给子类)}protected:// 2. 声明虚函数 Process(可变步骤,子类必须重写自己的逻辑)virtual apollo::common::Status Process(Frame* frame, ReferenceLineInfo* reference_line_info) {return apollo::common::Status::OK(); // 父类默认实现(空逻辑,子类需覆盖)}
};
- 关键语法点 1:
Process
用virtual
修饰 → 声明为虚函数,允许子类重写,且调用时会根据 “对象的实际类型” 动态绑定到对应的实现(多态); - 关键语法点 2:
Execute
没有virtual
→ 是非虚函数,子类无法重写,强制复用父类的流程框架(保证所有子类的执行流程一致)。
2. 子类 LaneChangePath
的 “重写” 设计
LaneChangePath
作为具体的 “换道路径生成类”,不重写 Execute
(直接用父类的流程),只重写 Process
(实现换道路径的核心逻辑),代码如下:
cpp
运行
class LaneChangePath : public PathGeneration {private:// 重写父类 PathGeneration 的虚函数 Process(用 override 关键字确保重写正确)apollo::common::Status Process(Frame* frame, ReferenceLineInfo* reference_line_info) override {// 这里是 LaneChangePath 自己的核心逻辑:// 1. 计算换道的起始点和目标点(基于当前车道和目标车道);// 2. 生成换道的平滑路径(如基于多项式插值,满足车辆最小转弯半径);// 3. 校验路径是否避障(与障碍物的距离是否满足安全阈值);// 4. 将生成的路径存入 reference_line_info;return apollo::common::Status::OK(); // 返回自己的执行结果}
};
- 关键语法点:
override
关键字 → 显式声明 “该方法是重写父类的虚函数”,编译器会检查父类是否存在对应的虚函数,避免因函数签名不一致导致 “重写失败”(如参数类型错误、返回值不匹配)。
三、调用流程:为什么父类 Execute
会返回子类 Process
的结果?
当 LaneChangePath
的对象调用 Execute
时,整个流程会通过 “多态” 自动绑定到子类的 Process
,具体步骤如下:
步骤 1:调用 LaneChangePath::Execute
由于 LaneChangePath
没有重写 Execute
,会直接调用父类 PathGeneration
的 Execute
方法:
cpp
运行
// 假设创建了 LaneChangePath 的对象
std::unique_ptr<PathGeneration> path_task = std::make_unique<LaneChangePath>();
// 调用 Execute,实际执行的是父类 PathGeneration 的 Execute
path_task->Execute(frame, reference_line_info);
步骤 2:父类 Execute
内部调用 Process
进入父类 PathGeneration::Execute
后,代码会执行:
cpp
运行
return Process(frame, reference_line_info); // 关键:调用虚函数 Process
此时,由于 Process
是虚函数,且 path_task
的实际类型是 LaneChangePath
(虽然指针类型是父类 PathGeneration
),C++ 会通过 “动态绑定” 找到 LaneChangePath
重写的 Process
方法,而非父类的默认实现。
步骤 3:返回子类 Process
的结果
LaneChangePath::Process
执行完换道路径生成逻辑后,返回自己的 Status
(成功 / 失败),这个结果会被父类 Execute
直接返回给调用者。
最终效果:调用的是父类的 Execute
流程,但实际执行的是子类的 Process
逻辑,返回的也是子类 Process
的结果。
四、这种设计的好处(为什么要这么写?)
Apollo 用这种 “父类框架 + 子类细节” 的设计,核心是为了复用流程、统一接口、灵活扩展:
- 复用固定流程:所有路径生成类(如
LaneChangePath
、StraightPath
、AvoidObstaclePath
)的Execute
流程一致(如前置初始化、日志记录),无需每个子类重复写一遍; - 统一接口:所有路径生成类都通过
PathGeneration
基类指针调用,调用者无需关心具体是哪个子类(如换道路径还是直线路径),符合 “开闭原则”; - 灵活扩展:新增路径生成逻辑时(如 “紧急避障路径”),只需新建子类继承
PathGeneration
,重写Process
即可,无需修改父类代码。
总结:核心语法与设计模式
核心语法 / 模式 | 作用 |
---|---|
虚函数(virtual ) | 允许子类重写父类方法,实现 “动态绑定”(调用时根据对象实际类型找对应实现); |
方法重写(override ) | 显式声明重写,避免语法错误,增强代码可读性; |
模板方法模式 | 父类定义固定流程(Execute ),子类定制核心逻辑(Process ),实现 “流程复用 + 细节灵活”; |
简单说:父类 PathGeneration
搭好了 “架子”(Execute
),子类 LaneChangePath
只需要填自己的 “核心内容”(Process
),就能通过父类的架子完成整个流程,同时返回自己的结果。