Типобезопасные флаги на базе enum

При работе с микроконтроллерами на низком уровне очень часто приходится работать с отдельными битами в конфигурационных регистрах, с различными флагами состояний и т.д. Традиционно в языке «С» для этого используются целочисленные переменные и битовые операции с ними. Всё вроде-бы просто и понятно, что еще нужно? Типобезопасность. Целочисленной переменной, используемой для флагов, можно присвоить любое неосмысленное число, и компилятор совершенно не будет против.

Перейдём к практике. Символические имена для флагов удобно объявить с помощью enum-а:
typedef enum{
	flag0 = (1 << 0),
flag1 = (1 << 1),
...
flagN = (1 << N)
} MyFlags;

Типичная целочисленная флаговая переменная:

int myFlags;
…
// установили флаги 0 и 1
myFlags = flag0 | flag1;
…
// если флаг 1 установлен - сбросить его
if(myFlags & flag1)
	myFlags &= ~flag1;
…
// переключить флаг 0
myFlags ^= flag1;

Пока всё выглядит чисто и красиво, но ничего не мешает нам писать так:

myFlags = 55;
...
myFlags |= 7;
...
myFlags &= 10;
...
// и т.д.

Какие там флаги были сброшены, установлены? Компилятор всё съест и не подавится, myFlags — целочисленная переменная и может хранить что угодно. А ведь, иногда, кажется так просто вместо длинной строки, вида: flag0 | flag1 |...| flagM написать одно коротенькое число, которое кажется таким очевидным… Сейчас — очевидным, завтра для себя-же оно уже ничего не будет значить, а для других и подавно.
Хочется огородить себя от таких «лишних возможностей», чтобы работа с флагами была более строгой. Очевидно, что в таком случае, хранить флаги, нужно не в целочисленной переменной, а в переменной типа enum-а — MyFlags.

MyFlags flags = flag0;
...
flags = flag0 | flag1; // ошибка - flag0 | flag1 --> int

Первая строчка работает как ожидается, переменной flags нельзя присвоить абы-что — только элементы перечисления. Но тут нас поджидает одна неприятность, выражение flag0 | flag1 (как и другие битовые операции с перечислениями) имеет тип int и вторая строчка вызовет ошибку компиляции. Придётся использовать приведения типов, но делать это явно при каждой операции с флагами не лучше, чем хранить флаги в int:

flags = (MyFlags)(flag0 | flag1);

Можно запрятать эти преобразования типа в специальные функции (только не в макросы, иначе весь контроль типов пойдёт лесом):
inline MyFlags Combine2(MyFlags a, MyFlags b)
{
	return (MyFlags)(a | b);
}

inline MyFlags Combine3(MyFlags a, MyFlags b, MyFlags c)
{
	return (MyFlags)(a | b | c);
}

inline MyFlags Clear2(MyFlags a, MyFlags b)
{
	return (MyFlags)(a & b);
}

inline MyFlags Clear3(MyFlags a, MyFlags b, MyFlags c)
{
	return (MyFlags)(a & b & c);
}

Написать таких функций для какого-то разумного числа аргументов и пользоваться:

// установить флаги 0, 2 и 3
MyFlags flags = Combine3(flag0, flag2, flag3);
…
// очистить флаг 2
flags = Clear2(flags,  flag2);
...
// так нельзя - ошибка компиляции
flags |= 11;
…
if(flags & flag0) // по-прежнему работает
...

Вроде уже не плохо — своей цели, типобезопасности, мы уже достигли. На С++ можно сделать еще лучше — просто перегрузить для нашего флагового еnum-а битовые операции:

// битовое или
inline MyFlags
operator|(MyFlags left, MyFlags right)
{ return MyFlags(static_cast<int>(left) | static_cast<int>(right)); }
// и версия оператора с присваиванием 
inline const MyFlags&
operator|=(MyFlags& left, MyFlags right)
{ return left = left | right; }
// остальные операции
...

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

MyFlags flags = flag0 | flag2; // OK
flags = flag0 | flag1; // OK
flags &= ~(flag0 | flag2); // OK
flags |= flag3; // OK

