2013-10-25 4 views
5

Lassen Sie uns sagen, dass ich eine Familie von Klassen haben, die alle die gleiche Schnittstelle implementieren, vielleicht für die Planung:Entwurfsmuster für das Caching von verschiedenen abgeleiteten Typen ohne RTTI mit

class Foo : public IScheduler { 
public: 
    Foo (Descriptor d) : IScheduler (d) {} 
    /* methods */ 
}; 

class Bar : public IScheduler { 
public: 
    Bar (Descriptor d) : IScheduler (d) {} 
    /* methods */ 
}; 

Lassen Sie uns jetzt sagen, dass ich einen Scheduler-Klasse haben, die Sie kann fragen, ob eine IScheduler-abgeleitete Klasse für einen gegebenen Deskriptor gestartet werden soll. Wenn es bereits existiert, erhalten Sie einen Hinweis darauf. Wenn einer nicht existiert, erzeugt er einen neuen.

Foo & foo = scheduler->findOrCreate<Foo>(descriptor); 

dass Implementierung würde eine Karte, deren Schlüssel erforderlich waren (Beschreiber, RTTI) zugeordnet Basisklasse Zeiger:

Eine hypothetische Aufruf wäre so etwas wie sein. Dann müssten Sie dynamic_cast. Etwas in dieser Richtung, ich denke:

template<class ItemType> 
ItemType & Scheduler::findOrCreate(Descriptor d) 
{ 
    auto it = _map.find(SchedulerKey (d, typeid(ItemType))); 
    if (it == _map.end()) { 
     ItemType * newItem = new ItemType (d); 
     _map[SchedulerKey (d, typeid(ItemType))] = newItem; 
     return *newItem; 
    } 
    ItemType * existingItem = dynamic_cast<ItemType>(it->second); 
    assert(existingItem != nullptr); 
    return *existingItem; 
} 

fragen sich, ob jemand eine Möglichkeit hat, ein ähnliches Ergebnis ohne stützte sich auf RTTI wie dies zu erreichen. Vielleicht könnte jeder geplante Elementtyp eine eigene Karteninstanz haben? Ein Designmuster oder ...?

Antwort

5

Die Adresse einer Funktion oder eines statischen Members der Klasse ist garantiert eindeutig (soweit < angezeigt wird), sodass Sie eine solche Adresse als Schlüssel verwenden können.

template <typename T> 
struct Id { static void Addressed(); }; 

template <typename ItemType> 
ItemType const& Scheduler::Get(Descriptor d) { 
    using Identifier = std::pair<Descriptor, void(*)()>; 

    Identifier const key = std::make_pair(d, &Id<ItemType>::Addressed); 

    IScheduler*& s = _map[key]; 

    if (s == nullptr) { s = new ItemType{d}; } 

    return static_cast<ItemType&>(*s); 
} 

Beachten Sie die Verwendung von operator[] ein Doppelnachschau zu vermeiden und die Funktionskörper vereinfachen.

+0

Keine allgemeine Musteränderung, aber eine interessante Vermeidung von RTTI. Denkst du, dass es in einem modernen C++ 11-Compiler einen großen Vorteil hat, so zu arbeiten? – HostileFork

+0

@HostileFork: Ich denke nicht, dass es viel bringt im Vergleich zur Verwendung von 'typeinfo' ... es sei denn, Sie wollen ohne RTTI-Unterstützung kompilieren :) –

+0

Das wird aufhören zu arbeiten, wenn man z. Windows DLLs. Ich weiß, dass der C++ - Standard immer noch garantiert, dass die Adresse eindeutig ist, aber ich kenne keinen Compiler, der dieses Verhalten tatsächlich mit Windows DLLs implementiert. Die Verwendung von typeid funktioniert jedoch immer noch. –

2

Hier ist ein Weg.

eine rein virtuelle Methode zu IScheduler hinzufügen:

virtual const char *getId() const =0; 

Dann auf seine eigene .h oder CPP Datei jeder Unterklasse setzen, und definieren Sie die Funktion:

virtual const char *getId() const { return __FILE__; } 

Zusätzlich zur Verwendung von Vorlagen, wo Sie den genauen Typ zur Kompilierzeit haben, im gleichen Fil e definieren statische Methode, die Sie ohne Klasseninstanz (AKA static polymorphism) verwenden können:

static const char *staticId() { return __FILE__; } 

Dann verwenden Sie diese als Cache-Karte Schlüssel. __FILE__ ist im C++ - Standard, also ist dies auch portabel.

Wichtiger Hinweis: Verwenden Sie den richtigen Zeichenfolge vergleichen, anstatt nur Zeiger zu vergleichen. Vielleicht std::string statt char* zurückgeben, um Unfälle zu vermeiden. Auf der positiven Seite können Sie dann mit beliebigen String-Werten vergleichen, speichern Sie sie in Datei usw., Sie müssen nicht nur Werte verwenden, die von diesen Methoden zurückgegeben werden.

Wenn Sie Zeiger vergleichen möchten (wie für Effizienz), benötigen Sie ein bisschen mehr Code, den Sie pro Klasse genau ein Zeigerwert, um sicherzustellen, haben (variable Deklaration private static Mitglied in hinzufügen .h und Definition + Initialisierung mit FILE in entsprechenden .cpp, und geben Sie dann zurück, und verwenden Sie nur die von diesen Methoden zurückgegebenen Werte.


Hinweis über Klassenhierarchie, wenn Sie etwas haben, wie

  • A erbt IScheduler muss getId()
  • A2 erbt A, Compiler beschweren getId()

über das Vergessen nicht außer Kraft setzen Dann, wenn Sie sicherstellen wollen, dass Sie nicht leiden ntally vergessen getId() außer Kraft zu setzen, können Sie stattdessen

  • abstrakt sollte AbaseIScheduler erbt, ohne zu definieren getId()
  • letzte AAbase erbt und muss getId()
  • final A2 erbt Abase, hinzufügen und getId() hinzufügen müssen, in Neben den Änderungen an A

(Hinweis: final keyword identifier with special meaning ist C 11 ++ Funktion, die für frühere Versionen es nur auszulassen ...)

+0

Es vermeidet die Typid ... und ich habe klapprige Dinge dieser Art (sogar bis zu dem Punkt Hashing einen Dateinamen und Zeilennummer zusammen und in der Hoffnung, dass es einzigartig ist: - /) getan. Aber es scheint [@ MatthieuM.'s Technik] (http://stackoverflow.com/a/19598439/211160) ist eine stabilere Möglichkeit, das gleiche Ziel zu erreichen, und diktiert die Dateistruktur nicht? – HostileFork

+0

Ja, es scheint fast so zu sein wie die Version, die ich mit einer statischen privaten Mitgliedsvariablen beschreibe, und wahrscheinlich eine bessere Lösung für diesen Fall. Meine String-Lösung ist besser, wenn Sie die Klassen-ID für viele Zwecke verwenden müssen, wie zum Beispiel in eine Datei speichern oder eine andere Persistenz haben. – hyde

1

static_cast die ItemType* zu void* und speichern, die in der Karte. Dann, in findOrCreate, erhalten Sie einfach die void* und static_cast es zurück zu ItemType*.

static_cast ing T * -> void * -> T * ist garantiert, dass Sie wieder den ursprünglichen Zeiger zu erhalten. Sie verwenden bereits typeid(ItemType) als Teil Ihres Schlüssels, sodass sichergestellt ist, dass die Suche nur dann erfolgreich ist, wenn derselbe Typ angefordert wird. Das sollte also sicher sein.

Wenn Sie auch die IScheduler* in der Scheduler Map benötigen, speichern Sie einfach beide Zeiger.

+0

Eigentlich ist die Frage über ** vermeiden ** 'typeid' ... –

+0

Casting zu void ist nicht sehr C++ ish ... Ich bin tatsächlich auf der Suche nach einem Muster in C++ zu" sauberer "als was ich überlege. Aber funktioniert auch ein 'static_cast' nicht von einem IScheduler *, wenn Sie sicher sind, dass das Objekt vom Zieltyp ist? Ich dachte, was * würde nicht funktionieren würde "IScheduler * ptr = &foo; Foo * fooPtr = (Foo *) ((void *) ptr);' – HostileFork

+0

Ja, Casting von 'IScheduler' würde in Ihrem Beispiel funktionieren. Der Punkt ist, dass Sie keinen 'dynamic_cast' brauchen. Und Casting zum Stornieren von IMO ist sehr viel "C++ ish". Ich würde es auch nicht als "unrein" bezeichnen. Nur das, wie Sie richtig angemerkt haben, ist es in Ihrem Fall unnötig. –

2

Wenn Scheduler ein Singleton ist, würde dies funktionieren.

template<typename T> 
T& Scheduler::findOrCreate(Descriptor d) { 
    static map<Descriptor, unique_ptr<T>> m; 
    auto& p = m[d]; 
    if (!p) p = make_unique<T>(d); 
    return *p; 
} 

Wenn Scheduler kein Singleton ist, könnte man eine zentrale Registrierung haben die gleiche Technik verwenden, aber einen Scheduler */Descriptor Paar zum unique_ptr zuordnen.

+0

Obwohl ziemlich clever, unterliegt dies Datenrennen in Multi-Thread-Programmen. Dies unterliegt auch dem Syndrom des ständig wachsenden Caches. –

+1

Alle diese Antworten (und der ursprüngliche Code in der Frage) unterliegen Datenrennen in Multithread-Programmen. Dies könnte leicht synchronisiert werden, wenn dies erforderlich wäre. – mattnewport

+0

+1 für die Richtung, die ich suchte, obwohl der Scheduler Zugriff benötigt, um alle Elemente zu durchlaufen ... und es wäre nicht unbedingt ein Singleton. : -/Ich frage mich, was der Kompromiss ist? Ich bin mir nicht sicher, was @MatthieuM. bezieht sich auf, aber ohne zu abgelenkt zu werden, erinnere ich mich an einige Dinge, über die ich mich mit statischen Mitgliedern Gedanken machen muss (http://stackoverflow.com/questions/9507973/). – HostileFork

1

Wenn Sie alle Ihre verschiedenen Subtypen von IsScheduler kennen, dann ja absolut. In Boost.Fusion können Sie eine Karte erstellen, deren Schlüssel wirklich ein Typ ist. So für Ihr Beispiel, könnten wir so etwas wie tun:

typedef boost::fusion::map< 
    boost::fusion::pair<Foo, std::map<Descriptor, Foo*>>, 
    boost::fusion::pair<Bar, std::map<Descriptor, Bar*>>, 
    .... 
    > FullMap; 

FullMap map_; 

Und wir werden diese Karte verwenden thuslly:

template <class ItemType> 
ItemType& Scheduler::findOrCreate(Descriptor d) 
{ 
    // first, we get the map based on ItemType 
    std::map<Descriptor, ItemType*>& itemMap = boost::fusion::at_key<ItemType>(map_); 

    // then, we look it up in there as normal 
    ItemType*& item = itemMap[d]; 
    if (!item) item = new ItemType(d); 
    return item; 
} 

Wenn Sie versuchen, ein Element findOrCreate, die Sie nicht in Ihrem FullMap definiert haben, dann kann at_key nicht kompilieren. Wenn Sie also etwas wirklich Dynamisches brauchen, wo Sie neue Scheduler ad hoc hinzufügen können, wird dies nicht funktionieren. Aber wenn das keine Voraussetzung ist, funktioniert das großartig.

+0

Verpasste diese. Interessant, hatte noch nicht von boost :: fusion gehört. Vielen Dank. In meinem Fall kenne ich jedoch nicht alle verschiedenen IsScheduler-Typen, also habe ich mich auf die typefinfo-Lösung verlegt. – HostileFork

Verwandte Themen