Должен ли я использовать #define, enum или const?

В проекте С++, над которым я работаю, у меня есть флаг типа значения, который может иметь четыре значения. Эти четыре флага могут быть объединены. Флаги описывают записи в базе данных и могут быть:

  • новая запись
  • удаленная запись
  • измененная запись
  • существующая запись

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

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 может попасть по ошибке.

Есть ли какой-нибудь другой более чистый способ?

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

P.S. Остальная часть кода представляет собой довольно чистый современный С++ без #define s, и я использовал пространства имен и шаблоны в нескольких местах, поэтому они тоже не могут быть и речи.

Ответ 1

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

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

namespace RecordType {

Перечисление объявляет и определяет заданное время компиляции. Всегда используйте проверку типа времени компиляции, чтобы убедиться, что аргументы и переменные заданы правильным типом. В С++ нет необходимости в 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;
}

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

Ответ 2

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

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

битовые?

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 или файле заголовка, их значения будут вложенными. Вы можете использовать переключатель для этих значений, но это немного увеличит сцепление.

А, да: удалить статическое ключевое слово. 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. Вы работаете на С++. Те typedefs перечислений и структур загрязняют код больше, чем что-либо еще.

Результат выглядит следующим образом:

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

Ответ 3

Вы исключили 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, вероятно, более полезны.

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

Я не покупаю, что листинг перечислений является серьезным недостатком - хорошо, так что он немного шумно, и присвоение значения вне диапазона для перечисления - это поведение undefined, поэтому теоретически можно застрелить себя в ногами на некоторых необычных реализациях С++. Но если вы делаете это только при необходимости (что происходит при переходе от int к enum iirc), это совершенно нормальный код, который люди видели раньше.

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

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

Для предпочтения я также поставил "= 2" в перечислении, между прочим. Это не обязательно, но "принцип наименьшего удивления" предполагает, что все 4 определения должны выглядеть одинаково.

Ответ 5

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

Ответ 6

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

Ответ 7

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

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

// 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 может попасть по ошибке.

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

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

Ну, в конце концов, проверенный C-стиль побитового OR перечислений работает очень хорошо, как только у вас есть битовые поля и пользовательские операторы на картинке. Вы можете еще больше повысить свою надежность с помощью некоторых пользовательских функций проверки и утверждений, как в ответе mat_geek; методы, которые в равной степени применимы к обработке строки, int, двойным значениям и т.д.

Вы можете утверждать, что это "чище":

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

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

Я безразличен: бит данных упаковывает, но код значительно растет... зависит от того, сколько у вас объектов, а ламбы - красивые, как есть, - все еще беспорядочно и сложнее, чем побитовые ORs.

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

Ответ 8

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

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

В конце дня вы должны спросить себя: стоит ли использовать "статический const" для сохранения 3 байтов памяти для вашей структуры данных?

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

Ответ 9

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

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

Ответ 10

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

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

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

Ответ 11

Я, вероятно, не буду использовать перечисление для такого типа вещей, где значения могут быть объединены вместе, более типично перечисления являются взаимоисключающими состояниями.

Но какой бы метод вы ни использовали, чтобы было более ясно, что это значения, которые являются битами, которые могут быть объединены вместе, вместо этого используйте этот синтаксис для фактических значений:

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

Использование сдвига влево помогает указать, что каждое значение предназначено для одного бита, менее вероятно, что позже кто-то сделает что-то не так, как добавит новое значение и присвоит ему что-то значение 9.

Ответ 12

На основе KISS, высокая степень сцепления и низкое сцепление, задайте эти вопросы -

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

Существует отличная книга " "Масштабный дизайн программного обеспечения на С++" , это способствует использованию базовых типов извне, если вы можете избежать другого файла заголовка/интерфейса, вы должны попробовать.

Ответ 13

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

Ответ 14

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

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

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

  • Он чище и делает код удобным для чтения.
  • Он логически группирует константы.
  • Время программиста более важно, если только ваша задача не сохранять эти 3 байта.

Ответ 15

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

void setDeleted();

void clearDeleted();

bool isDeleted();

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

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

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

При всем этом, если вы беспокоитесь о хранении, вы все равно можете иметь класс только с элементом данных char ', поэтому берете только небольшой объем памяти (при условии, что он не виртуальный). Разумеется, в зависимости от оборудования и т.д. Могут возникнуть проблемы с выравниванием.

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

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