Для удобства дальнейшего использования запихнём перегруженные операторы для enum-ов в макроопределение:
//////////////////////////////////////////////////////////////////////////
// DECLARE_ENUM_OPERATIONS is used to declare enum bitwise operations
// to use enum type as a type safe flags
//////////////////////////////////////////////////////////////////////////

#define DECLARE_ENUM_OPERATIONS(ENUM_NAME)				\\
inline ENUM_NAME							\\
operator|(ENUM_NAME left, ENUM_NAME right)				\\
{ return ENUM_NAME(static_cast<int>(left) | static_cast<int>(right)); }	\\
									\\
inline ENUM_NAME							\\
operator&(ENUM_NAME left, ENUM_NAME right)				\\
{ return ENUM_NAME(static_cast<int>(left) & static_cast<int>(right)); }	\\
									\\
inline ENUM_NAME							\\
operator^(ENUM_NAME left, ENUM_NAME right)				\\
{ return ENUM_NAME(static_cast<int>(left) ^ static_cast<int>(right)); }	\\
									\\
inline ENUM_NAME							\\
operator~(ENUM_NAME left)						\\
{ return ENUM_NAME(~static_cast<int>(left)); }				\\
									\\
inline const ENUM_NAME&							\\
operator|=(ENUM_NAME& left, ENUM_NAME right)				\\
{ return left = left | right; }						\\
									\\
inline const ENUM_NAME&							\\
operator&=(ENUM_NAME& left, ENUM_NAME right)				\\
{ return left = left & right; }						\\
									\\
inline const ENUM_NAME&							\\
operator^=(ENUM_NAME& left, ENUM_NAME right)				\\
{ return left = left ^ right; }						\\

Теперь для пользования типобезопасных флагов достаточно написать соответствующее перечисление и задать для него битовые операции:

enum MyFlags
{
	flag0 = (1 << 0),
	flag1 = (1 << 1),
	flag2 = (1 << 2),
	flag3 = (1 << 3)
};
DECLARE_ENUM_OPERATIONS(MyFlags)
…
// можно использовать
MyFlags flags = flag0 | flag2 | flag3;
...

Такими флагами, например, удобно пользоваться для инициализации различной периферии. Пример инициализации модуля USART в STM32:

enum UsartMode
{
	DataBits8 = 0,
	DataBits9 = USART_CR1_M

	NoneParity = 0,
	EvenParity = USART_CR1_PCE,
	OddParity  = USART_CR1_PS | USART_CR1_PCE

	NoClock = 0,
	
	Disabled = 0,
	RxEnable = USART_CR1_RE,
	TxEnable = USART_CR1_TE,
	RxTxEnable  = USART_CR1_RE | USART_CR1_TE,
	
	OneStopBit         = 0,
	HalfStopBit        = USART_CR2_STOP_0 << 16,
	TwoStopBits        = USART_CR2_STOP_1 << 16,
	OneAndHalfStopBits = (USART_CR2_STOP_0 | USART_CR2_STOP_1) << 16,
	
	Default = RxTxEnable | DataBits8 | NoneParity | OneStopBit
};
DECLARE_ENUM_OPERATIONS(UsartMode)
...
static void Usart::Init(unsigned baud, UsartMode flags = Default)
{
	ClockCtrl::Enable();
	unsigned brr = Clock::SysClock::FPeriph() / baud;
	Regs()->BRR = brr;
	Regs()->CR1 = (flags & 0xffff) | USART_CR1_UE ;
	Regs()->CR2 = (flags >> 16) & 0xffff;
}
...
Usart::Init(115200, RxTxEnable | TwoStopBits | EvenParity);


  • +2
  • 31 мая 2011, 16:41
  • neiver

Комментарии (73)

RSS свернуть / развернуть
При определении констант обязательно типизировать единицу:

flag0 = (1UL),
flag1 = (1UL << 1),
flag2 = (1UL << 2),

flag31 = (1UL << 31)

