Что лучше использовать #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;
}

Требования к пространству важны (byte vs int), но не критичны. С определениями я теряю безопасность типов, а с ними enumя теряю некоторое пространство (целые числа) и, вероятно, мне приходится приводить, когда я хочу выполнить побитовую операцию. С constЯ думаю , что я тоже потеряет типобезопасность , так как случайные uint8могли по ошибке.

Есть ли другой способ чище?

Если нет, что бы вы использовали и почему?

PS Остальной код - довольно чистый современный C ++ без #defines, и я использовал пространства имен и шаблоны в нескольких местах, так что это тоже не исключено.

Милан Бабушков
источник
"с enum я теряю некоторое пространство (целые числа)". Не обязательно. См. Stackoverflow.com/questions/366017/… и stackoverflow.com/questions/1113855/… (и gcc -fshort-enum . (Я предполагаю, что эти ответы C по-прежнему верны для C ++).
idbrii
@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, хотя вам все равно придется предоставлять побитовые операторы.
Джастин Тайм - Восстановить Монику

Ответы:

88

Комбинируйте стратегии, чтобы уменьшить недостатки одного подхода. Я работаю во встроенных системах, поэтому следующее решение основано на том факте, что целочисленные и побитовые операторы работают быстро, занимают мало памяти и мало используют флэш-память.

Поместите перечисление в пространство имен, чтобы константы не загрязняли глобальное пространство имен.

namespace RecordType {

Перечисление объявляет и определяет время компиляции, проверенное типизированным. Всегда используйте проверку типа во время компиляции, чтобы убедиться, что аргументы и переменные имеют правильный тип. В C ++ нет необходимости в typedef.

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

Создайте еще один член для недопустимого состояния. Это может быть полезно как код ошибки; например, если вы хотите вернуть состояние, но операция ввода-вывода не выполняется. Это также полезно для отладки; используйте его в списках инициализации и деструкторах, чтобы узнать, следует ли использовать значение переменной.

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

Единственный способ обеспечить безопасность правильных значений - использовать выделенный класс с перегрузками операторов, и это оставлено в качестве упражнения для другого читателя.

mat_geek
источник
1
В основном это хороший ответ, но в вопросе указано, что флаги можно комбинировать, а функция IsValidState () не позволяет их комбинировать.
Джонатан Леффлер,
3
@Jonathan Leffler: с моей точки зрения, я думаю, что IsValidState не должен этого делать, IsValidMask.
Жуан Портела,
1
Желательно, чтобы IsValidMaskне было возможности выбрать ничего (т.е. 0)?
Иоахим Зауэр
2
-1 Идея проверки типов во время выполнения - мерзость.
Приветствия и hth. - Альф
54

Забудьте об определениях

Они будут загрязнять ваш код.

битовые?

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

Никогда не используйте это . Вас больше заботит скорость, чем экономия 4-х единиц. Использование битовых полей на самом деле медленнее, чем доступ к любому другому типу.

Однако битовые члены в структурах имеют практические недостатки. Во-первых, порядок битов в памяти варьируется от компилятора к компилятору. Кроме того, многие популярные компиляторы генерируют неэффективный код для чтения и записи битовых элементов , и существуют потенциально серьезные проблемы безопасности потоков, связанные с битовыми полями (особенно в многопроцессорных системах) из-за того, что большинство машин не могут манипулировать произвольными наборами битов в памяти, но вместо этого должен загружать и хранить целые слова. например, следующее не будет потокобезопасным, несмотря на использование мьютекса

Источник: http://en.wikipedia.org/wiki/Bit_field :

И если вам нужно больше причин не использовать битовые поля, возможно, Рэймонд Чен убедит вас в своей статье The Old New Thing Post: Анализ затрат и выгод битовых полей для набора логических значений на 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 . static не рекомендуется в C ++ при использовании в том же порядке, и если 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 ++. Эти определения перечислений и структур загрязняют код больше, чем что-либо другое.

Результат вроде бы:

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?

Если вы хотите уменьшить связь (то есть иметь возможность скрыть значения констант и, таким образом, изменять их по желанию, не требуя полной перекомпиляции), вы можете объявить целые числа как 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

paercebal
источник
5
Как вы думаете, почему битовые поля медленные? Вы действительно профилировали код, используя его и другой метод? Даже если это так, ясность может быть важнее скорости, что немного упрощает «никогда не используйте это».
wnoise
"static const uint8 xNew;" является избыточным только потому, что в C ++ переменные с ограниченным пространством имен const по умолчанию имеют внутреннее связывание. Удалите «const», и у него будет внешняя связь. Кроме того, "enum {...} RecordType;" объявляет глобальную переменную с именем «RecordType», тип которой является анонимным перечислением.
bk1e
onebyone: Во-первых, основная причина заключалась в том, что выигрыш (несколько байтов, если они есть) был завышен потерями (медленнее доступ, как чтение, так и запись) ...
paercebal
3
onebyone: Во-вторых, весь код, который я создаю на работе или дома, по своей сути является потокобезопасным. Это легко сделать: без глобальных переменных, статических переменных, не разделяемых между потоками, если не защищено блокировкой. Использование этой идиомы нарушило бы эту базовую безопасность потоков. А для чего? Несколько байтов возможно ... :-) ...?
paercebal
Добавлена ​​ссылка на статью Раймонда Чена о скрытых затратах битовых полей.
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 к enum iirc), это совершенно нормальный код, который люди видели раньше.

