确保可枚举类型的 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_cast
到 int
是允许的。如果需要。
int n = static_cast<int>(icon); // Would return 0, the index of icon_t::ICON_A
我们有一个 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_cast
到 int
是允许的。如果需要。
int n = static_cast<int>(icon); // Would return 0, the index of icon_t::ICON_A