我应该使用#define,枚举或常量?

在我正在进行的C ++项目中,我有一个标志types的值,可以有四个值。 这四个标志可以合并。 标志描述数据库中的logging,可以是:

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

现在,对于每个logging我希望保持这个属性,所以我可以使用枚举:

enum { xNew, xDeleted, xModified, xExisting } 

但是,在代码中的其他地方,我需要select哪些logging对用户可见,所以我希望能够将其作为单个parameter passing,如:

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

空间要求是重要的(字节与整数),但并不重要。 随着定义我失去了types的安全性,并与enum我失去了一些空间(整数),并可能不得不施放时,我想做一个按位操作。 与const我认为我也失去了types安全,因为随机uint8可能uint8

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

如果不是,你会用什么,为什么?

PS其余的代码是相当干净的现代C ++没有#define s,我已经在很less的空间使用了命名空间和模板,所以这些都不是问题。

结合策略来减less单一方法的缺点。 我在embedded式系统中工作,所以下面的解决scheme是基于这样一个事实,整数和位运算符是快速,低内存和低使用闪存。

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

 namespace RecordType { 

一个枚举声明并定义一个编译时间检查types。 总是使用编译时types检查来确保参数和variables被赋予正确的types。 在C ++中不需要typedef。

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

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

 xInvalid = 16 }; 

考虑一下你有两个这种types的用途。 跟踪logging的当前状态并创build一个掩码来select某些状态下的logging。 创build一个内联函数来testingtypes的值是否适合您的目的; 作为状态标记与状态掩码。 这将会捕获错误,因为typedef只是一个int而像0xDEADBEEF这样的值可能会通过未初始化或错误指定的variables存在于variables中。

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

如果要经常使用该types,请添加using指令。

 using RecordType ::TRecordType ; 

值检查function在断言使用后尽快捕获错误值时非常有用。 跑步时发现错误的速度越快,造成的伤害就越小。

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

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

确保正确的价值安全的唯一方法是使用一个带有操作符重载的专用类,这是另一个读者的练习。

忘记定义

他们会污染你的代码。

位域?

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

永远不要使用它 。 与节约4个整数相比,你更关心速度。 使用位字段实际上比访问任何其他types要慢。

但是,结构中的位成员有实际的缺点。 首先,内存中位的顺序因编译器而异。 另外, 许多stream行的编译器生成用于读和写位成员的低效率代码 ,并且由于大多数机器不能操纵存储器中的任意位集,所以与位字段(特别是在多处理器系统中)有潜在的严重的线程安全问题 ,但是必须加载并存储整个单词。 例如,尽pipe使用了互斥锁,但以下内容仍然不是线程安全的

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

如果您需要更多的理由使用位域,也许Raymond Chen会在他的“新旧事物 ”中说服您:在http://blogs.msdn.com/oldnewthing/上; 为布尔集合进行位域的成本效益分析。 存档/ 2008/11月26日/ 9143050.aspx

const int?

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

把它们放在一个名字空间是很酷的。 如果它们在CPP或头文件中声明,则它们的值将被内联。 您将可以使用这些值切换,但会稍微增加耦合。

啊,是的: 删除静态关键字 。 静态在C ++中被弃用,如果uint8是一个构buildtypes,则不需要在同一个模块的多个源所包含的头中声明这一点。 最后,代码应该是:

 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; 

尽pipe如此,它们仍在污染着全球的命名空间。 顺便说一句… 删除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. } } 

extern const int?

如果你想减less耦合(即能够隐藏常量的值,所以根据需要修改它们而不需要完全的重新编译),你可以在头文件中将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; } 

不过,您将无法使用这些常量的开关。 所以最后select你的毒药… :-p

你排除了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 

或者类似的东西 – 我会很感激任何更正,因为我没有testing过这个。 您也可以通过索引引用这些位,但通常最好只定义一组常量,而RecordType常量可能更有用。

假设你已经排除了bitset,我投票给枚举

我不买这个枚举是一个严重的缺点 – 好,所以有点吵,给枚举分配一个超出范围的值是未定义的行为,所以理论上可以在脚上打一些不寻常的C ++实现。 但是如果你只在必要的时候(从int到enum iirc的时候)这样做,这是人们以前见过的完全正常的代码。

我也怀疑枚举的空间成本。 uint8variables和参数可能不会使用比ints更less的堆栈,所以只有类中的存储很重要。 在某些情况下,在一个结构体中打包多个字节将会获胜(在这种情况下,您可以将枚举投入和退出uint8存储),但是通常情况下,填充将无论如何都会损害好处。

所以这个枚举与其他types相比没有什么不利之处,并且作为一个优点,给了你一些types安全(你不能指定一些随机的整数值而不显式投射)和干净的方式来引用每一件事情。

首选我也会把“= 2”在枚举,顺便说一句。 这不是必要的,但是一个“最less惊讶的原则”表明,所有4个定义应该看起来是一样的。

这里有一些关于const与macros与enums的文章:

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

我认为你应该避免使用macros,特别是你写的大部分新代码都是现代C ++。

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

枚举会更合适,因为它们提供“标识符的含义”以及types安全。 你可以清楚的告诉“xDeleted”是“RecordType”,即使经过了几年,它也代表了“logging的types”(哇!)。 Consts将需要对此的评论,他们也需要在代码中上下。

与定义我失去型安全

不必要…

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

当我想按位操作时可能必须投。

您可以创build操作员来解决这个问题:

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

与const我认为我也失去了types安全,因为随机uint8可能误入歧途。

这些机制也可能发生同样的情况:范围和值检查通常与types安全是正交的(尽pipe用户定义的types – 也就是你自己的类)可以对他们的数据执行“不variables”)。 使用枚举,编译器可以自由select一个更大的types来托pipe这些值,而未初始化,损坏或只是未命中的枚举variables仍然可能最终将其位模式解释为一个您不会期望的数字 – 将不等于枚举标识符,它们的任意组合,以及0。

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

那么,一旦你在图片中有位字段和自定义运算符,那么经过validation和信任的C风格的枚举按位或运算相当不错。 你可以通过一些自定义的validation函数和断言来进一步提高你的健壮性,就像在mat_geek的答案中一样。 技术往往同样适用于处理string,整数,双值等。

你可以争辩说这是“更清洁”的:

 enum RecordType { New, Deleted, Modified, Existing }; showRecords([](RecordType r) { return r == New || r == Deleted; }); 

我对此漠不关心:数据位封装得更紧密,但是代码却显着增长……取决于你得到的对象数量,以及lamdbas(尽pipe它们很漂亮)仍然更加混乱,难以得到正确的比例。

顺便说一下,关于线程安全性相当弱的争论,恕我直言 – 最好记住作为背景考虑,而不是成为一个主要的决定驱动力; 即使不知道它们的包装(互斥体是相对庞大的数据成员 – 我必须真正关心性能,考虑在一个对象的成员上有多个互斥体),我会仔细看看足以注意到他们是位域)。 任何子字符types都可能有相同的问题(例如uint8_t )。 无论如何,如果你渴望更高的并发性,你可以尝试primefaces比较和交换风格的操作。

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

在使用GB内存的服务器的这个时代,一般情况下,应用程序级别的内存与4字节的内存对比1字节无关紧要。 当然,如果在你的特定情况下,内存使用是非常重要的(你不能让C ++使用一个字节来支持枚举),那么你可以考虑'静态常量'路由。

在一天结束的时候,你不得不问自己,为你的数据结构节省3个字节的内存是否值得使用“静态常量”的维护命中?

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

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

小心,但是。 最后,这个包使用模板macros!

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

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

然后你的logging类可以有一个结构RecordFlag成员variables,函数可以采取typesstruct RecordFlag等的参数。编译器应该打包位字节,节省空间。

我可能不会使用枚举来将这些值组合在一起,更典型的是枚举是相互排斥的状态。

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

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

使用左移有助于表明每个值都是一个单一的位,不太可能稍后有人会做一些错误,如添加一个新的值,并赋值为9。

基于KISS , 高内聚和低耦合 ,请问这些问题 –

  • 谁需要知道? 我的课程,我的图书馆,其他课程,其他图书馆,第三方
  • 我需要提供什么级别的抽象? 消费者是否了解位操作?
  • 我将不得不从VB / C#等接口?

有一本好书“ 大规模的C ++软件devise ”,如果你可以避免另一个头文件/接口的依赖,你应该尝试从外部推广基types。

如果你正在使用Qt,你应该看看QFlags 。 QFlags类提供了一种types安全的方式来存储枚举值的OR组合。

我宁愿去

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

只是因为:

  1. 它更清洁,使代码易读和可维护。
  2. 它在逻辑上将常量分组。
  3. 程序员的时间更重要,除非你的工作保存这3个字节。

不是我喜欢过度devise一切,但有时在这些情况下,可能需要创build一个(小)类来封装这些信息。 如果你创build一个RecordType类,那么它可能有如下function:

void setDeleted();

void clearDeleted();

bool isDeleted();

等等…(或任何约定适合)

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

这个类还可以给你赋予每个状态有意义的日志logging信息的能力,你可以添加一个函数来返回当前状态的string表示forms(或者使用stream操作符'<<')。

尽pipe如此,如果你担心存储问题,你仍然可以让这个类只有一个'char'数据成员,所以只需要less量的存储(假设它是非虚拟的)。 当然,根据硬件等你可能有alignment问题。

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

如果你发现使用enum / #define / bitmask等代码有很多“支持”代码来处理无效组合,日志等,那么封装在一个类可能是值得考虑的。 当然,大多数情况下,简单的问题最好用简单的解决scheme。