Я тоже сомневаюсь в стоимости места в перечислении. Переменные и параметры uint8, вероятно, не будут использовать меньше стека, чем ints, поэтому имеет значение только хранение в классах. Бывают случаи, когда упаковка нескольких байтов в структуру дает преимущество (в этом случае вы можете приводить перечисления в хранилище uint8 и из него), но обычно заполнение все равно убивает выгоду.

Таким образом, перечисление не имеет недостатков по сравнению с другими, и в качестве преимущества дает вам некоторую безопасность типов (вы не можете назначить какое-то случайное целочисленное значение без явного приведения) и чистые способы ссылки на все.

Кстати, для предпочтения я бы также поместил в перечисление "= 2". В этом нет необходимости, но «принцип наименьшего удивления» предполагает, что все 4 определения должны выглядеть одинаково.

Стив Джессоп
источник
1
Собственно, битсет я вообще не рассматривал. Однако не уверен, что это было бы хорошо. С битовым набором я должен адресовать биты как 1, 2, 3, 4, что сделало бы код менее читаемым - это означает, что я, вероятно, использовал бы перечисление для «именования» битов. Хотя мог бы сэкономить место. Спасибо.
Милан Бабушков,
Милан, вам не нужно «называть» биты с помощью перечисления, вы можете просто использовать предопределенные биты, как показано выше. Если вы хотите включить бит один, а не my_bitset.flip (1), вы должны сделать my_bitset | = xNew;
Мосвальд,
это меньше направлено на вас и больше на STL, но: я действительно должен спросить: зачем вам это использовать bitset? он обычно переводится в long(в моей реализации iirc; да, насколько расточительно) или аналогичный интегральный тип для каждого элемента в любом случае, так почему бы просто не использовать не запутанные интегралы? (или, в настоящее время, constexprс нулевым хранилищем)
underscore_d
[править тайм-аут] ... но тогда я никогда толком не понимал смысла этого bitsetкласса, за исключением того, что кажется повторяющимся скрытым течением в окружающих обсуждениях «тьфу, мы должны скрыть неприятные низкоуровневые корни языка» '
underscore_d
« uint8переменные и параметры, вероятно, не будут использовать стек меньше, чем ints» неверно. Если у вас есть ЦП с 8-битными регистрами, вам intпотребуется как минимум 2 регистра, тогда как uint8_tнужен только 1, поэтому вам понадобится больше места в стеке, потому что у вас больше шансов быть вне регистров (что также медленнее и может увеличить размер кода ( в зависимости от комплекта инструкций)). ( У вас есть тип, он должен быть uint8_tне uint8)
+12431234123412341234123
5

По возможности НЕ используйте макросы. Когда дело доходит до современного C ++, ими не слишком восхищаются.

INS
источник
4
Правда. Что я ненавижу в макросах, так это то, что в них нельзя вмешиваться, если они ошибочны.
Carl
Думаю, это можно исправить в компиляторе.
celticminstrel 05
4

