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

时间:2020-03-06 14:31:02  来源:igfitidea点击:

在我正在研究的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可能会误入其中。

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

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

P.S.其余代码是没有#define的相当干净的现代C ++,并且我在很少的空间中使用了名称空间和模板,因此也不是没有问题。

解决方案

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

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

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

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

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

以下是有关const,宏和枚举的几篇文章:

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

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

我们是否排除了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变量和参数可能不会使用比ints少的堆栈,因此仅存储类中的内容很重要。在某些情况下,将多个字节打包到一个结构中会获胜(在这种情况下,我们可以将枚举强制转换为uint8存储空间),但通常填充会以某种方式扼杀该优势。

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

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

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

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

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

我宁愿去

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

仅仅是因为:

  • 它更干净,并且使代码可读和可维护。
  • 它在逻辑上对常量进行分组。
  • 程序员的时间更重要,除非工作是保存这3个字节。

忘记定义

他们会污染代码。

位域?

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

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

However, bit members in structs have practical drawbacks. First, the ordering of bits in memory varies from compiler to compiler. In addition, many popular compilers generate inefficient code for reading and writing bit members, and there are potentially severe thread safety issues relating to bit fields (especially on multiprocessor systems) due to the fact that most machines cannot manipulate arbitrary sets of bits in memory, but must instead load and store whole words. e.g the following would not be thread-safe, in spite of the use of a mutex

资料来源: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或者头文件中声明了它们,则将它们的值内联。我们将可以使用这些值上的开关,但是会稍微增加耦合。

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

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

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

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

使用左移可以帮助指示每个值都是一个位,以后某人执行诸如添加新值并为其分配值为9的错误的可能性较小。

如果我们希望类的类型安全,并借助枚举语法和位检查的便利,请考虑使用C ++中的"安全标签"。我和作者一起工作过,他很聪明。

但是要当心。最后,该程序包使用模板和宏!

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

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

有一本很棒的书"大型C ++软件设计",它在外部提升了基本类型,如果我们可以避免应该依赖另一个头文件/接口的依赖性。

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

如果可能,请勿使用宏。当涉及到现代C ++时,他们并不太钦佩。

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

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

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

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

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

void setDeleted();

void clearDeleted();

bool isDeleted();

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

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

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

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

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

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