寻找滥用枚举的替代方法

Finding An Alternative To Abusing Enums

在我最近帮助的一个项目中,整个代码库依赖于一个巨大的枚举,它有效地用作美化哈希的键 Table。现在唯一的问题是它很大,每当枚举发生变化时编译基本上是对已经很大的代码库的重建。这需要永远,我真的很想更换它。

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

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

我目前的尝试基本上可以总结为延迟定义值直到 link 时间。

使用示例

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

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

实现此目的的一种方法是使用键 class 包装数字 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);
  }
}
}

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