126

在我正在处理的 C++ 项目中,我有一个标志类型的值,它可以有四个值。这四个标志可以组合。标志描述数据库中的记录,可以是:

  • 新纪录
  • 删除记录
  • 修改记录
  • 现有记录

现在,对于我希望保留此属性的每条记录,我可以使用枚举:

enum { xNew, xDeleted, xModified, xExisting }

但是,在代码的其他地方,我需要选择哪些记录对用户可见,所以我希望能够将其作为单个参数传递,例如:

showRecords(xNew | xDeleted);

所以,似乎我有三种可能的方法:

#define X_NEW      0x01
#define X_DELETED  0x02
#define X_MODIFIED 0x04
#define X_EXISTING 0x08

或者

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

或者

namespace RecordType {
    static const uint8 xNew = 1;
    static const uint8 xDeleted = 2;
    static const uint8 xModified = 4;
    static const uint8 xExisting = 8;
}

空间要求很重要(字节与整数),但并不重要。使用定义我失去了类型安全性,并且enum我失去了一些空间(整数)并且当我想要进行按位运算时可能不得不强制转换。我const认为我也失去了类型安全性,因为随机uint8可能会错误地进入。

还有其他更清洁的方法吗?

如果没有,你会用什么,为什么?

PS 其余的代码是相当干净的没有#defines 的现代 C++,而且我在少数空间中使用了命名空间和模板,所以这些也不是没有问题的。

4

15 回答 15

90

结合策略以减少单一方法的缺点。我在嵌入式系统中工作,因此以下解决方案基于整数和按位运算符速度快、内存低且闪存使用率低的事实。

将枚举放在命名空间中,以防止常量污染全局命名空间。

namespace RecordType {

枚举声明并定义了一个编译时检查类型。始终使用编译时类型检查来确保参数和变量的类型正确。C++ 中不需要 typedef。

enum TRecordType { xNew = 1, xDeleted = 2, xModified = 4, xExisting = 8,

为无效状态创建另一个成员。这可以用作错误代码;例如,当您想要返回状态但 I/O 操作失败时。它对调试也很有用;在初始化列表和析构函数中使用它来了解是否应该使用变量的值。

xInvalid = 16 };

考虑到这种类型有两个目的。跟踪记录的当前状态并创建掩码以选择处于某些状态的记录。创建一个内联函数来测试该类型的值是否对您的目的有效;作为状态标记与状态掩码。这将捕获错误,因为typedef它只是一个int和一个值,例如0xDEADBEEF可能通过未初始化或错误指向的变量在您的变量中。

inline bool IsValidState( TRecordType v) {
    switch(v) { case xNew: case xDeleted: case xModified: case xExisting: return true; }
    return false;
}

