代码之家  ›  专栏  ›  技术社区  ›  codehearts

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

  •  0
  • codehearts  · 技术社区  · 7 年前

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

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

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

    由于它是一个枚举,如果值不是唯一的,我们实际上没有办法断言它。

    是否有更好的数据结构来管理这些数据,或者有不会出现这样错误的设计更改?我的想法是开发一种工具,如果检测到这个问题,可以分析请求并阻止合并。

    3 回复  |  直到 7 年前
        1
  •  1
  •   Gem Taylor    7 年前

    我以前做过一些测试,检查以前的构建并扫描头文件以发现这种类型的版本破坏行为。您可以使用diff生成任何更改的报告,对公共模式进行grep,并确定删除固定索引项、更改索引项和删除或插入浮动索引项之间的区别。

    避免这种情况的一个明显方法是不删除死索引,而是重命名它们,即。 ICON_A 成为 ICON_A_RETIRED ,其插槽将永远保留。然而,不可避免地会有人更改索引,因此良好的单元测试也会有所帮助。强制使用样板样式意味着测试比处理一般情况更简单。

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

    #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,
    

    然后,在运行时,任何不在当前范围内的图标都很容易被拒绝,或者您可能会在版本之间提供特殊的查找,可能是通过拖网版本控制修订生成的。请注意,您只能通过这种方式提供向后兼容性,而不能提供向前兼容性。这将取决于较新的代码,以抢先的方式将其图标编号反译为发送到较旧的模块,这可能需要付出更多的努力。

        2
  •  0
  •   codehearts    7 年前

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

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

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

        3
  •  0
  •   badola    7 年前

    由于问题已标记为C++11,因此使用 作用域枚举
    请在此处阅读: http://en.cppreference.com/w/cpp/language/enum

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

    所有需要改变的是 icon_t
    从升级 enum enum class

    enum class icon_t
    {
        ICON_A,
        ICON_B,
    };
    

    现在你不能明目张胆地通过 int 而不是 图标\u 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;
    }
    

    此外,允许从作用域枚举器中提取数值。 static_cast 内景 是允许的。如果需要。

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