(Кстати, на сдвиг на 0 некоторые компиляторы ругаются, потому его без надобности лучше не писать
0
Почему необходимо?
0
Потому что константа (1 << 16) будет равна 0
0
На AVR — да, на STM32 — нет.
Надо учитывать размерность int на целевой платформе.
Кстати, на нулевой сдвиг, ГЦЦ не ругается даже с опцией -pedantic. IAR тоже молчит.
А вот на (1 << 16) avr-gcc ругается и IAR для AVR тоже.
0
>>Надо учитывать размерность int на целевой платформе.

Надо учитывать возможность переноса кода на другую платформу.

А варнинги — они всегда на усмотрение разработчиков компилятора. 9 компиляторов ругнутся на (1<<16), а 10ый промолчит. Нельзя им оставлять неоднозначностей.
0
Ц такой Ц.
«Верёвка достаточной длины, чтобы выстрелить себе в ногу», однако.
0
Хм, ну это если юзается long под флаги )
А такое оч редко бывает
К тому же, если что, всё равно компилятор ругнётся
0
Сурово… Но, по-моему, больно избыточно все это.
+2
Почему избыточно? Очень многие Си-плюс-плюс-ные библиотеки такой приём используют. В новом стандарте специальную конструкцию придумали enum class для жестко типизированных перечислений, которые вообще не кастятся к int. Вобщем-то, получаем более жесткий контроль типов во время компиляции и почти бесплатно.
0
Почти?
0
Да, почти. Выражение типа flag0 | flag2 перестаёт быть константой времени компиляции в полной мере. Это не значит, что компилятор не вычислит его во время компиляции, вычислит. Но его нельзя использовать там, где нужно именно целочисленное константное выражение.
0
"… но ничего не мешает нам писать так..."
Вспоминается анек:
— Доктор, я когда так делаю, мне больно!
— А вы так не делайте!
0
Как на счет ситуации, когда Вы создаёте основу, архитектуру проекта, а завершают его реализацию и сопровождают его другие люди? Чем меньше им будет возможностей напортачить, тем больше шансов, что проект останется целостным и самосогласованным. У вас, видимо, маленький опыт коммандной работы :)
0
да тут у большинства он не особо большой
0
Ололо, я таки не ошибся насчет командной работы?) Насчет закладывания архитектуры, которую реализуют другие, зачастую индусы, я тоже подумал.
0
Ну давайте не будем делать необоснованные предположения…
Вы путаете архитектуру и стиль написания. Тут всё дело в том, что сам язык позволяет делать подобные конструкции, а значит их можно вписать, даже в Вашем варианте можно вывернуться, если использоваться приведение типов (-а вы так не делайте). Если уж разговор заходит о безопасных конструкциях, имеет смысл вводить изменения в язык! Чтобы сам компилятор (да даже редактор) отслеживал слабые конструкции. Но тогда это будет не Си. Всё остальное — в любом случае договорённости, чтобы не было больно…
0
Явные приведения типов в данном случае будут сразу бросаться в глаза и их, в отличии от целоцисленной константы, легко найти поиском с помощью регулярного выражения.
Если можно себя и других чуточку опезопасить от таких вещей, причем бесплатно, почему этого не сделать?
0
А почему бы не пойти дальше и использовать битовые поля? :)
А вообще, языку C++ не место в микроконтроллерах имхо. У него есть свои области применения, хоть он и универсален.
0
С битовыми полями запись будет много длиннее, если нужно изменить несколько «независимых» флагов:
struct FlagsS
{
unsigned flag0 :1;
...
unsigned flagN :1;
};
FlagsS flags;
....
flags.flag1 = 1;
flags.flag3 = 1;


