Вопрос

В проекте на 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), но не критичны.С defines я теряю безопасность типов, а с enum Я теряю некоторое пространство (целые числа) и, вероятно, мне придется выполнять приведение, когда я хочу выполнить побитовую операцию.С const Я думаю, что я также теряю безопасность типов, так как случайный uint8 могли попасть по ошибке.

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

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

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

Это было полезно?

Решение

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

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

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 ;

Функции проверки значений полезны в asserts для отслеживания неверных значений, как только они используются.Чем быстрее вы обнаружите ошибку при запуске, тем меньше вреда она может нанести.

Вот несколько примеров, чтобы собрать все это воедино.

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

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

Другие советы

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

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

битовые поля?

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

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

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

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

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

Ах, да: удалите ключевое слово static.static устарел в C ++, когда используется так, как вы это делаете, и если uint8 является типом сборки, вам не понадобится объявлять this в заголовке, включенном несколькими источниками одного и того же модуля.В конце концов, код должен быть:

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 в заголовке и как constant в файле 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 для этих констант.Так что, в конце концов, выбери свой яд...:-п

Вы исключили 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

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

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

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

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

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

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

Вот пара статей о const vs .макросы противперечисления:

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

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

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

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

С defines я теряю безопасность типов

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

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

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

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

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

Вы могли бы возразить, что это "чище".:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Основанный на ПОЦЕЛУЙ, высокая когезия и низкое сцепление, задайте эти вопросы -

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

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

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

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

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

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

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

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

от Void setDeleted();

void очищен();

bool IsDeleted();

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top