2010-03-12 8 views
5

Dies ist aus Gründen der Frage stark vereinfacht. Sagen, ich habe eine Hierarchie:emulieren dynamische Dispatch in C++ basierend auf Template-Parameter

struct Base { 
    virtual int precision() const = 0; 
}; 

template<int Precision> 
struct Derived : public Base { 

    typedef Traits<Precision>::Type Type; 

    Derived(Type data) : value(data) {} 
    virtual int precision() const { return Precision; } 

    Type value; 

}; 

Ich mag eine Nicht-Template Funktion mit der Signatur:

Base* function(const Base& a, const Base& b); 

Wo die spezifische Art des Ergebnisses der Funktion vom gleichen Typ ist wie welche auch immer von a und b hat die größere Precision; so etwas wie der folgenden Pseudo-Code:

Base* function(const Base& a, const Base& b) { 

    if (a.precision() > b.precision()) 

     return new A(((A&)a).value + A(b.value).value); 

    else if (a.precision() < b.precision()) 

     return new B(B(((A&)a).value).value + ((B&)b).value); 

    else 

     return new A(((A&)a).value + ((A&)b).value); 

} 

Wo A und B die spezifischen Arten von a und b sind, respectively. Ich möchte function unabhängig davon betreiben, wie viele Instanziierungen von Derived es gibt. Ich möchte eine massive Tabelle von typeid() Vergleichen vermeiden, obwohl RTTI in den Antworten gut ist. Irgendwelche Ideen?

+2

Ich denke, Sie sollten erwähnen, dass Sie nicht den vollständigen Klassentyp kennen. Du kennst einfach die 'Base &'. Mehrere Antworten, einschließlich meiner eigenen, nahmen an, dass Sie den genauen Typ "Derived " kennen. –

+0

Hinweis auf einen Kommentar zu einer Antwort: Eine zusätzliche Anforderung besteht darin, dass die Funktion keine Vorlage sein kann. es muss die angegebene Base * (Base &, Base &) Signatur haben. –

+0

Eingeschlossen die Einschränkungen deutlicher in die Frage. –

Antwort

3

Sie können function() nicht direkt an einen Vorlagencode delegieren, ohne zwischen einer umfangreichen Liste aller möglichen Typen zu wählen, da Vorlagen zur Kompilierzeit dekomprimiert werden und die Funktion file() nicht weiß, was abgeleitet ist Typen, mit denen es tatsächlich aufgerufen wird. Sie müssen instantiations des Templat-Code kompiliert haben für jede Version Ihrer Templat-operation Funktion, die, die möglicherweise eine unendliche Menge ist erforderlich sein wird.

dieser Logik folgend, ist der einzige Ort, der alle Vorlagen weiß, die erforderlich sein könnte, ist die Derived Klasse selbst.Daher sollten Sie Ihre Derived Klasse ein Mitglied umfassen:

Derived<Precision> *operation(Base& arg2) { 
    Derived<Precision> *ptr = new Derived<Precision>; 
    // ... 
    return ptr; 
} 

Dann Sie function wie so definieren kann, und das Dispatching tun indirekt:

Base* function(const Base& a, const Base& b) { 
    if (a.precision() > b.precision()) 
    return a.operation(b); 
    else 
    return b.operation(a); 
} 

Beachten Sie, dass dies die vereinfachte Version ist; Wenn Ihre Operation in ihren Argumenten nicht symmetrisch ist, müssen Sie zwei Versionen der Memberfunktion definieren - eine mit this anstelle des ersten Arguments und eine mit der zweiten anstelle der zweiten.

Auch dies hat die Tatsache ignoriert, dass Sie einen Weg für a.operation benötigen, um eine geeignete Form von b.value zu erhalten, ohne den abgeleiteten Typ von b zu kennen. Sie müssen das selbst lösen - beachten Sie, dass es (durch die gleiche Logik wie zuvor) unmöglich ist, dies durch Templating auf den Typ b zu lösen, weil Sie zur Laufzeit versenden. Die Lösung hängt davon ab, welche Typen Sie genau haben und ob es für eine Art höherer Genauigkeit eine Möglichkeit gibt, einen Wert aus einem Derived Objekt mit gleicher Genauigkeit zu ziehen, ohne den genauen Typ dieses Objekts zu kennen. Dies ist möglicherweise nicht möglich. In diesem Fall haben Sie die lange Liste der Übereinstimmungen für Typ-IDs.

