[英]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 中進行。
我看到的替代方案是訪問者設計模式,但這可能:
對於這個看似簡單的問題來說太復雜了。
導致的問題多於解決方案。
此代碼應保留的一個屬性是,派生的 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*/;
}
};
如你所見,你離得並不遠。
筆記:
return intersectWith(*this);
需要在每個派生的 class 中重復。 方法的文本每次都是一樣的,但是this
的類型不同。 這可以被模板化以避免重復,但它可能不值得。Shape
基礎 class(當然還有它的每個派生類)需要了解所有Shape
派生類。 這會在類之間產生循環依賴。 有一些方法可以避免它,但這些確實需要強制轉換。這不是多分派問題的解決方案,但它是一個解決方案。 基於變體的解決方案可能更可取,也可能不更可取,具體取決於您的代碼中還有哪些其他內容。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.