2017-09-05 4 views
3

Ich habe Leute gesehen, die Behälter mit Zeigern zu der Basisklasse verwenden, um Gruppen von Objekten zu halten, die die gleichen virtuellen Funktionen teilen. Ist es möglich, überladene Funktionen der abgeleiteten Klasse mit diesen Basisklassenzeigern zu verwenden. Es ist schwer zu erklären, was ich meine, aber (glaube ich) einfach mit Code zu erhaltenÜberladen Sie eine Funktion mit einem abgeleiteten Klassenargument, wenn Sie nur einen Zeiger auf die Basisklasse in C++ haben

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 
} 

Meine erste Idee dieser Funktionen seines virtuellen Klassenmitglieder auch (siehe unten) zu machen, aber ich merkte schnell, dass es genau das hat das gleiche Problem.

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

Von googeln das Problem es wie vielleicht scheint es kann unter Verwendung von Guss gelöst werden, aber ich kann nicht herausfinden, wie man den richtigen Typen zu finden, um zu werfen (auch sie ist hässlich). Ich vermute, dass ich die falsche Frage stelle, und es gibt einen besseren Weg, meinen Code zu strukturieren, der dieses Problem umgeht und das gleiche Ergebnis erzielt.

+3

Schauen Sie sich [double dispatch] (https://en.wikipedia.org/wiki/Double_dispatch) an. – Jarod42

+0

Das Problem ist offensichtlich - keine Überladung benötigt 2 'PhysicsObject' als Argumente, daher der Fehler. Stellen Sie entweder die Überladung bereit oder verwenden Sie 'dynamic_cast'. –

+0

Sicher kann ich eine Überladung erstellen, die zwei PhysicsObjects benötigt, aber ich weiß immer noch nicht, welche Art von Physik-Objekt sie sind, also habe ich das gleiche Problem. Danke @ Jarod42, werde es jetzt lesen. – Beetroot

Antwort

2

Mit Doppel-Ausgabe, es wäre so etwas wie:

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

Ihr Code zeigt es deutlich, aber nur um es zu erwähnen: Der offensichtliche Nachteil ist, dass für 'n' involvierte Klassen jede einzelne Klasse' n' Methoden benötigt. –

+0

Danke, ich hatte vorher noch nichts von Doppelversendung gehört, aber mit Ihrer Antwort war es leicht, sich an meinen eigenen Code anzupassen. Es scheint eine einfache Lösung zu sein, wenn Sie nicht zu viele Formen haben. – Beetroot

+0

Ich habe auch getan [Mehrere Versand] (https://Stackoverflow.com/a/29345504/2684539), die die Verwendung vereinfachen könnte (auch wenn vollständige Implementierung komplizierter ist). – Jarod42

2

So wie ich das tun würde dies eine Extent Klasse zu bauen, ist, dass Sie über den physischen Umfang eines Objekt sagt, vielleicht in Bezug auf ihren Barycentrum. Darüber hinaus würden Sie

virtual const Extent& getExtent() const = 0;

in der PhysicsObject Klasse. Sie implementieren dann getExtent einmal pro Objekttyp.

Ihre Kollisionserkennung Linie wird

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

Obwohl in gewissem Sinne, tut dies etwas mehr als die Dose auf der Straße kicken wie die Komplexität der Extent Klasse geschoben wird, wird der Ansatz, da Sie skaliert gut Sie müssen nur eine Methode pro Objekt erstellen.

Der alternative Doppelversandmechanismus ist insofern intrusiv, als eine neue Form eine Anpassung an alle vorhandenen Formen erfordert. Die Klasse Circle neu kompilieren, zum Beispiel, wenn Sie eine Ellipse Klasse einführen, sagen, ist ein Code-Geruch für mich.

+0

Dies könnte in der Tat der einfachste Ansatz sein. Allerdings kann Ihre 'Extent'-Klasse kompliziert/komplex werden, wenn es eine Anzahl von Formen gibt und diese sehr unterschiedlich sind. Ein weiteres Problem, mit dem Sie möglicherweise konfrontiert werden, ist, dass Sie jetzt nur an dem Umfang arbeiten, nicht mehr an den tatsächlichen Objekten. Solange Sie nur eine Kollision erkennen, sollte dies ausreichend sein. Wenn Sie abhängig von der erkannten Kollision Kräfte auf die Objekte anwenden oder die Objekte an einen Rückruf übergeben möchten, der bei einer Kollision aufgerufen wird, benötigen Sie möglicherweise die Typen (und Zeiger auf) der tatsächlichen Formen. –

+0

@nh_ aber wenn du schlau bist, könnte die Extent-Klasse auch das Rendering übernehmen. Und Dinge wie das Trägheitsmoment sind nur eine Funktion von Perimeter und Baryzentrum (unter der Annahme einer gleichmäßigen Dichte), so dass es auch Spin geben könnte. – Bathsheba

+0

Kein Zweifel. Ich frage mich nur, wie Sie die "Ausdehnung" umsetzen würden. Müssten Sie nicht die Fälle von einem Center + Durchmesser und Box Case dort unterscheiden? Und würde das nicht in einer Menge von 'if (a-> isCircular() && b-> isCircular()) {/*...*/} enden, wenn (a-> isCircular() && b-> isBox()) {/*...*/} sonst ... '? –

2

Ich werde eine Implementierung skizzieren, die nicht auf Doppelversand angewiesen ist. Stattdessen verwendet es eine Tabelle, in der alle Funktionen registriert sind. Auf diese Tabelle wird dann über den dynamischen Typ der Objekte zugegriffen (als Basisklasse übergeben).

Zuerst haben wir einige Beispielformen. Ihre Typen sind in einem enum class eingetragen. Jede Formklasse definiert einen MY_TYPE als ihren jeweiligen Enum-Eintrag. Darüber hinaus müssen sie rein virtuelle type Methode der Basisklasse implementieren:

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

Als nächstes müssen Sie Ihre Kollisionsauflösung Funktionen, müssen sie je nach den Formen umgesetzt werden, natürlich.

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

Beachten Sie, dass wir nur Circle-Box hier keine Box-Circle, wie ich ihre Kollision übernehmen in der gleichen Art und Weise erfasst wird.Mehr zu dem Kollisionsfall Box - Circle später.

Nun zu dem Kernteil, die Funktionstabelle:

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); 

Der Tisch selbst ist ein 2D-Array aus std::function s. Beachten Sie, dass diese Funktionen Zeiger auf PhysicsObject akzeptieren, nicht die abgeleiteten Klassen. Dann verwenden wir einige Makros für die einfache Registrierung. Natürlich könnte der entsprechende Code von Hand geschrieben werden und ich bin mir durchaus der Tatsache bewusst, dass die Verwendung von Makros typischerweise als schlechte Angewohnheit betrachtet wird. Meiner Meinung nach sind diese Dinge jedoch genau das, wofür Makros gut sind, und solange Sie aussagekräftige Namen verwenden, die Ihren globalen Namensraum nicht durcheinander bringen, sind sie akzeptabel. Beachten Sie nochmals, dass nur Circle - Box registriert ist, nicht umgekehrt.

Nun zur Phantasie Makro:

#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; \ 
    }(); 