 inline bool IsValidMask( TRecordType v) {
    return v >= xNew  && v < xInvalid ;
}

using如果您想经常使用该类型,请添加指令。

using RecordType ::TRecordType ;

值检查函数在断言中很有用,可以在使用错误值时立即捕获它们。跑的时候越快发现虫子,它造成的伤害就越小。

这里有一些例子可以把它们放在一起。

void showRecords(TRecordType mask) {
    assert(RecordType::IsValidMask(mask));
    // do stuff;
}

void wombleRecord(TRecord rec, TRecordType state) {
    assert(RecordType::IsValidState(state));
    if (RecordType ::xNew) {
    // ...
} in runtime

TRecordType updateRecord(TRecord rec, TRecordType newstate) {
    assert(RecordType::IsValidState(newstate));
    //...
    if (! access_was_successful) return RecordType ::xInvalid;
    return newstate;
}

确保正确值安全的唯一方法是使用带有运算符重载的专用类,这留给其他读者作为练习。

于 2008-09-22T07:09:14.143 回答
57

忘记定义

他们会污染你的代码。

位域?

struct RecordFlag {
    unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};

永远不要使用它。您更关心速度而不是节省 4 个整数。使用位域实际上比访问任何其他类型都慢。

然而,结构中的位成员有实际的缺点。首先,内存中位的顺序因编译器而异。此外,许多流行的编译器生成读取和写入位成员的代码效率低下,并且由于大多数机器无法操作内存中的任意位集,因此可能存在与位域相关的严重线程安全问题(尤其是在多处理器系统上),但必须改为加载和存储整个单词。例如,尽管使用了互斥锁,但以下内容不是线程安全的

来源:http ://en.wikipedia.org/wiki/Bit_field :

如果您需要更多使用位域的理由,也许Raymond Chen会在他的 The Old New Thing帖子中说服您: http://blogs.msdn.com/oldnewthing/上的布尔集合的位域的成本效益分析存档/2008/11/26/9143050.aspx

常量整数?

namespace RecordType {
    static const uint8 xNew = 1;
    static const uint8 xDeleted = 2;
    static const uint8 xModified = 4;
    static const uint8 xExisting = 8;
}

将它们放在命名空间中很酷。如果它们在您的 CPP 或头文件中声明,它们的值将被内联。您将能够在这些值上使用 switch,但它会稍微增加耦合。

啊,是的:删除 static 关键字。在 C++ 中不推荐使用 static ,如果 uint8 是内置类型,则不需要在同一模块的多个源包含的标头中声明它。最后,代码应该是:

namespace RecordType {
    const uint8 xNew = 1;
    const uint8 xDeleted = 2;
    const uint8 xModified = 4;
    const uint8 xExisting = 8;
}

这种方法的问题是您的代码知道常量的值,这会稍微增加耦合。

枚举

与 const int 相同,具有更强的类型。

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

但是,它们仍在污染全局命名空间。顺便说一句...删除 typedef。您正在使用 C++ 工作。枚举和结构的那些 typedef 对代码的污染比其他任何东西都多。

结果有点:

enum RecordType { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } ;

void doSomething(RecordType p_eMyEnum)
{
   if(p_eMyEnum == xNew)
   {
       // etc.
   }
}

如您所见,您的枚举正在污染全局命名空间。如果你把这个枚举放在一个命名空间中,你会得到类似的东西:

namespace RecordType {
   enum Value { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } ;
}

void doSomething(RecordType::Value p_eMyEnum)
{
   if(p_eMyEnum == RecordType::xNew)
   {
       // etc.
   }
}

外部常量 int ?

如果您想减少耦合(即能够隐藏常量的值,因此可以根据需要修改它们而无需完全重新编译),您可以在头文件中将 int 声明为 extern,并在 CPP 文件中声明为常量,如下例所示:

// Header.hpp
namespace RecordType {
    extern const uint8 xNew ;
    extern const uint8 xDeleted ;
    extern const uint8 xModified ;
    extern const uint8 xExisting ;
}

和:

// Source.hpp
namespace RecordType {
    const uint8 xNew = 1;
    const uint8 xDeleted = 2;
    const uint8 xModified = 4;
    const uint8 xExisting = 8;
}

但是,您将无法在这些常量上使用 switch。所以最后,选择你的毒药...... :-p

于 2008-09-21T23:52:53.160 回答
30

你排除了 std::bitset 吗?标志集就是它的用途。做

typedef std::bitset<4> RecordType;

然后

static const RecordType xNew(1);
static const RecordType xDeleted(2);
static const RecordType xModified(4);
static const RecordType xExisting(8);

因为 bitset 有一堆运算符重载,你现在可以做

RecordType rt = whatever;      // unsigned long or RecordType expression
rt |= xNew;                    // set 
rt &= ~xDeleted;               // clear 
if ((rt & xModified) != 0) ... // test

或与此非常相似的东西-由于我尚未对此进行测试,因此我将不胜感激。您也可以按索引引用位,但通常最好只定义一组常量,而 RecordType 常量可能更有用。

假设您已经排除了 bitset,我投票支持enum

我不认为强制枚举是一个严重的缺点 - 好吧,所以它有点吵,并且为枚举分配一个超出范围的值是未定义的行为,所以理论上有可能在一些不寻常的 C++ 上开枪打死自己实施。但是,如果您只在必要时(即从 int 到 enum iirc 时)这样做,那么这是人们以前见过的完全正常的代码。

我也怀疑枚举的任何空间成本。uint8 变量和参数使用的堆栈可能不会比 int 少,因此只有类中的存储很重要。在某些情况下,在结构中打包多个字节会胜出(在这种情况下,您可以在 uint8 存储中输入和输出枚举),但通常填充无论如何都会扼杀好处。

因此,与其他枚举相比,枚举没有任何缺点,并且作为一个优势,可以为您提供一点类型安全性(如果没有显式转换,您无法分配一些随机整数值)和引用所有内容的简洁方式。

顺便说一句,为了偏好,我还将“= 2”放在枚举中。这不是必需的,但“最小惊讶原则”表明所有 4 个定义应该看起来相同。

于 2008-09-21T23:06:16.347 回答
8

以下是关于 const 与宏与枚举的几篇文章:

符号常量
枚举常量与常量对象

我认为您应该避免使用宏,尤其是因为您编写的大部分新代码都是用现代 C++ 编写的。

于 2008-09-21T23:01:03.487 回答
5

如果可能,不要使用宏。当谈到现代 C++ 时,他们并没有受到太多的钦佩。

于 2008-09-22T06:30:24.000 回答
4

枚举会更合适,因为它们提供“标识符的含义”以及类型安全。您可以清楚地看出“xDeleted”属于“RecordType”并且即使在多年之后也代表“记录类型”(哇!)。Consts 需要对此进行注释,他们也需要在代码中上下移动。

于 2008-09-21T22:57:33.230 回答
4

有了定义,我失去了类型安全

不必要...

// signed defines
#define X_NEW      0x01u
#define X_NEW      (unsigned(0x01))  // if you find this more readable...

和枚举我失去了一些空间(整数)

不一定-但您必须在存储点上明确...

struct X
{
    RecordType recordType : 4;  // use exactly 4 bits...
    RecordType recordType2 : 4;  // use another 4 bits, typically in the same byte
    // of course, the overall record size may still be padded...
};

当我想做按位运算时可能必须强制转换。

您可以创建运算符来减轻痛苦:

RecordType operator|(RecordType lhs, RecordType rhs)
{
    return RecordType((unsigned)lhs | (unsigned)rhs);
}

使用 const 我认为我也会失去类型安全性,因为随机 uint8 可能会错误地进入。

这些机制中的任何一种都可能发生同样的情况:范围和值检查通常与类型安全正交(尽管用户定义的类型 - 即您自己的类 - 可以对其数据强制执行“不变量”)。使用枚举,编译器可以自由选择更大的类型来托管值,并且未初始化、损坏或只是未设置的枚举变量最终仍可能将其位模式解释为您不期望的数字 - 比较不等于任何枚举标识符、它们的任意组合和 0。

还有其他更清洁的方法吗?/ 如果没有,你会用什么,为什么?

好吧,最终,一旦您在图片中拥有位字段和自定义运算符,经过验证的 C 风格的枚举按位 OR 就可以很好地工作。您可以使用一些自定义验证函数和断言进一步提高您的稳健性,如 mat_geek 的回答;技术通常同样适用于处理字符串、整数、双精度值等。

你可以说这是“更干净”:

enum RecordType { New, Deleted, Modified, Existing };

showRecords([](RecordType r) { return r == New || r == Deleted; });

我无所谓:数据位打包得更紧,但代码显着增长......取决于你有多少对象,而 lamdbas - 尽管它们很漂亮 - 仍然比按位 OR 更混乱,更难正确处理。

顺便说一句/-关于线程安全相当弱的恕我直言的论点-最好作为背景考虑而不是成为主导的决策驱动力而被记住;即使不知道它们的打包,在位域之间共享互斥锁也是一种更有可能的做法(互斥锁是相对庞大的数据成员 - 我必须非常关心性能才能考虑在一个对象的成员上拥有多个互斥锁,我会仔细查看足以注意到它们是位字段)。任何子字长类型都可能有相同的问题(例如 a uint8_t)。无论如何,如果您迫切需要更高的并发性,您可以尝试原子比较和交换样式操作。

于 2012-08-07T13:24:37.303 回答
3

即使你必须使用 4 字节来存储一个枚举(我对 C++ 不太熟悉——我知道你可以在 C# 中指定底层类型),它仍然值得——使用枚举。

在当今拥有 GB 内存的服务器时代,应用程序级别的 4 字节与 1 字节内存之类的东西通常无关紧要。当然,如果在您的特定情况下,内存使用非常重要(并且您不能让 C++ 使用字节来支持枚举),那么您可以考虑“静态 const”路线。

归根结底,您必须问自己,使用“静态常量”为您的数据结构节省 3 个字节的内存是否值得维护?

还有一点要记住——IIRC,在 x86 上,数据结构是 4 字节对齐的,所以除非你的“记录”结构中有许多字节宽度的元素,否则它实际上可能并不重要。在权衡性能/空间的可维护性之前进行测试并确保它确实有效。

于 2008-09-21T22:54:24.767 回答
3

如果您想要类的类型安全,以及枚举语法和位检查的便利,请考虑C++ 中的安全标签。我和作者合作过,他很聪明。

不过要小心。最后,这个包使用了模板宏!

于 2008-09-22T02:05:27.383 回答
2

您是否真的需要将标志值作为一个概念整体传递,或者您是否会有很多每个标志的代码?无论哪种方式,我认为将其作为 1 位位域的类或结构实际上可能更清楚:

struct RecordFlag {
    unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};

然后你的记录类可以有一个结构记录标志成员变量,函数可以接受结构记录标志类型的参数,等等。编译器应该将位域打包在一起,节省空间。

于 2008-09-21T23:15:55.620 回答
2

对于可以将值组合在一起的这种事情,我可能不会使用枚举,更典型的是枚举是互斥状态。

但无论您使用哪种方法,为了更清楚地表明这些值是可以组合在一起的位,请使用以下语法代替实际值:

#define X_NEW      (1 << 0)
#define X_DELETED  (1 << 1)
#define X_MODIFIED (1 << 2)
#define X_EXISTING (1 << 3)

在那里使用左移有助于表明每个值都是一个位,以后有人做错事的可能性较小,比如添加一个新值并将其分配为 9。

于 2008-09-22T02:03:37.997 回答
2

基于KISS高内聚低耦合,问这些问题——

  • 谁需要知道?我的班级,我的图书馆,其他班级,其他图书馆,第 3 方
  • 我需要提供什么级别的抽象?消费者是否了解位操作。
  • 我必须从 VB/C# 等接口吗?

有一本很棒的书“ Large-Scale C++ Software Design ”,它在外部促进了基本类型,如果你可以避免另一个头文件/接口依赖,你应该尝试。

于 2008-09-22T05:58:03.517 回答
2

如果您使用的是 Qt,则应该查看QFlags。QFlags 类提供了一种类型安全的方式来存储枚举值的 OR 组合。

于 2008-09-22T06:26:03.450 回答
0

我宁愿和

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

仅仅是因为:

  1. 它更干净,它使代码可读和可维护。
  2. 它在逻辑上对常量进行分组。
  3. 程序员的时间更重要,除非你的工作保存这 3 个字节。
于 2008-09-21T23:27:00.150 回答
0

并不是说我喜欢过度设计一切,但有时在这些情况下,可能值得创建一个(小)类来封装这些信息。如果您创建一个 RecordType 类,那么它可能具有以下功能:

无效 setDeleted();

无效清除删除();

布尔 isDeleted();

等等......(或任何适合的公约)

它可以验证组合(在并非所有组合都合法的情况下,例如,如果不能同时设置“新”和“已删除”)。如果您只是使用位掩码等,那么设置状态的代码需要验证,一个类也可以封装该逻辑。

该类还可以使您能够将有意义的日志记录信息附加到每个状态,您可以添加一个函数来返回当前状态的字符串表示等(或使用流操作符'<<')。

尽管如此,如果你担心存储,你仍然可以让这个类只有一个“char”数据成员,所以只需要少量的存储(假设它是非虚拟的)。当然,根据硬件等,您可能会遇到对齐问题。

如果实际位值位于 cpp 文件内的匿名命名空间中,而不是在头文件中,则它们可能对“世界”的其他部分不可见。

如果您发现使用 enum/#define/ 位掩码等的代码有很多“支持”代码来处理无效组合、日志记录等,那么在类中封装可能值得考虑。当然,大多数时候简单的问题最好用简单的解决方案来解决......

于 2008-09-22T12:41:50.300 回答