我见过人们使用指向基类的指针容器来保存共享相同虚函数的对象组。是否可以使用这些基类指针来使用派生类的重载函数。很难解释我的意思,但(我认为)很容易用代码显示
class PhysicsObject // A pure virtual class
{
// Members of physics object
// ...
};
class Circle : public PhysicsObject
{
// Members of circle
// ...
};
class Box : public PhysicsObject
{
// Members of box
// ...
};
// Overloaded functions (Defined elsewhere)
void ResolveCollision(Circle& a, Box& b);
void ResolveCollision(Circle& a, Circle& b);
void ResolveCollision(Box& a, Box& b);
int main()
{
// Container to hold all our objects
std::vector<PhysicsObject*> objects;
// Create some circles and boxes and add to objects container
// ...
// Resolve any collisions between colliding objects
for (auto& objA : objects)
for (auto& objB : objects)
if (objA != objB)
ResolveCollision(*objA, *objB); // !!! Error !!! Can't resolve overloaded function
}
我的第一个想法是让这些函数也成为虚拟类成员(如下所示),但我很快意识到它有完全相同的问题。
class Circle;
class Box;
class PhysicsObject // A pure virtual class
{
virtual void ResolveCollision(Circle& a) = 0;
virtual void ResolveCollision(Box& a) = 0;
// Members of physics object
// ...
};
class Box;
class Circle : public PhysicsObject
{
void ResolveCollision(Circle& a);
void ResolveCollision(Box& a);
// Members of circle
// ...
};
class Circle;
class Box : public PhysicsObject
{
void ResolveCollision(Circle& a);
void ResolveCollision(Box& a);
// Members of box
// ...
};
从谷歌搜索问题似乎可以通过使用强制转换来解决,但我无法弄清楚如何找到正确的类型(也很难看)。我怀疑我问的是错误的问题,并且有一种更好的方法来构建我的代码,它可以避免这个问题并获得相同的结果。
答案 0 :(得分:2)
使用双重调度,它将类似于:
class Circle;
class Box;
// Overloaded functions (Defined elsewhere)
void ResolveCollision(Circle& a, Box& b);
void ResolveCollision(Circle& a, Circle& b);
void ResolveCollision(Box& a, Box& b);
class PhysicsObject // A pure virtual class
{
public:
virtual ~PhysicsObject() = default;
virtual void ResolveCollision(PhysicsObject&) = 0;
virtual void ResolveBoxCollision(Box&) = 0;
virtual void ResolveCircleCollision(Circle&) = 0;
};
class Circle : public PhysicsObject
{
public:
void ResolveCollision(PhysicsObject& other) override { return other.ResolveCircleCollision(*this); }
void ResolveBoxCollision(Box& box) override { ::ResolveCollision(*this, box);}
void ResolveCircleCollision(Circle& circle) override { ::ResolveCollision(*this, circle);}
// ...
};
class Box : public PhysicsObject
{
public:
void ResolveCollision(PhysicsObject& other) override { return other.ResolveBoxCollision(*this); }
void ResolveBoxCollision(Box& box) override { ::ResolveCollision(box, *this);}
void ResolveCircleCollision(Circle& circle) override { ::ResolveCollision(circle, *this);}
// ...
};
答案 1 :(得分:2)
我这样做的方法是建立一个Extent
类来告诉你一个物体的物理周长,也许是关于它的重心。此外,您还有
virtual const Extent& getExtent() const = 0;
在PhysicsObject
课程中。然后,每个对象类型实现一次getExtent
。
您的碰撞检测线变为
ResolveCollision(objA->getExtent(), objB->getExtent());
虽然从某种意义上说,这只不过是将复杂性推向Extent
类,但这种方法可以很好地扩展,因为你只需要为每个对象构建一个方法。
替代双重调度机制是侵入性的,因为新形状需要调整所有现有形状。例如,如果你引入一个Circle
类,那么必须重新编译Ellipse
类,这对我来说是一种代码味道。
答案 2 :(得分:2)
我将绘制一个不依赖于双重调度的实现。相反,它使用一个表,其中所有功能都已注册。然后使用动态类型的对象(作为基类传递)访问此表。
首先,我们有一些示例形状。他们的类型被列入enum class
。每个形状类都将MY_TYPE
定义为它们各自的枚举条目。此外,他们必须实现基类的纯虚type
方法:
enum class ObjectType
{
Circle,
Box,
_Count,
};
class PhysicsObject
{
public:
virtual ObjectType type() const = 0;
};
class Circle : public PhysicsObject
{
public:
static const ObjectType MY_TYPE = ObjectType::Circle;
ObjectType type() const override { return MY_TYPE; }
};
class Box : public PhysicsObject
{
public:
static const ObjectType MY_TYPE = ObjectType::Box;
ObjectType type() const override { return MY_TYPE; }
};
接下来,你有了碰撞解决功能,当然,它们必须根据形状来实现。
void ResolveCircleCircle(Circle* c1, Circle* c2)
{
std::cout << "Circle-Circle" << std::endl;
}
void ResolveCircleBox(Circle* c, Box* b)
{
std::cout << "Circle-Box" << std::endl;
}
void ResolveBoxBox(Box* b1, Box* b2)
{
std::cout << "Box-Box" << std::endl;
}
请注意,我们这里只有Circle
- Box
,没有Box
- Circle
,因为我认为它们的碰撞检测方式相同。有关Box
- Circle
碰撞案例的更多信息。
现在到核心部分,功能表:
std::function<void(PhysicsObject*,PhysicsObject*)>
ResolveFunctionTable[(int)(ObjectType::_Count)][(int)(ObjectType::_Count)];
REGISTER_RESOLVE_FUNCTION(Circle, Circle, &ResolveCircleCircle);
REGISTER_RESOLVE_FUNCTION(Circle, Box, &ResolveCircleBox);
REGISTER_RESOLVE_FUNCTION(Box, Box, &ResolveBoxBox);
表本身是std::function
的二维数组。请注意,这些函数接受指向PhysicsObject
的指针,而不是派生类。然后,我们使用一些宏来轻松注册。当然,相应的代码可以手工编写,我很清楚使用宏通常被认为是坏习惯。但是,在我看来,这些东西都是宏的优点,只要你使用有意义的名称,不会混淆你的全局命名空间,它们是可以接受的。再次注意,只有Circle
- Box
被注册,而不是相反。
现在到了花哨的宏:
#define CONCAT2(x,y) x##y
#define CONCAT(x,y) CONCAT2(x,y)
#define REGISTER_RESOLVE_FUNCTION(o1,o2,fn) \
const bool CONCAT(__reg_, __LINE__) = []() { \
int o1type = static_cast<int>(o1::MY_TYPE); \
int o2type = static_cast<int>(o2::MY_TYPE); \
assert(o1type <= o2type); \
assert(!ResolveFunctionTable[o1type][o2type]); \
ResolveFunctionTable[o1type][o2type] = \
[](PhysicsObject* p1, PhysicsObject* p2) { \
(*fn)(static_cast<o1*>(p1), static_cast<o2*>(p2)); \
}; \
return true; \
}();
宏定义了一个唯一命名的变量(使用行号),但是这个变量仅用于获取要执行的初始化lambda函数内的代码。传递的两个参数的类型(来自ObjectType
枚举)(这些是具体的类Box
和Circle
)被用来索引表。整个机制假定类型的总顺序(如枚举中所定义)并检查Circle
- Box
碰撞的函数是否确实为此顺序中的参数注册。 assert
会告诉您是否做错(意外注册Box
- Circle
)。然后在表格内为这一特定类型的对象注册lambda函数。函数本身接受类型PhysicsObject*
的两个参数,并在调用注册函数之前将它们转换为具体类型。
接下来,我们可以看看如何使用该表。现在很容易实现一个函数来检查任何两个PhysicsObject
的冲突:
void ResolveCollision(PhysicsObject* p1, PhysicsObject* p2)
{
int p1type = static_cast<int>(p1->type());
int p2type = static_cast<int>(p2->type());
if(p1type > p2type) {
std::swap(p1type, p2type);
std::swap(p1, p2);
}
assert(ResolveFunctionTable[p1type][p2type]);
ResolveFunctionTable[p1type][p2type](p1, p2);
}
它接受参数的动态类型,并将它们传递给ResolveFunctionTable
内为这些相应类型注册的函数。请注意,如果参数不符合规则,则会交换参数。因此,您可以使用ResolveCollision
和Box
随意调用Circle
,然后在内部调用为Circle
- Box
碰撞注册的函数。
最后,我将举例说明如何使用它:
int main(int argc, char* argv[])
{
Box box;
Circle circle;
ResolveCollision(&box, &box);
ResolveCollision(&box, &circle);
ResolveCollision(&circle, &box);
ResolveCollision(&circle, &circle);
}
很容易,不是吗?有关上述内容的有效实施,请参阅this。
现在,这种方法有什么优势?上面的代码基本上只需要支持任意数量的形状。假设您即将添加Triangle
。您所要做的就是:
Triangle
添加到ObjectType
枚举。ResolveTriangleXXX
功能,但必须在所有情况下执行此操作。REGISTER_RESOLVE_FUNCTION(Triangle, Triangle, &ResolveTriangleTriangle);
就是这样。无需向PhysicsObject
添加更多方法,无需在所有现有类型中实现方法。
我知道这种方法的一些'缺陷'就像使用宏,拥有所有类型的中心enum
并依赖于全局表。如果将形状类构建到多个共享库中,后一种情况可能会导致一些问题。但是,根据我的拙见,这种方法非常实用(除了非常特殊的用例),因为它不会像其他方法(例如双重调度)那样导致代码爆炸。