Sie müssen dies jedoch nicht in einer switch-Anweisung tun. Sie können jedem Derived eine Reihe von Elementfunktionen für Up-Casting zu einer Funktion mit größerer Genauigkeit geben. Zum Beispiel:

template<int i> 
upCast<Derived<i> >() { 
    return /* upcasted value of this */ 
} 

Dann Ihre operator Memberfunktion kann auf b.upcast<typeof(this)> arbeiten, und es braucht nicht explizit einen Wert des Typs das Casting zu tun hat, zu bekommen. Möglicherweise müssen Sie einige dieser Funktionen explizit instanziieren, damit sie kompiliert werden können. Ich habe nicht genug mit RTTI gearbeitet, um es mit Sicherheit zu sagen.

Grundsätzlich ist das Problem, dass, wenn Sie N mögliche Genauigkeiten haben, Sie N N mögliche Kombinationen haben, und jeder von diesen wird in der Tat separat kompilierten Code benötigen. Wenn Sie keine Templates in Ihrer Definition von function verwenden können, dann müssen Sie kompilierte Versionen von allen N N dieser Möglichkeiten haben, und irgendwie müssen Sie dem Compiler sagen, sie alle zu generieren, und irgendwie müssen Sie das Recht auswählen Eins, zu dem zur Laufzeit gesendet werden soll. Der Trick der Verwendung einer Member-Funktion entfernt einen dieser Faktoren von N, aber der andere bleibt bestehen, und es gibt keine Möglichkeit, ihn vollständig generisch zu machen.

+0

Das ist gut genug für mich.Der zweite Faktor wird dadurch beseitigt, dass "Base" eine Möglichkeit definiert, zur Laufzeit in einen beliebigen Typ zu konvertieren. –

+0

Oh, gut! Ich habe gerade bearbeitet, um darauf hinzuweisen, dass eine solche Einrichtung nützlich wäre, um den zweiten Faktor zu entfernen. –

1

Zuerst möchten Sie Ihre precision Mitglied einen static const int Wert, keine Funktion, so dass Sie zur Kompilierzeit darauf arbeiten können. In Derived, wäre es:

static const int precision = Precision; 

Dann müssen Sie einige Helfer structs die meisten genaue Basis/abgeleiteten Klasse zu bestimmen. Zunächst müssen Sie eine generische Helfer-Helfer-Struktur eine von zwei Arten auf einem boolean abhängig wählen:

template<typename T1, typename T2, bool use_first> 
struct pickType { 
    typedef T2 type; 
}; 

template<typename T1, typename T2> 
struct pickType<T1, T2, true> { 
    typedef T1 type; 
}; 

Dann pickType<T1, T2, use_first>::type wird T1 lösen, wenn use_firsttrue ist, und sonst zu T2. Also, dann verwenden wir diese die präziseste Art zu wählen:

template<typename T1, typename T2> 
struct mostPrecise{ 
    typedef pickType<T1, T2, (T1::precision > T2::precision)>::type type; 
}; 

Nun mostPrecise<T1, T2>::type geben Ihnen je nachdem, welche die beiden Arten einen größeren precision Wert hat. So können Sie Ihre Funktion wie folgt definieren:

template<typename T1, typename T2> 
mostPrecise<T1, T2>::type function(const T1& a, const T2& b) { 
    // ... 
} 

Und da haben Sie es.

+0

Dies ist sehr interessant und nützlich für ein anderes Projekt von mir, aber ich brauche die Entscheidung zur Laufzeit. –

+0

Danke. Ich werde dieses hier für den historischen Wert belassen, anstatt es zu löschen, aber siehe meine andere Antwort für Kommentare, um dies zur Laufzeit zu tun. –

4

Was Sie fragen, heißt multiple dispatch, aka Multimethoden. Es ist kein Feature der C++ - Sprache.

Es gibt Problemumgehungen für Sonderfälle, aber Sie können nicht vermeiden, selbst einige Implementierungen durchzuführen.

Ein häufiges Muster für Mehrfachversand heißt "Redispatch", auch bekannt als "rekursive verzögerte Dispatching". Grundsätzlich löst eine virtuelle Methode einen Parametertyp auf und ruft dann eine andere virtuelle Methode auf, bis alle Parameter aufgelöst sind. Die Funktion der Außenseite (wenn es eine gibt) ruft nur die erste dieser virtuellen Methoden auf.

