如果在 C++ 中只有指向基 class 的指针,则使用派生的 class 参数重载函数
Overload a function with a derived class argument if you only have a pointer to the base class in C++
我看到有人使用指向基 class 的指针容器来保存共享相同虚函数的对象组。是否可以将派生 class 的重载函数与这些基 class 指针一起使用。很难解释我的意思,但(我认为)很容易用代码显示
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 成员(如下所示),但我很快意识到它有完全相同的问题。
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
// ...
};
通过谷歌搜索问题似乎可以使用转换来解决,但我无法弄清楚如何找到要转换为的正确类型(它也很丑陋)。我怀疑我问错了问题,有更好的方法来构建我的代码,它可以回避这个问题并获得相同的结果。
使用双重调度,它会是这样的:
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);}
// ...
};
我这样做的方法是构建一个 Extent
class 来告诉您物体的物理周长,也许是关于其重心的。此外,您还可以
virtual const Extent& getExtent() const = 0;
在PhysicsObject
class。然后,您为每个对象类型实施一次 getExtent
。
你的碰撞检测线变成
ResolveCollision(objA->getExtent(), objB->getExtent());
尽管从某种意义上说,随着复杂性被推向 Extent
class,这只不过是把罐头踢到路边而已,这种方法可以很好地扩展,因为您只需要每个对象构建一个方法。
另一种双分派机制是侵入性的,因为新形状需要调整 所有 现有形状。必须重新编译 Circle
class,例如,如果您引入 Ellipse
class,这对我来说是一种代码味道。
我将草拟一个不依赖双重调度的实现。相反,它使用 table 来注册所有函数。然后使用对象的动态类型(作为基础 class 传递)访问此 table。
首先,我们有一些示例形状。它们的类型在 enum class
中登记。每个形状 class 定义一个 MY_TYPE
作为它们各自的枚举条目。此外,他们必须实现基础 class' 纯虚拟 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
碰撞案例。
现在进入核心部分,函数table:
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);
table 本身是 std::function
的二维数组。请注意,这些函数接受指向 PhysicsObject
的指针,而不是派生的 classes。然后,我们使用一些宏来方便注册。当然,相应的代码可以手写,我很清楚使用宏通常被认为是坏习惯这一事实。然而,在我看来,这些事情正是宏的用武之地,只要您使用有意义的名称,并且不会弄乱您的全局名称空间,它们就是 acceptable。再次注意,只有 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 函数中的代码。传递的两个参数(这些是具体的 classes Box
和 Circle
)的类型(来自 ObjectType
枚举)被获取并用于索引 table.整个机制假定类型(如枚举中定义)有一个总顺序,并检查 Circle
-Box
碰撞的函数是否确实按此顺序为参数注册。 assert
告诉你是否做错了(不小心注册了 Box
-Circle
)。然后在 table 中为这对特定类型注册了一个 lambda 函数。该函数本身采用 PhysicsObject*
类型的两个参数,并在调用注册函数之前将它们转换为具体类型。
接下来,我们可以看看table是如何使用的。现在很容易实现一个函数来检查任意两个 PhysicsObject
s:
的碰撞
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
功能,但你必须在所有情况下都这样做。
- 使用宏将它们注册到您的 table:
REGISTER_RESOLVE_FUNCTION(Triangle, Triangle, &ResolveTriangleTriangle);
就是这样。无需向 PhysicsObject
添加更多方法,无需在所有现有类型中实现方法。
我知道这种方法的一些 'flaws',例如使用宏,具有所有类型的中央 enum
并依赖于全局 table。如果形状 classes 被构建到多个共享库中,后一种情况可能会导致一些麻烦。然而,以我的拙见,这种方法非常实用(除了非常特殊的用例),因为它不会像其他方法(例如双重分派)那样导致代码爆炸。
我看到有人使用指向基 class 的指针容器来保存共享相同虚函数的对象组。是否可以将派生 class 的重载函数与这些基 class 指针一起使用。很难解释我的意思,但(我认为)很容易用代码显示
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 成员(如下所示),但我很快意识到它有完全相同的问题。
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
// ...
};
通过谷歌搜索问题似乎可以使用转换来解决,但我无法弄清楚如何找到要转换为的正确类型(它也很丑陋)。我怀疑我问错了问题,有更好的方法来构建我的代码,它可以回避这个问题并获得相同的结果。
使用双重调度,它会是这样的:
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);}
// ...
};
我这样做的方法是构建一个 Extent
class 来告诉您物体的物理周长,也许是关于其重心的。此外,您还可以
virtual const Extent& getExtent() const = 0;
在PhysicsObject
class。然后,您为每个对象类型实施一次 getExtent
。
你的碰撞检测线变成
ResolveCollision(objA->getExtent(), objB->getExtent());
尽管从某种意义上说,随着复杂性被推向 Extent
class,这只不过是把罐头踢到路边而已,这种方法可以很好地扩展,因为您只需要每个对象构建一个方法。
另一种双分派机制是侵入性的,因为新形状需要调整 所有 现有形状。必须重新编译 Circle
class,例如,如果您引入 Ellipse
class,这对我来说是一种代码味道。
我将草拟一个不依赖双重调度的实现。相反,它使用 table 来注册所有函数。然后使用对象的动态类型(作为基础 class 传递)访问此 table。
首先,我们有一些示例形状。它们的类型在 enum class
中登记。每个形状 class 定义一个 MY_TYPE
作为它们各自的枚举条目。此外,他们必须实现基础 class' 纯虚拟 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
碰撞案例。
现在进入核心部分,函数table:
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);
table 本身是 std::function
的二维数组。请注意,这些函数接受指向 PhysicsObject
的指针,而不是派生的 classes。然后,我们使用一些宏来方便注册。当然,相应的代码可以手写,我很清楚使用宏通常被认为是坏习惯这一事实。然而,在我看来,这些事情正是宏的用武之地,只要您使用有意义的名称,并且不会弄乱您的全局名称空间,它们就是 acceptable。再次注意,只有 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 函数中的代码。传递的两个参数(这些是具体的 classes Box
和 Circle
)的类型(来自 ObjectType
枚举)被获取并用于索引 table.整个机制假定类型(如枚举中定义)有一个总顺序,并检查 Circle
-Box
碰撞的函数是否确实按此顺序为参数注册。 assert
告诉你是否做错了(不小心注册了 Box
-Circle
)。然后在 table 中为这对特定类型注册了一个 lambda 函数。该函数本身采用 PhysicsObject*
类型的两个参数,并在调用注册函数之前将它们转换为具体类型。
接下来,我们可以看看table是如何使用的。现在很容易实现一个函数来检查任意两个 PhysicsObject
s:
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
功能,但你必须在所有情况下都这样做。 - 使用宏将它们注册到您的 table:
REGISTER_RESOLVE_FUNCTION(Triangle, Triangle, &ResolveTriangleTriangle);
就是这样。无需向 PhysicsObject
添加更多方法,无需在所有现有类型中实现方法。
我知道这种方法的一些 'flaws',例如使用宏,具有所有类型的中央 enum
并依赖于全局 table。如果形状 classes 被构建到多个共享库中,后一种情况可能会导致一些麻烦。然而,以我的拙见,这种方法非常实用(除了非常特殊的用例),因为它不会像其他方法(例如双重分派)那样导致代码爆炸。