Перечисления были бы более подходящими, поскольку они предоставляют «смысл идентификаторам», а также безопасность типов. Вы можете ясно сказать, что «xDeleted» относится к «RecordType» и представляет «тип записи» (вау!) Даже спустя годы. Для этого константы потребуют комментариев, а также перехода вверх и вниз в коде.

hayalci
источник
4

С определениями я теряю безопасность типа

Не обязательно...

// signed defines
#define X_NEW      0x01u
#define X_NEW      (unsigned(0x01))  // if you find this more readable...

и с enum я теряю немного места (целые числа)

Не обязательно - но вы должны быть явными в точках хранения ...

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; методы, которые в равной степени применимы к обработке строковых, int, double значений и т. д.

Вы можете возразить, что это «чище»:

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

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

Мне безразлично: биты данных упаковываются плотнее, но код значительно увеличивается ... зависит от того, сколько у вас объектов, а lamdbas - какими бы красивыми они ни были - по-прежнему более беспорядочные и трудные для правильного выполнения, чем побитовые ИЛИ.

BTW / - аргумент о довольно слабой безопасности потоков IMHO - лучше всего помнить как фоновое соображение, а не стать доминирующей движущей силой решения; совместное использование мьютексов между битовыми полями является более вероятной практикой, даже если не знать об их упаковке (мьютексы являются относительно громоздкими элементами данных - мне нужно действительно беспокоиться о производительности, чтобы рассмотреть возможность наличия нескольких мьютексов на элементах одного объекта, и я бы внимательно посмотрел достаточно, чтобы заметить, что это были битовые поля). У любого типа размером с вложенное слово может быть такая же проблема (например, а uint8_t). В любом случае вы можете попробовать операции в стиле атомарного сравнения и замены, если вам отчаянно нужен более высокий уровень параллелизма.

Тони Делрой
источник
1
+1 Отлично. Но перед инструкцией он operator|должен быть приведен к целочисленному типу ( unsigned int) |. В противном случае он operator|будет рекурсивно вызывать себя и вызывать переполнение стека во время выполнения. Я предлагаю: return RecordType( unsigned(lhs) | unsigned(rhs) );. Cheers
olibre
3

Даже если вам нужно использовать 4 байта для хранения перечисления (я не так хорошо знаком с C ++ - я знаю, что вы можете указать базовый тип в C #), это все равно того стоит - используйте перечисления.

В наше время серверов с гигабайтами памяти такие вещи, как 4 байта против 1 байта памяти на уровне приложения в целом не имеют значения. Конечно, если в вашей конкретной ситуации использование памяти так важно (и вы не можете заставить C ++ использовать байт для поддержки перечисления), вы можете рассмотреть маршрут 'static const'.

В конце концов, вы должны спросить себя, стоит ли использовать «static const» для экономии памяти 3 байта для вашей структуры данных?

Следует иметь в виду еще кое-что - IIRC, на x86, структуры данных выровнены по 4 байта, поэтому, если у вас нет нескольких элементов ширины байта в структуре «записи», это может не иметь значения. Протестируйте и убедитесь, что это так, прежде чем идти на компромисс между ремонтопригодностью и производительностью / пространством.

Джонатан Рупп
источник
Вы можете указать базовый тип в C ++, начиная с версии языка C ++ 11. До этого я полагал, что оно было «по крайней мере достаточно большим для хранения и использования в качестве битового поля для всех указанных счетчиков, но, вероятно, intесли оно не слишком мало». [Если вы не укажете базовый тип в C ++ 11, он будет использовать устаревшее поведение. И наоборот, enum classбазовый тип C ++ 11 явно использует значение по умолчанию, intесли не указано иное.]
Джастин Тайм - Восстановить Монику
3

Если вам нужна безопасность типов классов с удобством синтаксиса перечисления и проверки битов, рассмотрите безопасные метки в C ++ . Я работал с автором, он довольно умен.

Однако будьте осторожны. В конце концов, этот пакет использует шаблоны и макросы!

Дон Уэйкфилд
источник
Похоже, для моего маленького приложения перебор. но это действительно кажется хорошим решением.
Милан Бабушков,
2

Вам действительно нужно передавать значения флагов как концептуальное целое, или у вас будет много кода для каждого флага? В любом случае, я думаю, что наличие этого как класса или структуры 1-битных битовых полей может быть более ясным:

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

Тогда ваш класс записи может иметь переменную-член struct RecordFlag, функции могут принимать аргументы типа struct RecordFlag и т. Д. Компилятор должен упаковывать битовые поля вместе, экономя место.

wnoise
источник
Иногда целиком, иногда как флаг. И мне также нужно проверить, установлен ли определенный флаг (когда я передаю его целиком).
Милан Бабушков
ну, когда отдельно, просто попросите int. Когда вместе, передайте struct.
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, ...
Джонатан Леффлер
2

Основываясь на KISS , высокой когезии и низкой связи , задайте следующие вопросы:

  • Кому нужно знать? мой класс, моя библиотека, другие классы, другие библиотеки, третьи стороны
  • Какой уровень абстракции мне нужно предоставить? Понимает ли потребитель битовые операции.
  • Придется ли мне взаимодействовать с VB / C # и т. Д.?

Есть отличная книга « Крупномасштабный дизайн программного обеспечения C ++ », в которой внешние типы продвигают базовые типы, если вы можете избежать другой зависимости файла заголовка / интерфейса, которую вы должны попробовать.

titanae
источник
1
а) 5-6 классы. б) только я, это проект одного человека в) без интерфейса
Милан Бабушков
2

