2017-09-05 33 views
3

我见过有人使用指向基类的指针容器来容纳共享相同虚拟函数的对象组。是否有可能使用派生类的重载函数与这些基类指针。这是很难解释我的意思,但(我认为)易于使用代码来显示如果你只有一个指向C++基类的指针,就用一个派生类参数重载一个函数

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 
    // ... 
}; 

从Google上搜寻它看起来可能可以使用铸造解决,但我无法弄清楚如何找到正确的类型强制转换为(也实在是丑陋的)问题。我怀疑我问的是错误的问题,并且有更好的方法来构建我的代码来避免这个问题并获得相同的结果。

+3

看看[double dispatch](https://en.wikipedia.org/wiki/Double_dispatch)。 – Jarod42

+0

问题很明显 - 没有重载需要2个'PhysicsObject'作为参数,因此错误。要么提供超载,要么使用'dynamic_cast'。 –

+0

当然,我可以创建一个需要两个PhysicsObjects的重载,但我仍然不知道它们是哪种类型的物理对象,所以我有同样的问题。谢谢@ Jarod42,现在会给它一个阅读。 – Beetroot

回答

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);} 
    // ... 
}; 
+0

你的代码清楚地显示了它,但只是提及它:显而易见的缺点是对于涉及'n'的类,每个类都需要'n'方法。 –

+0

谢谢,我之前没有听说过双重调度,但使用您的答案很容易适应我自己的代码。如果你没有太多的形状,这看起来像一个简单的解决方案。 – Beetroot

+0

我也完成了[Multiple dispatch](https://stackoverflow.com/a/29345504/2684539),这可能会简化使用(即使完整实现更复杂)。 – Jarod42

2

我会做到这一点是要建立一个Extent类,告诉你一个对象的物理边界,也许对于这样的重心。此外,你必须

virtual const Extent& getExtent() const = 0;

PhysicsObject

。然后,您对每个对象类型执行一次getExtent

您的碰撞检测线变得

ResolveCollision(objA->getExtent(), objB->getExtent()); 

虽然,在某种意义上,这确实有点多踢罐头的道路的复杂性推到了Extent类,该办法将很好地扩展,因为你只需要为每个对象构建一个方法。

另一种双派遣机制是侵入式的,因为新形状需要调整以现有形状为全部。例如,需要重新编译Circle类,例如,如果您引入了Ellipse类,那么对我而言就是一种代码味道。

+0

这可能是最简单的方法。然而,如果有多种形状,你的'Extent'类可能会变得复杂/复杂,并且它们差别很大。你可能遇到的另一个问题是,你现在只能在范围内工作,不再在实际的对象上工作。只要你只是检测到碰撞,这应该是足够的。如果您想对对象施加力,这取决于检测到的碰撞,或者需要将对象传递给碰撞时调用的回调函数,您可能需要(和指向)实际形状的类型。 –

+0

@nh_但是如果你很聪明,Extent类也可以接管渲染。像惯性矩这样的东西只是周边和重心的一个函数(假设密度均匀),所以它也可以旋转。 – Bathsheba

+0

毫无疑问。只是想知道你将如何执行'Extent'。你不需要区分中心+直径和盒子的情况吗?如果(a-> isCircular()&& b-> isCircular()){/*...*/}否则if(a-> isCircular()&& b-> isBox()){/*...*/} else ...'? –

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函数。传递的两个参数(这些是具体类BoxCircle)的类型(来自ObjectType enum)被采用并用于索引表。整个机制假定类型的总顺序(如枚举中定义的那样),并检查碰撞的函数是否确实以该顺序为参数注册。 assert告诉你,如果你做错了(意外注册Box - Circle)。然后一个lambda函数被注册在这个特定的类型对中。该函数本身带有两个参数PhysicsObject*,并在调用注册函数之前将它们转换为具体类型。

接下来,我们可以看看如何使用表格。现在是很容易实现,检查任何两个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内的各登记注册类型的功能。请注意,如果参数不正确,参数将被交换。因此,您可以用BoxCircle自由调用ResolveCollision,然后它将在内部调用注册为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。您只需要:

  1. 将条目Triangle添加到ObjectType枚举中。
  2. 实现您的ResolveTriangleXXX函数,但您必须在所有情况下都这样做。
  3. 使用宏将其保存到您的表:

就是这样。无需向PhysicsObject添加更多方法,无需在所有现有类型中实现方法。

我知道这种方法的一些“缺陷”,如使用宏,具有所有类型的中心enum并依赖于全局表。如果形状类被构建到多个共享库中,后一种情况可能会导致一些麻烦。然而,在我看来,这种方法是非常实用的(除了非常特殊的用例),因为它不像其他方法(例如双重调度)那样导致代码的爆炸。

+0

你已经付出了很多努力 - 虽然不是我会这么做的方式(我的方法是简单得多的恕我直言),但这显然值得赞赏:这是一个合理的解决方案。感谢您抽出宝贵的时间。 – Bathsheba

+0

非常感谢,非常感谢! –

+0

@nh_谢谢,我已经预见到在添加解决方案解决的更多形状时,它不会很好地扩展。很难选择一个答案,因为他们都以不同的方式提供帮助。我认为你可能是帮助很多人的最好的一般答案,但是现在我个人可以更容易地采取其他答案。当我有更多的时间,因为我现在对所有的概念都不熟悉,所以我会再回来再研究一下你的答案。 – Beetroot

相关问题