我应该使用#define,enum还是const?

时间:2008-09-21 22:49:05

标签: c++ enums bit-manipulation c-preprocessor

在我正在处理的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可能会错误地进入。

还有其他清洁方式吗?

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

P.S。其余的代码是相当简洁的现代C ++而没有#define,我在很少的空间中使用了名称空间和模板,所以这些也不是问题。

15 个答案:

答案 0 :(得分:85)

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

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

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

确保正确价值安全的唯一方法是使用具有操作员重载的专用类,并留作另一位读者的练习。

答案 1 :(得分:53)

忘记定义

他们会污染你的代码。

位域?

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/archive/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 ++中不推荐使用static,如果uint8是buildin类型,则不需要在同一模块的多个源包含的头中声明这一点。最后,代码应为:

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

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

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

答案 2 :(得分: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,我投票支持枚举

我不买那个演员的枚举是一个严重的劣势 - 好吧所以它有点吵,并且为枚举指定一个超出范围的值是未定义的行为所以理论上可以拍摄自己的脚一些不寻常的C ++实现。但是如果你只在必要时(从int到enum iirc)这样做的话,这是人们以前见过的完全正常的代码。

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

因此枚举与其他枚举相比没有任何缺点,并且作为一个优点为您提供了一些类型安全性(您无法在不显式转换的情况下分配一些随机整数值)以及干净的方式来引用所有内容。

顺便提一下,我也会在枚举中加上“= 2”。这没有必要,但“最不惊讶的原则”表明所有4个定义应该看起来都一样。

答案 3 :(得分:8)

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

Symbolic Constants
Enumeration Constants vs. Constant Objects

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

答案 4 :(得分:5)

如果可能,请勿使用宏。在现代C ++方面,它们并不太受人尊敬。

答案 5 :(得分:4)

枚举更合适,因为它们提供“标识符的含义”以及类型安全性。你可以清楚地告诉“xDeleted”是“RecordType”并且代表“记录类型”(哇!)甚至在几年之后。 Consts需要对此进行评论,也需要在代码中上下。

答案 6 :(得分: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的答案中的一些自定义验证函数和断言来进一步提高您的稳健性;技术通常同样适用于处理string,int,double值等。

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

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

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

我无动于衷:数据位收紧但代码增长明显...取决于你有多少个对象,lamdbas - 它们是美丽的 - 仍然比按位OR更难和更难得到

BTW / - 关于线程安全的论点相当弱恕我直言 - 最好记住作为背景考虑而不是成为主导决策驱动力;即使不知道它们的包装(互斥体是相对庞大的数据成员 - 我必须真正关心性能,考虑在一个对象的成员上有多个互斥体,并且我会仔细查看)足以注意到它们是位字段)。任何子字大小类型都可能具有相同的问题(例如uint8_t)。无论如何,如果你渴望更高的并发性,你可以尝试原子比较和交换样式操作。

答案 7 :(得分:3)

即使您必须使用4个字节来存储枚举(我不熟悉C ++ - 我知道您可以在C#中指定基础类型),它仍然值得 - 使用枚举。

在具有GB内存的服务器的这个时代,应用程序级别的4字节与1字节内存之类的事情通常无关紧要。当然,如果在您的特定情况下,内存使用非常重要(并且您不能让C ++使用一个字节来支持枚举),那么您可以考虑使用'static const'路由。

在一天结束时,你必须问自己,为你的数据结构节省3个字节的内存使用'static const'值得维护吗?

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

答案 8 :(得分:3)

如果您希望类的类型安全,并且具有枚举语法和位检查的便利性,请考虑Safe Labels in C++。我和作者合作过,他很聪明。

但是,请注意。最后,该软件包使用模板宏!

答案 9 :(得分:2)

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

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

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

答案 10 :(得分:2)

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

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

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

使用左移有助于指示每个值都是一个比特,后来某个人做错事的可能性更小,比如添加新值并为其赋值为9。 / p>

答案 11 :(得分:2)

根据KISShigh cohesion and low coupling,提出以下问题 -

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

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

答案 12 :(得分:2)

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

答案 13 :(得分:0)

我宁愿选择

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

仅仅因为:

  1. 它更清晰,它使代码可读和可维护。
  2. 逻辑上对常量进行分组。
  3. 程序员的时间更重要,除非您的作业 以保存这3个字节。

答案 14 :(得分:0)

并非我喜欢过度设计所有内容,但有时在这些情况下,创建一个(小)类来封装此信息可能是值得的。 如果您创建一个RecordType类,那么它可能具有如下函数:

void setDeleted();

void clearDeleted();

bool isDeleted();

等...(或任何惯例诉讼)

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

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

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

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

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