Das Makro definiert einen eindeutig benannte Variable (mit der Zeilennummer), aber diese Variable dient lediglich um den Code zu bekommen innerhalb der Initialisierung Lambda-Funktion ausgeführt werden. Die Typen (aus der ObjectType enum) der übergebenen zwei Argumente (das sind die konkreten Klassen Box und Circle) werden genommen und verwendet, um die Tabelle zu indizieren. Der gesamte Mechanismus geht davon aus, dass es eine Gesamtordnung für die Typen gibt (wie in der Enumeration definiert), und prüft, ob für die Argumente in dieser Reihenfolge tatsächlich eine Funktion für die Kollision Circle10 registriert ist. Die assert sagt Ihnen, wenn Sie es falsch machen (versehentlich Registrierung Box - Circle). Dann wird eine Lambda-Funktion in der Tabelle für dieses spezielle Typenpaar registriert. Die Funktion selbst nimmt zwei Argumente vom Typ PhysicsObject* und wandelt sie vor dem Aufruf der registrierten Funktion in die konkreten Typen um.

Als nächstes können wir uns ansehen, wie die Tabelle dann verwendet wird. Es ist nun leicht eine einzige Funktion zu implementieren, die Kollision von zwei beliebigen PhysicsObject s überprüft:

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); 
} 

