2015-01-01 13 views
6

In einem Projekt, mit dem ich in letzter Zeit geholfen habe, hängt die gesamte Code-Basis von einem monströsen Enum ab, das effektiv als Schlüssel für eine verherrlichte Hash-Tabelle verwendet wird. Das einzige Problem ist jetzt, dass es RIESIG ist, das Kompilieren, wann immer sich die Enumeration ändert, ist im Grunde eine Neuerstellung für eine bereits große Codebasis. Das dauert ewig und ich würde es wirklich gerne ersetzen.Eine Alternative zum Missbrauch von Enums finden

enum Values 
{ 
    Value = 1, 
    AnotherValue = 2, 
    <Couple Thousand Entries> 
    NumValues // Sentinel value for creating arrays of the right size 
} 

Was ich suche Möglichkeiten, um diese Enum zu ersetzen, aber immer noch ein System, das typsicher (keine ungeprüften Strings) und auch kompatibel mit MSVC2010 (kein constexpr) ist. Zusätzlicher Kompilierungs-Overhead ist akzeptabel, da es immer noch eine kürzere Zeit für die Kompilierung sein kann, als einen Haufen Dateien neu zu kompilieren.

Meine aktuellen Versuche können im Wesentlichen zusammengefasst werden als Verzögerung der Definition der Werte bis zur Verbindungszeit.

Beispiele für seine Verwendung

GetValueFromDatabase(Value); 
AddValueToDatabase(Value, 5); 
int TempArray[NumValues]; 

Edit: Compiletime und Runtime Vorverarbeitung ist akzeptabel. Zusammen mit dem Aufbau einer Caching-Datenstruktur zur Laufzeit.

+0

* "Kompilieren wann immer sich die enum ändert ist im Grunde eine Neuerstellung für eine bereits große Code-Basis" * Ich nehme an, dass Sie Unterstützung für einen automatischen Build-Prozess benötigen?Wenn Sie beispielsweise (C++) * Bezeichner * zum Benennen der Nummern verwenden, müssen diese Bezeichner vor ihrer Verwendung deklariert werden. Wenn dies in einer zentralen Header-Datei erfolgt und Sie zusätzliche Bezeichner hinzufügen müssen, erkennt das Build-Tool die Änderung und kompiliert jede Quelldatei neu, die es enthält. Wäre es möglich, eine solche Funktion für diese Header-Datei in Ihrem Build-Prozess zu deaktivieren? – dyp

Antwort

3

Eine Möglichkeit, dies erreichen kann, ist mit einem Schlüssel-Klasse, die die numerische ID hüllt und die sich nicht direkt instanziiert daher Referenzen zwingt durch eine typsichere Variable zu tun:

// key.h 

namespace keys { 

// Identifies a unique key in the database 
class Key { 
    public: 
    // The numeric ID of the key 
    virtual size_t id() const = 0; 
    // The string name of the key, useful for debugging 
    virtual const std::string& name() const = 0; 
}; 

// The total number of registered keys 
size_t count(); 

// Internal helpers. Do not use directly outside this code. 
namespace internal { 
    // Lazily allocates a new instance of a key or retrieves an existing one. 
    const Key& GetOrCreate(const std::string& name, size_t id); 
} 
} 

#define DECLARE_KEY(name) \ 
    extern const ::keys::Key& name 

#define DEFINE_KEY(name, id) \ 
    const ::keys::Key& name = ::keys::internal::GetOrCreate(STRINGIFY(name), id) 

Mit dem Code oben, würde die Definition von Tasten wie folgt aussehen:

// some_registration.h 
DECLARE_KEY(Value); 
DECLARE_KEY(AnotherValue); 
// ... 

// some_registration.cpp 
DEFINE_KEY(Value, 1); 
DEFINE_KEY(AnotherValue, 2); 
// ... 

Wichtig ist, dass über den Registrierungscode nun in mehrere separate Dateien aufgeteilt werden könnte, so dass Sie auf einmal alle Definitionen nicht neu kompiliert werden müssen. Sie könnten beispielsweise die Registrierung in logische Gruppierungen aufteilen, und wenn Sie einen neuen Eintrag hinzufügen, müsste nur die eine Teilmenge neu kompiliert werden, und nur Code, der tatsächlich von der entsprechenden * .h-Datei abhing, müsste neu kompiliert werden (Ein anderer Code, der nicht auf diesen bestimmten Schlüsselwert verweist, müsste nicht mehr aktualisiert werden).

Die Nutzung würde zu sehr ähnlich vor:

GetValueFromDatabase(Value); 
AddValueToDatabase(Value, 5); 
int* temp = new int[keys::count()]; 

Die entsprechende key.cpp Datei, dies zu erreichen würde wie folgt aussehen:

namespace keys { 
namespace { 
class KeyImpl : public Key { 
    public: 
    KeyImpl(const string& name, size_t id) : id_(id), name_(name) {} 
    ~KeyImpl() {} 
    virtual size_t id() const { return id_; } 
    virtual const std::string& name() const { return name_; } 

    private: 
    const size_t id_; 
    const std::string name_; 
}; 

class KeyList { 
    public: 
    KeyList() {} 
    ~KeyList() { 
     // This will happen only on program termination. We intentionally 
     // do not clean up "keys_" and just let this data get cleaned up 
     // when the entire process memory is deleted so that we do not 
     // cause existing references to keys to become dangling. 
    } 

    const Key& Add(const string& name, size_t id) { 
     ScopedLock lock(&mutex_); 
     if (id >= keys_.size()) { 
     keys_.resize(id + 1); 
     } 

     const Key* existing = keys_[id] 
     if (existing) { 
     if (existing->name() != name) { 
      // Potentially some sort of error handling 
      // or generation here... depending on the 
      // desired semantics, for example, below 
      // we use the Google Log library to emit 
      // a fatal error message and crash the program. 
      // This crash is expected to happen at start up. 
      LOG(FATAL) 
       << "Duplicate registration of key with ID " 
       << id << " seen while registering key named " 
       << "\"" << name << "\"; previously registered " 
       << "with name \"" << existing->name() << "\"."; 
     } 
     return *existing; 
     } 

     Key* result = new KeyImpl(name, id); 
     keys_[id] = result; 
     return *result; 
    } 

    size_t length() const { 
     ScopedLock lock(&mutex_); 
     return keys_.size(); 
    } 
    private: 
    std::vector<const Key*> keys_; 
    mutable Mutex mutex_; 
}; 

static LazyStaticPtr<KeysList> keys_list; 
} 

size_t count() { 
    return keys_list->length(); 
} 

namespace internal { 
    const Key& GetOrCreate(const std::string& name, size_t id) { 
    return keys_list->Add(name, id); 
    } 
} 
} 

Wie treffend unten in den Kommentaren erwähnt, besteht ein Nachteil Ein Ansatz, der eine dezentrale Registrierung ermöglicht, besteht darin, dass es möglich wird, in Konfliktszenarien zu gelangen, in denen der gleiche Wert mehrmals verwendet wird (der obige Beispielcode fügt einen Fehler für diesen Fall hinzu, der jedoch zur Laufzeit auftritt wäre wirklich schön, so etwas zur Kompilierzeit aufzutauchen). Einige Möglichkeiten, dies zu verringern, umfassen Commit Hooks, die Tests ausführen, die nach einer solchen Bedingung suchen, oder Richtlinien zur Auswahl des ID-Werts, die die Wahrscheinlichkeit der Wiederverwendung einer ID reduzieren, z. B. eine Datei, die die nächste verfügbare ID angibt, die inkrementiert und gesendet werden muss als eine Möglichkeit, IDs zuzuweisen. Unter der Annahme, dass es Ihnen erlaubt ist, die IDs neu zu mischen (ich nehme an, dass Sie in dieser Lösung die bereits vorhandenen IDs beibehalten müssen), können Sie den Ansatz so ändern, dass die numerische ID automatisch aus dem Namen generiert wird ein Hash des Namens) und möglicherweise andere Faktoren wie __FILE__ verwenden, um mit Kollisionen umzugehen, so dass IDs eindeutig sind.

+0

Dieser Ansatz ist vielversprechend. Ich werde ein bisschen eine Geige haben und sehen, ob es andere Ansätze gibt. – BlamKiwi

+2

Wenn Sie die Aufzählung in Teile aufteilen möchten, die in mehreren (unabhängigen) Header-Dateien definiert sind, sollten Sie sicherstellen, dass die Teile kompatibel sind. Z.B. Eine ID wird nicht zweimal für verschiedene Namen verwendet oder umgekehrt. Derzeit erzwingen Sie die Funktion "Hinzufügen" nicht. – dyp

+0

@dyp deshalb suche ich nach einer Lösung, die eine Bottom-up-Gruppierung verspricht und dennoch eine zentrale Liste haben kann. Die Integrität der Liste kann dann durch eine statische Assert gewährleistet werden. – BlamKiwi