C++ 如何根据两个 Shape 的实际类型分派相交函数?
数据阿明
结论
可以使用双重分派(double dispatch),让两个对象各通过一次虚函数参与类型选择,从而避免手动逐个尝试 dynamic_cast。
原例中,s1 和 s2 的静态类型都是 Shape*。调用 s1->intersects(*s2) 时,虚函数会根据 s1 的实际类型选择实现,但参数 *s2 的静态类型仍是 Shape&,编译器无法据此选择接受 Circle 或 Line 的重载。全局函数调用也一样:intersect(*s1, *s2) 的两个实参在编译时都被视为 Shape&。
用双重分派选择两个实际类型
先声明四个具体类型对应的全局重载,再在 Shape 接口中声明用于分派第二个对象类型的虚函数:
class Circle;
class Line;
bool intersect(const Circle& first, const Circle& second);
bool intersect(const Circle& first, const Line& second);
bool intersect(const Line& first, const Circle& second);
bool intersect(const Line& first, const Line& second);
class Shape {
public:
virtual ~Shape() = default;
virtual bool intersects(const Shape& other) const = 0;
// 这些虚函数用于分派第一个对象的具体类型。
virtual bool intersectsWith(const Circle& first) const = 0;
virtual bool intersectsWith(const Line& first) const = 0;
};
class Circle : public Shape {
public:
bool intersects(const Shape& other) const override {
return other.intersectsWith(*this);
}
bool intersectsWith(const Circle& first) const override {
return intersect(first, *this);
}
bool intersectsWith(const Line& first) const override {
return intersect(first, *this);
}
};
class Line : public Shape {
public:
bool intersects(const Shape& other) const override {
return other.intersectsWith(*this);
}
bool intersectsWith(const Circle& first) const override {
return intersect(first, *this);
}
bool intersectsWith(const Line& first) const override {
return intersect(first, *this);
}
};
bool intersect(const Shape* first, const Shape* second) {
// 调用者须保证两个指针都非空;也可以在这里定义空指针的处理方式。
return first->intersects(*second);
}
四个全局重载分别负责对应的几何计算:
bool intersect(const Circle& first, const Circle& second) {
// Circle 与 Circle 的相交计算
}
bool intersect(const Circle& first, const Line& second) {
// Circle 与 Line 的相交计算
}
bool intersect(const Line& first, const Circle& second) {
// Line 与 Circle 的相交计算
}
bool intersect(const Line& first, const Line& second) {
// Line 与 Line 的相交计算
}
函数体需要填入实际的几何逻辑。如果这里只展示分派结构,可以先声明这些重载,再在其他位置定义。
分派过程
调用 intersect(first, second) 后,分派分两步进行:
first->intersects(*second)是虚调用,会根据first的实际类型进入Circle::intersects或Line::intersects。- 该实现接着调用
second.intersectsWith(*this)。这也是虚调用,会根据second的实际类型选择实现。此时,*this在对应类中的静态类型已知,因此会匹配正确的重载。最终调用的全局函数就对应这两个对象的具体类型。
例如,若 first 指向 Circle,second 指向 Line,最终调用的是 intersect(const Circle&, const Line&)。
需要留意
Shape 需要声明虚析构函数,才能通过基类指针安全地销毁派生类对象。示例中的指针也必须保证非空,或者由接口明确处理空指针。
新增形状时,需要扩展 Shape 的分派接口,并为现有形状补上相应实现。如果类型集合固定,也可以考虑使用 std::variant 配合 std::visit。对于通过基类接口管理的开放式继承层次,双重分派是常见做法。
备注:内容仅供参考。