Es nimmt die dynamischen Typen des Arguments und übergibt sie an die Funktion für die jeweiligen Typen innerhalb der ResolveFunctionTable registriert. Beachten Sie, dass die Argumente ausgetauscht werden, wenn sie nicht in der richtigen Reihenfolge sind. Somit können Sie ResolveCollision mit Box und Circle aufrufen und intern die für Circle - Box registrierte Kollision aufrufen.

Schließlich werde ich ein Beispiel geben, wie man es benutzt:

int main(int argc, char* argv[]) 
{ 
    Box box; 
    Circle circle; 

    ResolveCollision(&box, &box); 
    ResolveCollision(&box, &circle); 
    ResolveCollision(&circle, &box); 
    ResolveCollision(&circle, &circle); 
} 

Leicht ist es nicht? Siehe this für eine funktionierende Implementierung der obigen.


Nun, was ist der Vorteil dieses Ansatzes? Der obige Code ist im Grunde alles, was Sie benötigen, um eine beliebige Anzahl von Formen zu unterstützen. Angenommen, Sie fügen eine Triangle hinzu. Alles, was Sie tun müssen, ist:

  1. Triangle zum ObjectType Enum einen Eintrag hinzufügen.
  2. Implementieren Sie Ihre ResolveTriangleXXX Funktionen, aber Sie müssen dies in allen Fällen tun.
  3. sie auf den Tisch mit dem Makro registrieren:

Das ist es. Es müssen keine weiteren Methoden zu PhysicsObject hinzugefügt werden, es müssen keine Methoden in allen vorhandenen Typen implementiert werden.

Ich bin mir einiger 'Fehler' dieses Ansatzes bewusst, wie die Verwendung von Makros, die eine zentrale enum aller Typen haben und sich auf eine globale Tabelle verlassen. Der letztere Fall könnte zu Problemen führen, wenn die Formklassen in mehrere gemeinsam genutzte Bibliotheken integriert sind. Meiner bescheidenen Meinung nach ist dieser Ansatz jedoch ziemlich praktisch (außer für sehr spezielle Anwendungsfälle), da er nicht zur Codeexplosion führt, wie dies bei anderen Ansätzen der Fall ist (z. B. Doppelversand).

+0

Sie haben sich sehr viel Mühe gegeben - obwohl ich das nicht so machen würde (mein Ansatz ist viel einfacher), das ist eine Aufzählung wert: es ist eine plausible Lösung. Danke, dass du dir die Zeit genommen hast. – Bathsheba

+0

Vielen Dank, schätzen Sie es! –

+0

@nh_ Danke, ich hatte das Problem vorausgesehen, dass es beim Hinzufügen weiterer Formen, die Ihre Lösung löst, nicht gut skalieren würde. Es war schwierig, eine Antwort zu wählen, da sie alle auf unterschiedliche Art und Weise helfen. Ich denke, Ihre war vielleicht die beste allgemeine Antwort, die vielen Menschen helfen wird, aber die andere Antwort war für mich persönlich jetzt einfacher umzusetzen. Ich werde zurückkommen und deine Antwort noch einmal studieren, wenn ich mehr Zeit habe, da ich mit all den Konzepten im Moment nicht sehr vertraut bin. – Beetroot

Verwandte Themen