如果在 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;

PhysicsObjectclass。然后,您为每个对象类型实施一次 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 BoxCircle)的类型(来自 ObjectType 枚举)被获取并用于索引 table.整个机制假定类型(如枚举中定义)有一个总顺序,并检查 Circle-Box 碰撞的函数是否确实按此顺序为参数注册。 assert 告诉你是否做错了(不小心注册了 Box-Circle)。然后在 table 中为这对特定类型注册了一个 lambda 函数。该函数本身采用 PhysicsObject* 类型的两个参数,并在调用注册函数之前将它们转换为具体类型。

接下来,我们可以看看table是如何使用的。现在很容易实现一个函数来检查任意两个 PhysicsObjects:

的碰撞
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 中为这些相应类型注册的函数。请注意,如果参数不按顺序,它们将被交换。因此,您可以自由调用 ResolveCollisionBoxCircle,然后它将在内部调用为 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. 使用宏将它们注册到您的 table:REGISTER_RESOLVE_FUNCTION(Triangle, Triangle, &ResolveTriangleTriangle);

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

我知道这种方法的一些 'flaws',例如使用宏,具有所有类型的中央 enum 并依赖于全局 table。如果形状 classes 被构建到多个共享库中,后一种情况可能会导致一些麻烦。然而,以我的拙见,这种方法非常实用(除了非常特殊的用例),因为它不会像其他方法(例如双重分派)那样导致代码爆炸。