C++ не место в микроконтроллерах
А почему? Обычно единственные вменяемый аргумент, который я слышу — это что-то типа: «Я его не знаю, поэтому и не место.»
Так, что попрошу аргументировать.
0
Аргумент очень простой. Нужно лишь ответить на пару вопросов:
Почему большинство драйверов пишут именно на Си, а не на C++?
И почему ядра большинства операционных систем тоже написаны на Си, да еще и с ассемблерными вставками?
Высокоуровневому языку не место в низкоуровневых приложениях.
0
С таких нелепых аргументов начинаются холивары.
90% успешности реализации приложения зависит не от инструмента, а от программиста.
0
Давайте будем программировать микроконтроллеры на JavaScript, если мы прекрасно владеем этим языком. От инструмента же ничего не зависит.
Или обратно. А не стоит ли нам попробовать написать веб-приложение на ассемблере?
0
С++ своеобразная штука. Из-за некоторые средств языка и общей ориентированности компиляторов на оптимизацию, он позволяет писать (при наличии прямых рук, разумеется) весьма эффективные программы не в ущерб архитектуре.
neiver не впервые приводит весьма интересные конструкции на С++, которые позволяют писать красиво и гибко без потерь в производительности и памяти.
0
Красиво для того, кто это написал.
Для того, кто будет читать и поддерживать код, это ад.
0
Смотря с какой стороны подходить к коду. Тот же boost достаточно прост и удобен в использовании и замечательно выносит мозг своим устройством.
Алсо, «красиво» — это написание кода таким образом, чтобы его было легко читать и поддерживать.
0
Хзхз, для мя красиво=просто. :)
0
Ну, простота в том числе способствует читаемости и поддерживаемости. Если конечно под простотой не считать 0x2F вместо flag1 | flag2 | flagN.
Но в моем понимании красиво — это просто, технически изящно и читаемо.

