C++ 实现支持功能自由组合的访问者模式时的基类歧义问题
报错根因定位
你的判断方向是对的,但核心原因不是两份accept()实现冲突,是非虚继承造成的多份Component基类子对象问题:
- 当前继承链路是
Component -> ComponentUpdate、Component -> ComponentDraw,两条链路没有做虚继承,各自持有一份独立的Component基类子对象 - 当
ObjectBase同时继承ComponentUpdate和ComponentDraw时,内部会存在两份完全割裂的Component实例 - 执行
components.push_back(new ObjectBase)时,编译器需要把ObjectBase*隐式转换为Component*,但因为有两条可选的继承路径,没法确定要返回哪份Component子对象的地址,就抛出了base class is ambiguous - 别被栈/堆的表面现象误导,这个报错和内存分配区域完全无关:栈上创建对象的操作没有触发向
Component*的隐式转换,所以编译器不会检查继承路径的歧义;而堆上new完要往vector<Component*>里塞,必须做这个类型转换,歧义才会显现出来。
另外哪怕解决了基类歧义,现在的实现还有隐藏问题:两个中间组件类各自实现了一套accept(),ObjectBase同时继承两个类时,同名的accept()方法会直接形成调用歧义,访问者模式依赖的双重分派逻辑完全失效——你没法让ObjectBase在被访问时同时触发更新、绘制两种逻辑。
适配灵活能力搭配的调整方案
第一步:修复菱形继承的基类歧义
给所有继承自公共基类Component的中间组件类加上虚继承,让整条继承链路只保留一份Component子对象:
class ComponentUpdate : public virtual Component { // 原有成员保留 }; class ComponentDraw : public virtual Component { // 原有成员保留 };
改完这步,之前往vector里塞对象的歧义会直接消失。
第二步:解耦能力和固定accept实现
现在的设计里,每个能力类都绑定了固定的accept()逻辑,导致多能力叠加的时候冲突。正确的做法是把能力的抽象定义和accept()的实现拆分开,让每个具体节点类自己按需组合访问逻辑:
- 删掉两个中间组件类里写死的
accept()实现,只保留能力对应的纯虚接口:class ComponentUpdate : public virtual Component { public: virtual void update() = 0; // 移除原有的accept实现 }; class ComponentDraw : public virtual Component { public: virtual void draw() = 0; // 移除原有的accept实现 }; - 单能力需求的类(比如
Camera只需要更新能力),自己实现专属的accept(),只触发对应的访问逻辑:class Camera : public ComponentUpdate { public: void update() override { std::cout << "Camera update" << std::endl; } void accept(Visitor* visitor) override { visitor->VisitUpdate(this); } }; - 多能力叠加的类(比如
ObjectBase需要同时支持更新、绘制),自己重写accept(),按实际需要的顺序触发所有关联的访问逻辑:class ObjectBase : public ComponentDraw, public ComponentUpdate { public: void update() override { std::cout << "ObjectBase update" << std::endl; } void draw() override { std::cout << "ObjectBase draw" << std::endl; } void accept(Visitor* visitor) override { // 按需组合所有需要的访问调用 visitor->VisitUpdate(this); visitor->VisitDraw(this); } };
这种设计下你完全可以按需给节点搭配能力:需要加个碰撞检测能力,就写个class ComponentCollision : public virtual Component { public: virtual void collide() = 0; };,哪个节点需要碰撞能力就继承这个类,然后在自己的accept()里加一行对应的VisitCollision调用就行,不需要改原有类的继承结构。
额外踩坑提醒:你定义的抽象
Visitor类带了VisitOverlay纯虚方法,但SceneNodeVisitor没有实现这个方法,会因为自身是抽象类根本没法实例化。要么给抽象Visitor里不需要的方法加空的默认实现,要么按功能拆分独立的访问者接口,不要把所有可能的能力都堆在同一个抽象访问者类里。
内容的提问来源于stack exchange,提问作者str0yd

