4

主要问题很简单,真的。给定一个基类(更抽象的)和多个需要相互交互的派生类,你如何去做呢?

举一个更具体的例子,这里是一个 2d 视频游戏的 hitboxes 实现:

#include <stdio.h>
#include <vector>

#include "Header.h"


bool Hitbox::isColliding(Hitbox* otherHtb) {
    printf("Hitbox to hitbox.\n");
    return this->isColliding(otherHtb);
}

bool CircleHitbox::isColliding(Hitbox* otherHtb) {
    printf("Circle to hitbox.\n");

    // Try to cast to a circle.
    CircleHitbox* circle = dynamic_cast<CircleHitbox*>(otherHtb);
    if (circle) {
        return this->isColliding(circle);
    }

    // Try to cast to a square.
    SquareHitbox* square = dynamic_cast<SquareHitbox*>(otherHtb);
    if (square) {
        return this->isColliding(square);
    }

    // Default behaviour.
    return 0;
}

bool CircleHitbox::isColliding(CircleHitbox* otherHtb) {
    printf("Circle to circle.\n");

    // Suppose this function computes whether the 2 circles collide or not.
    return 1;
}

bool CircleHitbox::isColliding(SquareHitbox* otherHtb) {
    printf("Circle to square.\n");

    // Suppose this function computes whether the circle and the square collide or not.
    return 1;
}

// This class is basically the same as the CircleHitbox class!
bool SquareHitbox::isColliding(Hitbox* otherHtb) {
    printf("Square to hitbox.\n");

    // Try to cast to a circle.
    CircleHitbox* circle = dynamic_cast<CircleHitbox*>(otherHtb);
    if (circle) {
        return this->isColliding(circle);
    }

    // Try to cast to a square.
    SquareHitbox* square = dynamic_cast<SquareHitbox*>(otherHtb);
    if (square) {
        return this->isColliding(square);
    }

    // Default behaviour.
    return 0;
}

bool SquareHitbox::isColliding(CircleHitbox* otherHtb) {
    printf("Square to circle.\n");

    // Suppose this function computes whether the square and the circle collide or not.
    return 1;
}

bool SquareHitbox::isColliding(SquareHitbox* otherHtb) {
    printf("Square to square.\n");

    // Suppose this function computes whether the 2 squares collide or not.
    return 1;
}


int main() {
    CircleHitbox a, b;
    SquareHitbox c;
    std::vector<Hitbox*> hitboxes;

    hitboxes.push_back(&a);
    hitboxes.push_back(&b);
    hitboxes.push_back(&c);
    
    // This runtime polymorphism is the subject here.
    for (Hitbox* hitbox1 : hitboxes) {
        printf("Checking all collisions for a new item:\n");
        for (Hitbox* hitbox2 : hitboxes) {
            hitbox1->isColliding(hitbox2);
            printf("\n");
        }
    }

    return 0;
}

与头文件:

#pragma once

class Hitbox {
public:
    virtual bool isColliding(Hitbox* otherHtb);
};

class CircleHitbox : public Hitbox {
public:
    friend class SquareHitbox;

    bool isColliding(Hitbox* otherHtb) override;
    bool isColliding(CircleHitbox* otherHtb);
    bool isColliding(SquareHitbox* otherHtb);
};

class SquareHitbox : public Hitbox {
public:
    friend class CircleHitbox;

    bool isColliding(Hitbox* otherHtb) override;
    bool isColliding(CircleHitbox* otherHtb);
    bool isColliding(SquareHitbox* otherHtb);
};

我对此的主要问题是每个派生类都需要在重写函数中进行的“is-a”检查。

我看到的替代方案是访问者设计模式,但这可能:

  1. 对于这个看似简单的问题来说太复杂了。

  2. 导致的问题多于解决方案。

该代码应该保留的一个特性是,没有派生类被强制实现与每个(或任何其他派生类)的交互。另一个是能够将所有派生对象存储在基本类型数组中而无需任何对象切片。

4

3 回答 3

2

这是经典双重调度的简化示例(未经测试)。

struct Circle;
struct Rectangle;

struct Shape {
  virtual bool intersect (const Shape&) const = 0;
  virtual bool intersectWith (const Circle&) const = 0;
  virtual bool intersectWith (const Rectangle&) const = 0;
};

struct Circle : Shape {
  bool intersect (const Shape& other) const override { 
     return other.intersectWith(*this);
  }
  bool intersectWith (const Circle& other) const override {
     return /* circle x circle intersect code */;
  }
  bool intersectWith (const Rectangle& other) const override {
     return /* circle x rectangle intersect code*/;
  }
};

struct Rectangle : Shape {
  bool intersect (const Shape& other) const override { 
     return other.intersectWith(*this);
  }
  bool intersectWith (const Circle& other) const override {
     return /* rectangle x circle intersect code */;
  }
  bool intersectWith (const Rectangle& other) const override {
     return /* rectangle x rectangle intersect code*/;
  }
};

