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

в проекте 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;
}

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

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

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

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

15 ответов


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

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

namespace RecordType {

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

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

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

xInvalid = 16 };

считайте, что у вас есть две функции для этого типа. Для отслеживания текущего состояние записи и создать маску для выбора записей в некоторых штатах. Создайте встроенную функцию, чтобы проверить, является ли значение типа допустимым для вашей цели; как маркер состояния против маски состояния. Это будет ловить ошибки, как typedef просто int и значение 0xDEADBEEF может быть в вашей переменную через неинициализированное или переменные mispointed.

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

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


забыть определяет

они загрязнят ваш код.

bitfields?

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

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

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

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

и если вам нужно больше причин, чтобы не используйте bitfields, возможно Реймонд Чен убедит вас в его Старая Новая Вещь сообщение: анализ затрат и выгод битовых полей для коллекции логических значений at 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. 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 ?

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


вы исключили std:: bitset? Наборы флагов - это то, для чего они нужны. Do

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

потому что есть куча перегрузок оператора для bitset, теперь вы можете сделать

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 и из него), но обычно заполнение все равно убьет преимущество.

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

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


вот несколько статей о const против макросов против перечислений:

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

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


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


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


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

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

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

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

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

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

я равнодушен: бит данных пакет плотнее, но код значительно растет... зависит от того, сколько у вас объектов, а ламдбы - какими бы красивыми они ни были - все равно более беспорядочные и их труднее получить правильно, чем побитовые ОРС.

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


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

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

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

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


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

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


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

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

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


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

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

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

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


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

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

есть большая книга"Крупномасштабный Дизайн Программного Обеспечения C++" этот продвигает базовые типы извне, если вы можете избежать зависимости от другого файла заголовка / интерфейса, вы должны попытаться.


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


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

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

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

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

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

void setDeleted ();

void clearDeleted ();

bool isDeleted ();

etc... (или любой другой конвенции подходит)

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

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

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

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

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