簡體   English   中英

C++:對於這種(多分派)運行時多態性,是否有更優雅的解決方案?

[英]C++: Is there a more elegant solution to this (multiple dispatch) runtime polymorphism?

主要問題很簡單,真的。 給定一個基礎(更抽象的) class 和多個需要相互交互的派生的,你如何 go 去做呢?

舉一個更具體的例子,這里是一個 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;
}

使用 header 文件:

#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”檢查,每個派生的 class 都需要在覆蓋的 function 中進行。

我看到的替代方案是訪問者設計模式,但這可能:

  1. 對於這個看似簡單的問題來說太復雜了。

  2. 導致的問題多於解決方案。

此代碼應保留的一個屬性是,派生的 class 不會被迫與其他派生的 class 的每個(或任何一個)進行交互。 另一個是能夠將所有派生對象存儲在基本類型數組中,而無需任何 object 切片。

交互可以由基礎 class 本身管理。 像這樣的東西:

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";
    }
    else if constexpr (std::is_same_v<HITBOX, SquareHitBox>)
    {
        std::cout << "A SquareHitBox hit me.\n";
    }
}            
};

此外,每個子類都可以在map或某些結構中注冊自己,因此您可以使用循環(掃描map )或switch語句而不是if else語句。

您的問題來自您對刪除類型的必要性的假設。 當您刪除類型時(在您的情況下,通過將它們簡化為基本抽象類),您會刪除有關它們的屬性的信息(如它們的幾何圖形)。
但是你為什么首先使用類型擦除呢?
因為您想將所有需要的對象的引用存儲在一個容器中,這要求它們屬於同一類型
那么,你需要嗎? 對於您在編譯期間已知的對象類型之間的碰撞計算的特定問題,這是一個選擇不當的抽象。 因此,除非您沒有獲得在運行時“創建”的 object 類型,否則不要擦除 type

將對象存儲在多個容器中,以便在需要了解類型時使用。 它將減少運行時反射的冗余成本(通過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 意味着抽象基礎 class 后面的每個 object都是可互換的,並且具有完全相同的屬性- 在您進行某種類型轉換之前,這些屬性並不相同。

另外, HitBox類型最好只是一個 POD 類型來存儲數據。 您不需要任何非靜態成員函數,尤其是虛擬函數。 不要混合數據和行為,除非你需要(例如有狀態的功能對象)。

我假設您的問題是您必須在代碼中重載多個isColliding ,並且如果您還添加其他命中框形狀將很難管理。

對我來說,你的isColliding function 應該有點像:

bool Hitbox::isColliding(Hitbox* otherHtb)
{
    auto angle = atan((otherHtb->center.y - center.y) / (otherHtb->center.x - center.x));
    auto distance = hypot(otherHtb->center.y - center.y, otherHtb->center.x - center.x);
    return center_to_edge(angle) + otherHtb->center_to_edge(angle + PI) >= distance;
}

現在您需要做的就是覆蓋每個形狀的center_to_edge

注意:我不喜歡 2d 數學,所以無論我回答什么都可能正確/有幫助,也可能不正確/有幫助。

這是經典雙重調度的簡化示例(未經測試)。

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); 需要在每個派生的 class 中重復。 方法的文本每次都是一樣的,但是this的類型不同。 這可以被模板化以避免重復,但它可能不值得。
  2. Shape基礎 class(當然還有它的每個派生類)需要了解所有Shape派生類。 這會在類之間產生循環依賴。 有一些方法可以避免它,但這些確實需要強制轉換。

這不是多分派問題解決方案,但它是一個解決方案。 基於變體的解決方案可能更可取,也可能不更可取,具體取決於您的代碼中還有哪些其他內容。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM