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


125

在我正在研究的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;
}

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

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

如果没有,您将使用什么,为什么?

PS其余代码是没有#defines的现代C ++ ,并且我在很少的空间中使用了名称空间和模板,因此它们也不是没有问题。


“有了枚举,我就失去了一些空间(整数)”。不必要。请参阅stackoverflow.com/questions/366017/…stackoverflow.com/questions/1113855/…(以及gcc的-fshort-enum。(我假设这些C答案在C ++中仍然是正确的。)
idbrii,2012年

@pydave如果您不确定C和C ++的兼容性,我发现此链接非常有用,例如,请参见enum david.tribble.com/text/cdiffs.htm#C99-enum-type
aka.nice

3
这是一个投票率较高的较旧主题,是否有理由针对这种问题情况不提及C ++ 11枚举类。
布兰丁

需要注意的是,尽管还需要提供按位运算符,但enum RecordType : uint8_t将的类型安全性enum与的小尺寸值相结合uint8_t
贾斯汀时间-恢复莫妮卡

Answers:


88

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

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

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
通常,这是一个很好的答案-但问题规定可以将这些标志进行组合,而IsValidState()函数不允许将这些标志进行组合。
乔纳森·莱夫勒

3
@乔纳森·莱夫勒(Jonathan Leffler):从我的立场来看,我认为“ IsValidState”不应该这样做,而“ IsValidMask”是。
若昂里斯本

1
是否希望IsValidMask不允许不选择任何内容(例如0)?
约阿希姆·绍尔

2
-1运行时类型检查的想法是可憎的。
干杯和健康。-Alf

54

忘记定义

他们会污染您的代码。

位域?

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

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

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

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

如果你需要更多的理由使用位域,或许雷蒙德陈会说服你在他的旧事新帖子:位域的成本效益分析为布尔值的集合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或头文件中声明了它们,则它们的值将被内联。您将可以使用这些值上的开关,但是会稍微增加耦合。

嗯,是的:删除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.
   }
}

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

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


5
您为什么认为位域很慢?您是否实际使用它和其他方法来分析代码?即使是这样,清晰度也比速度更重要,这使得“永远不要使用”变得更加简单。
wnoise

“静态const uint8 xNew;” 这只是多余的,因为在C ++ const中,命名空间范围的变量默认为内部链接。删除“ const”,它具有外部链接。另外,“枚举{...} RecordType;” 声明一个名为“ RecordType”的全局变量,其类型是一个匿名枚举。
bk1e

onebyone:首先,主要原因是由于丢失(访问速度较慢,读取和写入速度较慢)而使增益(几个字节,如果有的话)过大了……
paercebal

3
onebyone:第二,我在工作或在家中生成的所有代码本质上都是线程安全的。这很容易做到:没有全局变量,没有静态变量,除非锁定保护,否则线程之间不共享。使用此习惯用法会破坏此基本线程安全性。为了什么 也许有几个字节?... :-) ...
paercebal

添加了有关Raymond Chen关于位域的隐藏成本的文章的参考。
paercebal

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

由于位集有很多运算符重载,因此您现在可以

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

或与之非常相似的东西-由于我还没有测试过,请多多指教。您也可以按索引引用位,但是通常最好只定义一组常量,而RecordType常量可能更有用。

假设您已经排除了问题,那么我投票支持该枚举

我不认为强制转换枚举是一个严重的缺点-好吧,这有点嘈杂,并且为枚举分配超出范围的值是未定义的行为,因此从理论上讲,可以使用一些不寻常的C ++射击自己实现。但是,如果仅在必要时执行此操作(即从int到枚举iirc进行转换),那么这就是人们以前见过的完全正常的代码。

我也怀疑枚举的空间成本。uint8变量和参数可能不会使用比int少的堆栈,因此仅存储类中的内容很重要。在某些情况下,将多个字节打包到一个结构中会获胜(在这种情况下,您可以将枚举转换为uint8存储空间),但通常情况下,填充会无济于事。

因此,与其他枚举相比,该枚举没有任何缺点,并且优点是给您带来了一些类型安全性(没有显式强制转换就不能分配一些随机整数值)和引用所有内容的简洁方法。

顺便说一句,我也希望在枚举中放入“ = 2”。这不是必需的,但是“最少惊讶的原则”建议所有4个定义看起来都应该相同。


1
实际上,我根本不考虑位集。但是,我不确定这是否会很好。使用位集时,我必须将位寻址为1、2、3、4,这会使代码的可读性降低-这意味着我可能会使用枚举来“命名”这些位。虽然可以节省空间。谢谢。
米兰·巴布斯科夫

米兰,您不必使用枚举为这些位“命名”,只需使用上面显示的预定义位即可。如果要打开第一位,而不是my_bitset.flip(1),则可以执行my_bitset | = xNew;。
moswald

这不是针对您而是针对STL,但是:我真的要问:您为什么要使用bitset它?它通常会转换long为每个元素的(在我的实现iirc中;是的,多么浪费)或类似的整数类型,所以为什么不只使用未混淆的整数呢?(或者,如今的constexpr存储空间为零)
underscore_d

[编辑超时] ...但是那时我从来没有真正理解过bitset该类的基本原理,除了围绕“ ugh”的讨论中似乎反复出现的暗流,我们必须掩盖该语言的卑鄙的低级根源。 '
underscore_d

uint8变量和参数使用的堆栈可能不会少于ints”,这是错误的。如果您的CPU具有8位寄存器,则int至少需要2个寄存器,而uint8_t只需要1 个寄存器,因此您将需要更多的堆栈空间,因为您更可能会用完寄存器(这也较慢,并且可能会增加代码大小(取决于指令集))。(您输入的是类型,uint8_t而不应该是uint8
12431234123412341234123


5

如果可能,请勿使用宏。当涉及到现代C ++时,他们并不是很欣赏。


4
真正。我自己对宏的讨厌是,如果它们错了,您就不能介入。
卡尔,

我想这可以在编译器中修复。
celticminstrel 2015年

4

枚举会更合适,因为它们提供了“标识符的含义”以及类型安全性。您可以清楚地知道“ xDeleted”是“ RecordType”,并且代表“记录类型”(哇!)。Consts为此需要注释,也需要在代码中上下移动。


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样式按位或运算就可以很好地工作。您可以使用mat_geek的答案中的一些自定义验证功能和断言来进一步提高鲁棒性。通常同样适用于处理字符串,整数,双精度值等的技术。

您可能会争辩说这是“更干净”的:

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

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

我无动于衷:数据位打包得更紧了,但是代码却显着增长了……取决于您拥有多少个对象,lamdbas(虽然很漂亮)仍然比按位OR更加混乱和难以正确处理。

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


1
+1好。但是在指令之前,operator|应该将其强制转换为整数类型(unsigned int|。否则,operator|will将递归调用自身,并导致运行时堆栈溢出。我建议:return RecordType( unsigned(lhs) | unsigned(rhs) );。干杯
olibre 2013年

3

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

在具有GB内存的服务器的当今时代,通常在应用程序级别上,像4字节vs. 1字节的内存之类的事情并不重要。当然,如果在您的特定情况下,内存使用非常重要(并且您无法让C ++使用字节来支持枚举),则可以考虑使用“静态const”路由。

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

还有其他需要注意的地方-IIRC,在x86上,数据结构是4字节对齐的,因此,除非在“记录”结构中有许多字节宽度的元素,否则实际上并不重要。在权衡性能/空间的可维护性之前,请进行测试并确保能做到。


从语言版本C ++ 11开始,您可以在C ++中指定基础类型。在此之前,我相信它“至少足够大,可以存储并用作所有指定枚举器的位字段,但是int除非太小,否则可能”。[如果您未在C ++ 11中指定基础类型,它将使用传统行为。相反,如果没有另外指定,则C ++ 11 enum class的基础类型显式默认为默认值int。]
贾斯汀时间-恢复莫妮卡


2

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

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

然后,您的记录类可以具有struct RecordFlag成员变量,函数可以采用struct RecordFlag类型的参数,等等。编译器应将位字段打包在一起,以节省空间。


有时作为一个整体,有时作为一个标志。而且,我还需要测试是否设置了某个标志(当我整体传递它时)。
米兰·巴布斯科夫,

好吧,当分开的时候,只要求一个整数。在一起时,传递该结构。
wnoise

不会更好。访问位字段的速度比其他任何东西都要慢。
paercebal

真?您认为编译器生成的用于测试位域的代码将与手动生成位的方法大不相同?那会大大慢吗?为什么?您不能惯用的一件事就是一次性屏蔽多个标志。
wnoise

运行一个简单的阅读测试,位屏蔽的时间为5.50-5.58秒,而位域访问的时间为5.45-5.59。几乎没有区别。
wnoise

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

2

基于 KISS高内聚和低耦合,提出以下问题-

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

有一本很棒的书《大规模C ++软件设计》,它在外部提升了基本类型,如果您可以避免其他头文件/接口的依赖性,您应该尝试这样做。


1
a)5-6节课。b)只有我一个人的项目c)没有接口
MilanBabuškov08年


0

我宁愿去

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

仅仅是因为:

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

好吧,我很容易拥有记录类的一百万个实例,因此这可能很重要。OTOH,这只是1MB和4MB之间的差异,所以也许我不用担心。
米兰·巴布斯科夫

@Vivek:您是否考虑过整数宽度限制?特别是在C ++ 11之前。
user2672165

0

我并不是想过度设计所有东西,但是有时候在这些情况下,值得创建一个(小)类来封装此信息。如果创建一个RecordType类,则它可能具有以下功能:

void setDeleted();

void clearDeleted();

bool isDeleted();

等等...(或任何适合的常规)

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

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

对于所有担心存储的问题,您仍然可以让该类仅具有一个'char'数据成员,因此仅占用少量存储(假设它是非虚拟的)。当然,取决于硬件等,您可能会遇到对齐问题。

如果“位”的其余部分位于cpp文件而不是头文件中的匿名命名空间中,则可能无法让“世界”的其余部分看到实际的位值。

如果您发现使用enum /#define / bitmask等的代码具有大量用于处理无效组合,日志记录等的“支持”代码,那么封装在类中可能值得考虑。当然,大多数情况下,简单的问题可以通过简单的解决方案来解决。


不幸的是,声明必须在.h文件中,因为它在整个项目中使用(约5-6个类使用)。
米兰·巴布斯科夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.