Angenommen, es gibt n abgeleitete Klassen, es wird eine Methode geben, um den ersten Parameter aufzulösen, n um die zweite aufzulösen, n * n um die dritte aufzulösen - schlimmstenfalls sowieso. Das ist eine Menge manueller Arbeit, und die Verwendung von Bedingungsblöcken auf der Basis von typeid ist möglicherweise einfacher für die anfängliche Entwicklung, aber es ist robuster für die Wartung, Redispatch zu verwenden.

class Base; 
class Derived1; 
class Derived2; 

class Base 
{ 
    public: 
    virtual void Handle (Base* p2); 

    virtual void Handle (Derived1* p1); 
    virtual void Handle (Derived2* p1); 
}; 

class Derived1 : public Base 
{ 
    public: 
    void Handle (Base* p2); 

    void Handle (Derived1* p1); 
    void Handle (Derived2* p1); 
}; 

void Derived1::Handle (Base* p2) 
{ 
    p2->Handle (this); 
} 

void Derived1::Handle (Derived1* p1) 
{ 
    // p1 is Derived1*, this (p2) is Derived1* 
} 

void Derived1::Handle (Derived2* p1) 
{ 
    // p1 is Derived2*, this (p2) is Derived1* 
} 

// etc 

Implementierung dieser eine Vorlage für die abgeleiteten Klassen wäre schwierig, und die Metaprogrammierung zu handhaben es wahrscheinlich nicht lesbar, wartbaren und sehr zerbrechlich wäre. Wenn Sie den Versand mithilfe von Nicht-Vorlagenmethoden implementieren, ist die Verwendung einer Mixin-Vorlage (eine Vorlagenklasse, die die Basisklasse als Vorlagenparameter verwendet), um diese mit zusätzlichen Funktionen zu erweitern, möglicherweise nicht so schlecht.

Die visitor design pattern ist eng verwandt mit (im Grunde mit Redispatch IIRC implementiert).

Der andere Ansatz besteht darin, eine Sprache zu verwenden, die entwickelt wurde, um das Problem zu behandeln, und es gibt einige Optionen, die gut mit C++ funktionieren. Eine ist die Verwendung von treecc - eine domänenspezifische Sprache für die Behandlung von AST-Knoten und Operationen mit mehreren Zuständen, die, wie lex und yacc, "Quellcode" als Ausgabe erzeugen.

Um die Dispatch-Entscheidungen zu bearbeiten, werden nur Switch-Anweisungen generiert, die auf einer AST-Knoten-ID basieren. Dies kann genauso gut eine dynamisch typisierte Wertklassen-ID sein, IYSWIM. Dies sind jedoch Switch-Anweisungen, die Sie nicht schreiben oder pflegen müssen, was ein wesentlicher Unterschied ist. Das größte Problem, das ich habe, ist, dass AST-Knoten manipuliert werden, was bedeutet, dass Destruktoren für Mitgliedsdaten nicht aufgerufen werden, wenn Sie keine besondere Anstrengung unternehmen - dh es funktioniert am besten mit POD-Typen für Felder.

Eine andere Option ist die Verwendung eines Sprachvorprozessors, der Multimethoden unterstützt. Es gab einige davon, zum Teil, weil Stroustrup recht gut entwickelte Ideen zur Unterstützung von Multimethoden an einem Punkt hatte. CMM ist eins. Doublecpp ist ein anderer. Ein weiterer ist der Frost Project. Ich glaube, CMM ist dem am nächsten, was Stroustrup beschrieben hat, aber ich habe es nicht überprüft.

Letztendlich ist Multiple Dispatch jedoch nur eine Möglichkeit, eine Laufzeitentscheidung zu treffen, und es gibt viele Möglichkeiten, die gleiche Entscheidung zu treffen. Spezialisierte DSLs bringen eine Menge Ärger mit sich, also machen Sie das im Allgemeinen nur, wenn Sie vielfachen Versand benötigen. Redispatch und das Besuchermuster sind robuste WRT-Wartung, aber auf Kosten von etwas Komplexität und Unordnung. Einfache bedingte Anweisungen sind möglicherweise eine bessere Wahl für einfache Fälle, obwohl Sie sich bewusst sein sollten, dass das Erkennen der Möglichkeit eines unbehandelten Falls zur Kompilierungszeit dann schwierig, wenn nicht unmöglich ist.

Wie so oft gibt es keinen richtigen Weg, zumindest in C++.

Verwandte Themen