寻找滥用枚举的替代方案

时间:2015-01-01 08:31:06

标签: c++ visual-c++ c++11 enums

在我最近一直在帮助的项目中,整个代码库依赖于一个可怕的枚举,它被有效地用作美化哈希表的键。唯一的问题是现在它是巨大的,只要枚举更改基本上是对已经很大的代码库的重建就编译。这需要永远,我真的很想替换它。

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

我正在寻找的是替换此枚举的方法,但仍然有一个类型安全的系统(没有未经检查的字符串),并且还与MSVC2010兼容(没有constexpr)。额外的编译开销是可以接受的,因为编译时可能比重新编译一堆文件的时间更短。

我目前的尝试基本上可以概括为延迟定义值直到链接时间。

使用示例

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

编辑:编译时和运行时预处理是可以接受的。同时在运行时基于某种缓存数据结构。

1 个答案:

答案 0 :(得分:3)

您可以实现此目的的一种方法是使用包装数字ID的密钥类,并且无法直接实例化,因此强制引用通过类型安全的变量完成:

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

使用上面的代码,键的定义如下所示:

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

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

重要的是,上面的注册码现在可以拆分成几个单独的文件,这样您就不需要一次重新编译所有定义。例如,您可以将注册拆分为逻辑分组,如果添加了新条目,则只需要重新编译一个子集,并且只需要重新编译实际依赖于相应* .h文件的代码(其他未引用该特定键值的代码将不再需要更新)。

使用情况与之前非常相似:

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

完成此操作的相应key.cpp文件如下所示:

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

正如下面的评论中恰当地指出的那样,允许分散注册的方法的一个缺点是,它可能会进入多次使用相同值的冲突场景(上面的示例代码为此添加了错误) case,但这发生在运行时,在编译时表达这样的东西真的很好)。缓解此问题的一些方法包括运行测试检查此类条件的提交挂钩或关于如何选择降低重用ID可能性的ID值的策略,例如指示必须递增和提交的下一个可用ID的文件作为分配ID的一种方式。或者,假设您被允许重新洗牌(我在此解决方案中假设您必须保留您已有的当前ID),您可以更改方法,以便从名称自动生成数字ID(例如,通过名称的哈希值)并可能使用其他因素(如__FILE__)来处理冲突,以便ID是唯一的。