Должен ли я использовать #define, enum или const? - PullRequest
121 голосов
/ 22 сентября 2008

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

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

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

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. Остальная часть кода - довольно чистый современный C ++ без #define s, и я использовал пространства имен и шаблоны в нескольких местах, так что о них тоже не может быть и речи.

Ответы [ 15 ]

85 голосов
/ 22 сентября 2008

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

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

namespace RecordType {

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

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

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

53 голосов
/ 22 сентября 2008

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

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

битовые

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

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

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

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

И если вам нужно больше причин, чтобы не использовать битовые поля, возможно Раймонд Чен убедит вас в его Старом Новом Посте: The анализ рентабельности битовых полей для коллекции логических значений при 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 устарела в C ++, когда используется как вы, и если uint8 является типом buildin, вам не нужно это объявлять в заголовке, включенном несколькими источниками одного и того же модуля. В конце код должен быть:

namespace RecordType {
    const uint8 xNew = 1;
    const uint8 xDeleted = 2;
    const uint8 xModified = 4;
    const uint8 xExisting = 8;
}

Проблема этого подхода в том, что ваш код знает значение ваших констант, что немного увеличивает связь.

1044 * перечисление * Так же, как 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?

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

Однако вы не сможете использовать переключатель для этих констант. Итак, в конце концов, выберите свой яд ... : -Р

30 голосов
/ 22 сентября 2008

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

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

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

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

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

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

8 голосов
/ 22 сентября 2008

Вот пара статей о константах, макросах и перечислениях:

Символические константы
Константы перечисления и постоянные объекты

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

5 голосов
/ 22 сентября 2008

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

4 голосов
/ 07 августа 2012

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

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

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

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

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

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

Можно утверждать, что это "чище":

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

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

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

КСТАТИ / - аргумент о довольно слабом ИМХО в отношении безопасности потоков - лучше всего запоминать в качестве фона, а не становиться доминирующей движущей силой принятия решений; Совместное использование мьютекса между битовыми полями является более вероятной практикой, даже если он не знает об их упаковке (мьютексы являются относительно громоздкими элементами данных - мне нужно серьезно задуматься о производительности, чтобы рассмотреть возможность использования нескольких мьютексов для членов одного объекта, и я бы внимательно посмотрел достаточно заметить, что они были битовыми полями). Любой тип размера подслов может иметь такую ​​же проблему (например, uint8_t). В любом случае, вы можете попробовать атомарные операции сравнения и замены, если вам нужен более высокий параллелизм.

4 голосов
/ 22 сентября 2008

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

3 голосов
/ 22 сентября 2008

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

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

3 голосов
/ 22 сентября 2008

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

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

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

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

2 голосов
/ 22 сентября 2008

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

...