Не менее эффективные программы позволяет писать и старый-добрый Си. А если не видно разницы…
Не менее эффективные программы позволяют писать старые добрые двоичные машинные коды. Ну ладно, насчет добрых я гоню. Но и С++ в умелых руках тем же от С отличается.
0
Эх, смотря на эту «простоту» плакать хочется. :(
Мля, даже бит в регистре не можете установить, не навертев километр оболочек вокруг элементарнейшего оператора |=.
Алсо городить бессмысленные ограничения — это не C-way. Алсо чтобы не юзать 0x2F вместо flag1 | flag2 | flagN нужно мозгов значнтельно меньше чем чтобы вкурить во все эти дебри кода на сраном Си++.
В общем, мне с вами не попути. :/
0
Ну, на уровне своей программы конкретно это возможно и перебор (если работаешь один — иначе такие фокусы могут преследовать целью заставить нормально писать остальных). А вот на уровне библиотеки уже вполне осмысленно.
Пример, кстати, судя по его виду, приведен как раз из библиотеки UART.
0
Пару раз шмякнуть лбом о клаву и будут писать нормально. :)
А лишний раз без необходимости перегружать стандартные операторы не нужно. Не нужно делать вещи более сложными и запутанными просто потому, что можешь.
0
Если бы все было так просто, Вирт бы не разрабатывал все новые языки, не позволяющие выстрелить себе в ногу :)
0
А нужны ли такие языки?
1. Ненавижу когда мне указывают что делать а что нет. :(
2. Иногда бывает нужно выстрелить себе в ногу.
0
Ну, все зависит от условий. Написать для себя мигалку — нет, а вот MISRA специальный свод правил для С выпустила, во избежание ошибок там, где они приведут к катастрофическим последствиям.
0
Соглашения — это хорошо. Но в общем и целом, чем ты внимательней, чем проще и прозрачнее код, тем меньше шанс сделать ошибку и тем проще её найти.
А дурацкими ограничения и непрозрачными абстракциями можно разве что заставить разработчика ссать кипятком.
Алсо, каждый раз, натыкаясь на что-то в таком роде, вспоминаю слова Линуса по отношению к Си++ и сразу становится как-то легче. :)
Линус молодец.
0
мне больше вспоминается «интервью» со Страуструпом cs.mipt.ru/docs/comp/rus/develop/other/stroustrup_interview/
0
Стоит отметить, есть довольно похожий текстик про C, в котором в «интервью» его разработчики утвержадют, что это первоапрельская шутка.
на что-то в таком роде
tl;dr. Что там «такое»?
0
Что-то бессмысленно большое и запутанное, что я даже не стал пытаться вкурить. :)
Первоапрельская шутка? 1 января? :)
0
Это в «интервью» страструпа. К тому же, я называю так, как помню. Ща погуглю. Нашел.
0
Честно говоря, я не уверен, что на С оно было бы прямее. Кашу продуцирует программист, а не язык. Вон «автошема» на дельфи вообще мем на дельфимастере, так там автор и без возможностей С++ написал такую кашу, что никто ее разобрать не сумел. Разве что посоревновались, кто больше эксцепшнов из проги выбьет)
0
В этом сорце не просто мешанина кода, а аццкая муть из тех самых вещей, которые вроде как призваны сделать код «более читабельным», заставить других «писать нормально», etc.
Перезагрузил бинарные операции, наделал умных указателей и получил такую хрень в которой кроме тебя никто не разберётся. (
А так согласен.
Щас сам ковырял mjpg-streamer, написанный на чистом Си. Вообще, кривая архитектура сделает код нечитабельным даже при идеальном стиле написания вне зависимости от языка. )
0
Он слишком длинный, я даже и не начинал читать на самом деле. Кстати, сама программа вполне неплохо работает, так что, наверно, не все так плохо. Да и есть VirtualDubMod, так что кто-то сумел в этих кодах и разобраться.
0
Алсо, еще про С писали — «С — как нож мясника. Прост, крайне эффективен в умелых руках и крайне опасен в неумелых». То же про С++ можно сказать, кроме простоты разве что)
0
Лучше-бы еще сразу вспомнили ответ Линусу Стефана Дьюхэрста, по-моему, совершенно замечательный, советую почитать:
www.informit.com/guides/content.aspx?g=cplusplus&seqNum=411
+1
Ответ ниочём. Линус говорил совсем о другом.
А я люблю Си за его простоту, открытость и прозрачность. И меня оскорбляет то, что эта чудовищная поделка носит имя Си, да ещё и с плюсами.
0
Бывает и хуже. Например, микропаскаль.
0
Ну микропаскаль, как я понял, это просто кривой компилятор.
Там же не насиловали сам язык.
0
Почти не насиловали. Но он настолько кривой, что это позор >_<
Я правда больше дельфист, там Паскаль изнасилован не хуже, чем С) Правда, и название языка другое.
0
И меня оскорбляет то, что эта чудовищная поделка носит имя Си, да ещё и с плюсами.
Спасибо, поржал!
Чтож тогда про C# сказать можно? Как Мелко-мягкие только посмели применить священную букву «Си» названии этого, с позволения сказать, языка! Там даже указателей нет(про unsafe сделаем вид, что забыли) и свободных цистых Си-шных функций! :)
0
А еще они взяли мой дельфи и замаскировали сишным синтаксисом и названием С#!
0
Ну на C# скорее явистам надо наезжать. Я же сишник. :)
Впрочем, радует что скока бы не изобретали Си++, Си--, Objective C и прочих C-киллеров, сишечка живее всех живых. :)
0
Не менее эффективные программы позволяет писать и старый-добрый Си. А если не видно разницы…
Я согласен с тем, что именно в этой статье приведен хороший пример применения C++. И что можно применять все его фичи, не касающиеся ООП. Применение ООП для микроконтроллеров это уже чистой воды извращение.
-1
Применение ООП для микроконтроллеров это уже чистой воды извращение
ООП бывает разное, микроконтроллеры бывают разные. все зависит от задачи
0
А почему бы нет? Самая короткая дорога та, которую знаешь. С++ великолепный инструмент для прогр. мк. Какой еще язык даст вам такую инкапсулированность, расширяемость и переносимость?
0
Спрос рождает предложение. Если возникнет необходимость в JavaScript'e для МК, то он будет создан и найдет свое применение.

Все эти Ваши суждения о С++, ООП и извращениях — из-за отсутствия достаточного опыта как в программировании, так и в применении МК. (То же самое в разное время говорили и о Си, и о RTOS).
0
Ну чтож… Никто не запрещает забивать гвозди микроскопом.
0
С битовыми полями запись будет много длиннее, если нужно изменить несколько «независимых» флагов

Объединение переменных в union решает эту проблемку

