确保可枚举类型的 key/value 从不在软件版本之间更改

Ensure key/value of enumerable type never changes between software revs

我们有一个 client/server 应用程序,其中较旧的服务器由较新的客户端支持。两者都支持一组共享图标。

我们将图标表示为包含在服务器和客户端构建中的隐式枚举:

enum icons_t { // rev 1.0
  ICON_A, // 0
  ICON_B, // 1
  ICON_C  // 2
};

有时我们会停用图标(未被使用,或内部使用且未在我们的 API 中列出),这导致提交以下代码:

enum icons_t { // rev 2.0
  ICON_B, // 0
  ICON_C  // 1 (now if a rev 1.0 server uses ICON_B, it will get ICON_C instead)
};

我已将我们的枚举更改为以下内容以尝试解决此问题:

// Big scary header about commenting out old icons
enum icons_t { // rev 2.1
  // Removed: ICON_A = 0,
  ICON_B = 1,
  ICON_C = 2
};

现在我担心的是多人添加新图标时合并不好:

// Big scary header about commenting out old icons
enum icons_t { // rev 30
  // Removed: ICON_A = 0,
  ICON_B = 1,
  ICON_C = 2,
  ICON_D = 3,
  ICON_E = 3 // Bad merge leaves 2 icons with same value
};

因为它是一个枚举,我们真的没有办法断言值是否唯一。

是否有更好的数据结构来管理这些数据,或者是否有不会出现此类错误的设计更改?如果检测到此问题,我的想法一直在转向分析拉取请求和阻止合并的工具。

我之前做过测试,检查以前的构建并扫描头文件以查找此类 version-breaking 行为。您可以使用 diff 生成任何更改的报告,grep 用于常见模式,并确定删除 fixed-index 条目、更改条目索引以及删除或插入浮动索引条目之间的区别。

避免它的一个明显方法是不删除死索引,而是重命名它们,即 ICON_A 变为 ICON_A_RETIRED,并且它的位置永远保留。但是,不可避免地会有人更改索引,因此良好的单元测试也会有所帮助。强制采用样板样式意味着测试比应对一般情况更简单。

另一个技巧可能是接受问题将会发生,但如果这只是客户的问题,并且在每个软件 release/revision 处更新范围的基数,发布软件并更新再次,因此开发版本与发布版本永远不兼容,例如

#define ICON_RANGE 0x1000
#define ICON_REVISION_BASE ((RELEASENUM+ISDEVFLAG)*ICON_RANGE)
enum icon_t {
  iconTMax = ICON_REVISION_BASE+ICON_RANGE,
  iconTBase = ICON_REVISION_BASE,
  icon_A,
  icon_B,

然后,在 run-time,任何不在当前范围内的图标很容易被拒绝,或者您可以在版本之间提供一个特殊的 look-up,这可能是通过拖网您的版本控制修订生成的。请注意,您只能通过这种方式提供向后兼容性,而不是向前兼容性。这将取决于较新的代码抢先 back-translate 将它们的图标编号发送到较旧的模块,这可能比它值得的更多努力。

我突然想到这个想法:如果我们在枚举大小的末尾保留一个文字,如果我们没有验证每个枚举文字,我们的单元测试可以使用它来断言:

enum icons_t {
  ICON_A_DEPRECATED,
  ICON_B,
  ICON_C,
  ICON_COUNT // ALWAYS KEEP THIS LAST
};

然后在测试中:

unsigned int verifyCount = 0;
verify(0, ICON_A_DEPRECATED); // verifyCount++, assert 0 was not verified before
verify(1, ICON_B); // verifyCount++, assert 1 was never verified before
assert(ICON_COUNT == verifyCount, "Not all icons verified");

那么我们唯一的问题就是在发布之前确保测试通过,这是我们无论如何都应该做的。

由于问题已被标记为 C++11,因此使用 Scoped enumerations.
可以更好地处理这个问题 在这里阅读:http://en.cppreference.com/w/cpp/language/enum

由于客户端和服务器中都包含相同的枚举文件,因此删除任何条目都会导致在使用缺失条目的地方出现编译失败。

所有需要改变的,就是你的icon_t
将它从 enum 升级到 enum class

enum class icon_t
{
    ICON_A,
    ICON_B,
};

现在你不能公然通过 int 而不是 icon_t。这大大降低了你犯错的可能性。

所以调用方

#include <iostream>

enum class icon_t
{
    ICON_A,
    ICON_B,
};

void test_icon(icon_t const & icon)
{
    if (icon == icon_t::ICON_A)
        std::cout << "icon_t::ICON_A";
    if (icon == icon_t::ICON_B)
        std::cout << "icon_t::ICON_B";
}

int main()
{
    auto icon = icon_t::ICON_A;
    test_icon(icon); // this is ok
    test_icon(1); // Fails at compile time : no known conversion from 'int' to 'const icon_t' for 1st argument 
    return 0;
}

此外,允许从 Scoped Enumerators 中提取数值。 static_castint 是允许的。如果需要。

int n = static_cast<int>(icon); // Would return 0, the index of icon_t::ICON_A