我该使用#define、枚举还是const?

130
在我正在处理的C++项目中,我有一个名为“flag”的值,可以有四个取值。这四个标志可以组合使用。 标志描述数据库中的记录,可能是:
  • 新记录
  • 已删除的记录
  • 修改过的记录
  • 现有记录
现在,对于每个记录,我希望保留此属性,因此我可以使用枚举:
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;
}

空间要求很重要(字节 vs 整型),但不是至关重要的。使用定义会失去类型安全,使用枚举会失去一些空间(整数)并且可能需要进行位运算时强制转换。使用const也可能会失去类型安全,因为一个随意的uint8可能会出错。

是否有其他更简洁的方法?

如果没有,你会使用什么方法以及为什么?

P.S. 其余代码都是相当干净的现代C++,没有#define,我在一些地方使用了命名空间和模板,所以它们也不是不可能的选择。


使用枚举会损失一些空间(整数)。不一定。请参见https://dev59.com/ZXRC5IYBdhLWcg3wP-dh和https://dev59.com/wXNA5IYBdhLWcg3wC5NR(以及gcc的[-fshort-enum](http://gcc.gnu.org/onlinedocs/gcc/Type-Attributes.html)。(我假设这些C的答案在C++中仍然是正确的。) - idbrii
如果您对C和C++的兼容性不确定,我发现这个链接非常有帮助,例如查看枚举类型http://david.tribble.com/text/cdiffs.htm#C99-enum-type - aka.nice
3
这是一个投票很高的旧话题,有没有不提到C++11枚举类的原因来解决这个问题呢? - Brandin
作为一条注释,enum RecordType : uint8_t结合了enum的类型安全性和uint8_t的小尺寸,尽管您仍需要提供位运算符。 - Justin Time - Reinstate Monica
15个回答

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

不,没有Qt。实际上,这是一个wxWidgets项目。 - Milan Babuškov

2

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

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

那么你的记录类可以有一个名为RecordFlag的结构体成员变量,函数可以使用struct RecordFlag类型的参数等。编译器应该将位域打包在一起以节省空间。


有时作为整体,有时作为标志。而且,我还需要测试是否设置了某个特定的标志(当我将其作为整体传递时)。 - Milan Babuškov
好的,当分开时,只需请求一个整数。当在一起时,请传递结构体。 - wnoise
它不会更好。访问位域比其他任何东西都要慢。 - paercebal
真的吗?你认为编译器在测试位域时会生成显着不同的代码,而手动位操作则不同?并且它会显着变慢吗?为什么?唯一不能轻松地以习惯用语方式做到的是同时屏蔽多个标志。 - wnoise
运行一个简单的读取测试,我得到了位掩码的5.50-5.58秒和位域访问的5.45-5.59秒。几乎无法区分。 - wnoise
显示剩余2条评论

2

针对这种值可以合并的情况,我可能不会使用枚举,通常情况下,枚举是互斥状态。

但无论使用哪种方法,为了更清晰地表明这些值是可以合并在一起的位值,请使用以下语法来表示实际值:

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

使用左移操作可以帮助表明每个值都是一个单独的比特位,这样就不太可能在以后出现错误,例如添加一个新值并将其赋值为9。


1
有足够的先例,特别是在ioctl()的常量中。虽然我更喜欢使用十六进制常量:0x01、0x02、0x04、0x08、0x10等。 - Jonathan Leffler

0

我更愿意选择

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

仅仅因为:

  1. 它更加简洁,使代码易读易维护。
  2. 它可以逻辑上分组常量。
  3. 程序员的时间更为重要,除非你的工作就是要节省那3个字节。

我可以轻松地拥有一百万个Record类的实例,所以这可能很重要。另一方面,这只是1MB和4MB之间的差异,所以也许我不应该担心。 - Milan Babuškov
@Vivek: 你考虑过整数宽度限制吗?特别是在 C++11 之前。 - user2672165

0

虽然我不喜欢过度设计,但在某些情况下,创建一个(小)类来封装这些信息可能是值得的。

如果您创建一个RecordType类,则它可能具有以下函数:

void setDeleted();

void clearDeleted();

bool isDeleted();

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

它可以验证组合(在不是所有组合都合法的情况下,例如如果“new”和“deleted”不能同时设置)。如果只使用位掩码等,则设置状态的代码需要进行验证,而类可以封装该逻辑。

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

尽管如此,如果您担心存储问题,仍然可以使该类仅具有“char”数据成员,因此仅占用少量存储空间(假设它是非虚拟的)。当然,根据硬件等,您可能会遇到对齐问题。

如果位值在cpp文件的匿名命名空间中而不是头文件中,则可以使实际位值对“外部世界”不可见。

如果发现使用枚举/#define/位掩码等的代码具有大量处理无效组合、记录日志等的“支持”代码,则封装在类中可能值得考虑。 当然,大多数情况下,简单问题最好使用简单的解决方案...


不幸的是,声明必须在.h文件中,因为它在整个项目中被使用(被5-6个类使用)。 - Milan Babuškov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接