typedef struct _FlagsS
{
unsigned _0 :1;
...
unsigned _N :1;
}FlagsS;
typedef union _Flag
{
  unsigned char f;
  FlagsS   flag;
}Flag;
Flag flags;
...
flags.f = 55;
flags.flag._0 = 1;
flags.flag._1 = 0;
0
Если не считать того, что MISRA и другие гайдлайны крайне косо смотрат на такое использование юнионов.
+1
Насколько я помню, некоторые гайдлайны косо смотрят и на битовые поля сами по себе. Вроде как всё ещё продолжается вечная борьба остроконечников с тупоконечниками.
0
MISRA, да. Но с юнионами все еще забавней:
YS: critical.eschertech.com/2010/03/12/using-and-abusing-unions/
Лооол! Оказывается, самое логичное и первым приходящее в голову использование union'ов вообще изначально не предполагалось.
YS: "… the language explicitly forbids it, by casting its second-strongest verdict: reading from any element of a union other than the one most recently written «yields an unspecified result»."
www.keil.com/forum/24022/
0
Если я правильно понял английский текст то в нём, по union-ам, не сказано ничего нового.
И так понятно, что при записи float в union нельзя прочитать как int в надежде получить целую часть числа. Даже если union состоит из float и int.

пс. если использовать безымянную структуру, то, запись флагов получится проще
typedef union _Flag
{
  unsigned char f;
  struct
  {
    unsigned _0 :1;
    ...
    unsigned _7 :1;
  };
}Flag;
Flag flag; 
...
flag.f = 55;
flag._0 = 0;
flag._1 = 1;


Про мисру ничего не знаю, кроме того что она есть.
0
Нет. Записав float в union, нельзя ожидать, что из int ты прочитаешь reinterpret_cast(float). И если ты пишешь flag.f = 0 — то нет никакой гарантии, что это сбросит битовые флаги. Да, обычно это работает, но согласно стандарту это UB.
0
Парсер…
reinterpret_cast<int>(float)
0
Записав float в union, нельзя ожидать, что из int ты прочитаешь reinterpret_cast(float).
Непонятно для чего может понадобится такой cast. Есть же отдельный доступ к int и отдельно к float.
Или ты о том что нельзя определить что именно хранится внутри union в каждый момент времени — int или float? Это да.
lag.f = 0 — то нет никакой гарантии, что это сбросит битовые флаги. Да, обычно это работает, но согласно стандарту это UB.
С битовыми полями UB не меньше проблем доставляет.
0
Я о том, что стандарт запрещает читать не те поля юниона, в которые производилась последняя запись. Другими словами, если в примере выше ты пишешь flag.f = 55 и рассчитываешь что это как-то повлияет на flag._0… flag._7 — ты получаешь UB. И мы вновь возвращаемся к тому, что флаги при старте должны инициализироваться как
flag._0 = 0;
flag._0 = 0;
0
Блин…
flag._0 = 0;
flag._1 = 0;
flag._2 = 1;
flag._3 = 1;
flag._4 = 0;
flag._5 = 0;
flag._6 = 1;
flag._7 = 0;

А в коде neiver'а можно написать
flag = flag2 | flag3 | flag6;
0
// можно использовать
MyFlags flags = flag0 | flag2 | flag3;
А если надо установить один флаг не затрагивая остальных, то вся «типобезопасность» испаряется…
flags |= flag0;
0
Почему?
0
flags |= flag0;
Вместо flag0 можно писать что-угодно, варнинга не будет.
Хотя с другой стороны, все это дело можно опять же засунуть в inline функции, как предлагает автор для установки нескольких флагов.
0
Почему не будет? На flags |= 0x02 компилятор ругается:
error C2679: binary '|=': no operator found which takes a right-hand operand of type 'int' (or there is no acceptable conversion)
0
У меня IAR не ругался…
0
Ну, это MSVC. А может у тебя что-то иначе было. Например, конструктор enum ENUM_NAME из инта добавил.
0
Юрий, если у тебя есть возражения по делу — выкладывай.
+1
Проверил еще на GCC, IAR и BCC (да, я знаю, что такие вещи проверять надо по стандарту, но мне лень его читать). Первые два ругаются, BCC спокойно компилит. Ругань везде имеет один смысл — нету переопределенного оператора для int.
IAR C/C++ Compiler for ARM 6.10.2.32244 (6.10.2.32244):
Error[Pe511]: this operation on an enumerated type requires an applicable user-defined operator function H:\tmp\main.cpp 42 
Error while running C/C++ Compiler
+1
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.