Если вы используете Qt, вам следует поискать QFlags . Класс QFlags обеспечивает безопасный для типов способ хранения ИЛИ-комбинаций значений перечисления.

Томас Кошель
источник
Нет, нет Qt. Собственно, это проект wxWidgets.
Милан Бабушков,
0

Я бы предпочел пойти с

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

Просто потому что:

  1. Он чище, делает код читабельным и обслуживаемым.
  2. Он логически группирует константы.
  3. Время программиста более важно, если только ваша задача не сохранить эти 3 байта.
Вивек
источник
Что ж, у меня легко может быть миллион экземпляров класса Record, так что это может быть важно. OTOH, это просто разница между 1 МБ и 4 МБ, так что, может, мне не о чем беспокоиться.
Милан Бабушков,
@Vivek: Вы учли ограничение ширины целого числа? В частности, до C ++ 11.
user2672165
0

Не то чтобы мне нравилось все перестраивать, но иногда в этих случаях может быть стоит создать (небольшой) класс для инкапсуляции этой информации. Если вы создадите класс RecordType, он может иметь такие функции, как:

void setDeleted ();

void clearDeleted ();

bool isDeleted ();

и т. д. (или что угодно по соглашению)

Он может проверять комбинации (в случае, когда не все комбинации допустимы, например, если «новый» и «удаленный» не могут быть установлены одновременно). Если вы просто использовали битовые маски и т. Д., Тогда код, который устанавливает состояние, необходимо проверить, класс также может инкапсулировать эту логику.

Класс также может дать вам возможность прикреплять значимую информацию о журнале к каждому состоянию, вы можете добавить функцию для возврата строкового представления текущего состояния и т. Д. (Или использовать операторы потоковой передачи '<<').

Тем не менее, если вы беспокоитесь о хранилище, вы все равно можете иметь класс только с элементом данных char, поэтому возьмите только небольшой объем хранилища (при условии, что он не виртуальный). Конечно, в зависимости от оборудования и т. Д. У вас могут возникнуть проблемы с выравниванием.

Фактические значения битов могут быть невидимы для остального «мира», если они находятся в анонимном пространстве имен внутри файла cpp, а не в файле заголовка.

Если вы обнаружите, что код, использующий enum / # define / bitmask и т. Д., Имеет много «вспомогательного» кода для работы с недопустимыми комбинациями, журналированием и т. Д., Возможно, стоит подумать об инкапсуляции в классе. Конечно, в большинстве случаев простые проблемы лучше решать простыми ...

Вениамин
источник
К сожалению, объявление должно быть в файле .h, поскольку оно используется во всем проекте (используется некоторыми 5-6 классами).
Милан Бабушков,