如你所见,你离得并不远。

笔记:

  1. return intersectWith(*this);需要在每个派生类中重复。方法的文本每次都是一样的,只是类型this不同。这可以被模板化以避免重复,但它可能不值得。
  2. Shape类(当然还有它的每个派生类)需要了解所有Shape派生类。这会在类之间产生循环依赖。有一些方法可以避免它,但这些确实需要强制转换。

这不是多分派问题的解决方案,但它是一个解决方案。基于变体的解决方案可能更可取,也可能不更可取,具体取决于您的代码中还有哪些其他内容。

于 2021-12-03T19:07:46.937 回答
1

交互可以由基类本身管理。像这样的东西:

struct CircleHitBox;
struct SquareHitBox;

struct HitBox
{

template <class HITBOX>
bool is_colliding(HITBOX) const
{
    if constexpr (std::is_same_v<HITBOX, CircleHitBox>)
    {
        std::cout << "A CircleHitBox hit me.\n";
        return true;
    }
    else if constexpr (std::is_same_v<HITBOX, SquareHitBox>)
    {
        std::cout << "A SquareHitBox hit me.\n";
        return true;
    }
}            
};

此外,每个子类都可以在一个map或某个结构中注册自己,因此您可以使用循环(扫描map)而不是if else语句。

于 2021-12-02T20:54:58.317 回答
1

使用 C++ 17 有一个简单而优雅的解决方案,它允许您在没有虚函数开销的情况下运行时多态性:

#include <iostream>

namespace hitbox
{
    struct rectangle
    {
        double h,
                w;
    };

    struct circle
    {
        double r;
    };
}

bool is_colliding(const hitbox::rectangle &, const hitbox::circle &) {
    std::cout << "Rectangle + Circle" << std::endl;
    return true;
}

bool is_colliding(const hitbox::rectangle &, const hitbox::rectangle &) {
    std::cout << "Rectangle + Rectangle" << std::endl;
    return true;
}

bool is_colliding(const hitbox::circle &, const hitbox::circle &) {
    std::cout << "Circle + Circle" << std::endl;
    return true;
}

#include <variant>

using hitbox_variant = std::variant<hitbox::rectangle, hitbox::circle>;

bool is_colliding(const hitbox_variant &hitboxA, const hitbox_variant &hitboxB)
{
    return std::visit([](const auto& hitboxA, const auto& hitboxB)
                      {
                          return is_colliding(hitboxA, hitboxB);
                      }, hitboxA, hitboxB);
}

int main()
{
    hitbox_variant rectangle{hitbox::rectangle()},
            circle{hitbox::circle()};

    is_colliding(rectangle, rectangle);
    is_colliding(rectangle, circle);
    is_colliding(circle, circle);

    return 0;
}

https://godbolt.org/z/KzPhq5Ehr - 你的例子


您的问题来自您对删除类型的必要性的假设。当您删除类型时(在您的情况下,通过将它们简化为基本抽象类),您会删除有关它们的属性的信息(如它们的几何图形)。
但是你为什么首先使用类型擦除呢?
因为您想将所有需要的对象的引用存储在一个容器中,这要求它们属于同一类型
那么,你需要吗?对于您在编译期间已知的对象类型之间的碰撞计算的特定问题,这是一个选择不当的抽象。因此,除非您没有获得在运行时“创建”的对象类型,不要擦除类型

将对象存储在多个容器中,以便在需要了解类型时使用。它将减少运行时反射的冗余成本(通过dynamic_cast、枚举等)。

// you HAVE to implement them because your program KNOWS about them already
bool has_collision(const CircleHitBox& circle, const CircleHitBox& circle);
bool has_collision(const CircleHitBox& circle, const SquareHitbox& square);
bool has_collision(const SquareHitbox& square, const SquareHitbox& square);

struct scene
{  
  template <typename T>
  using ref = std::reference_wrappet<T>;
  std::vector<ref<const CircleHitBox>> circleHitBoxes;
  std::vector<ref<const SquareHitbox>> squareHitBoxes;
  std::vector<ref<const HitBox>> otherHitBoxes;
};

// here you create an object for your scene with all the relevant objects of known types
void calc_collisions(scene s)
{
  // do your calculations here
}

您可以使用某种注册表,例如在实体组件系统 ( EnTT ) 中。


牢记:
您在这里解决了碰撞问题,因此您必须了解特定对象的属性。这意味着在不违反Liskov Substitution Principle的情况下,您不能在这里拥有运行时多态性。LSP 意味着抽象基类后面的每个对象都是可互换的,并且具有完全相同的属性-在您进行某种类型转换之前,这些属性是不一样的。

另外,HitBoxtype 最好只是一个 POD 类型来存储数据。您不需要任何非静态成员函数,尤其是虚拟函数。不要混合数据和行为,除非你需要(例如有状态的功能对象)。

于 2021-12-02T21:33:27.837 回答