Я работаю со встроенным материалом (PIC), и во всех кодах C, которые я видел, если функция принимает несколько флагов или небольших перечисляемых значений, это делается в виде битовых масок int, сделанных вручную, например:
/* first flag */
#define MY_FIRST_FLAG (1 << 0)
/* second flag */
#define MY_SECOND_FLAG (1 << 1)
/*
* some param that could have three variants
*/
#define __MY_TRISTATE_OFFSET 2
#define __MY_TRISTATE_MASK (0x03 << __MY_TRISTATE_OFFSET)
#define MY_TRISTATE_ONE (0 << __MY_TRISTATE_OFFSET)
#define MY_TRISTATE_TWO (1 << __MY_TRISTATE_OFFSET)
#define MY_TRISTATE_THREE (2 << __MY_TRISTATE_OFFSET)
/* third flag */
#define MY_THIRD_FLAG (1 << 4)
void my_func(int opts)
{
if (opts & MY_FIRST_FLAG){
/* ... */
}
switch (opts & __MY_TRISTATE_MASK){
case MY_TRISTATE_ONE:
/* ... */
break;
case MY_TRISTATE_TWO:
/* ... */
break;
case MY_TRISTATE_THREE:
/* ... */
break;
default:
/* error! */
break;
}
/* ... */
}
Но мне не очень нравится этот "компьютерный" подход:
- Типобезопасности во время компиляции вообще нет, можно по ошибке сделать следующее:
my_func(MY_TRISTATE_ONE | MY_TRISTATE_TWO);
и он скомпилируется. - Опять же, типобезопасность: если у нас есть другая функция с другими флагами, мы можем свободно передавать флаги для другой функции, и компилятор тут не поможет;
- Легко ошибиться при определении всех этих флагов; если нам нужно добавить больше флагов или параметров, мы должны быть очень осторожны, чтобы не перепутать смещение.
- Что, если однажды наше тройное состояние станет некоторой переменной с 5 состояниями? Затем мы должны изменить его маску, а также изменить смещения всех следующих за ним параметров. Если мы ошибемся здесь, никто нам не поможет. Поэтому, если что-то пойдет не так, мы должны пересматривать эту часть кода снова и снова.
Единственным преимуществом такого подхода является бинарная совместимость: данные организованы одинаково на всех платформах, поэтому разные устройства/программы могут взаимодействовать с этим интерфейсом. Но меня это почти всегда не волнует: большая часть кода работает только на одном конкретном устройстве, нет необходимости сохранять бинарную совместимость с другим миром.
Итак, для меня гораздо лучше определить небольшую структуру:
enum my_tristate_e {
MY_TRISTATE_ONE,
MY_TRISTATE_TWO,
MY_TRISTATE_THREE,
};
struct my_func_opts_s {
unsigned my_first_flag : 1;
unsigned my_second_flag : 1;
enum my_tristate_e my_tristate : 2;
unsigned my_third_flag : 1;
};
void my_func(struct my_func_opts_s opts)
{
if (opts.my_first_flag){
/* ... */
}
switch (opts.my_tristate){
case MY_TRISTATE_ONE:
/* ... */
break;
case MY_TRISTATE_TWO:
/* ... */
break;
case MY_TRISTATE_THREE:
/* ... */
break;
default:
/* error! */
break;
}
/* ... */
}
Преимущества такого подхода:
- Мы не можем по ошибке дать другую структуру
my_func
(скажем, у нас есть другая структура с параметрами для другой функции), компилятор выдаст ошибку, если мы попытаемся; - Нам не нужно копаться во всех офсетах; компилятор делает это за нас: так что здесь мы не можем ошибиться;
- Если однажды наше tristate станет переменной с 5 состояниями и мы забудем изменить ширину поля с
2
на3
, компилятор предупредит нас, что поле слишком узкое; - Поскольку компилятор может реализовать эти битовые поля по своему усмотрению, вполне вероятно, что он будет генерировать более оптимизированный код на любой платформе.
Опять же, этот подход не будет работать, если нам нужна бинарная совместимость программы, но это почти всегда не касается.
Кто-то может возразить, что вызов этой функции выглядит слишком громоздко: в случае с флагами int мы имеем:
my_func(MY_TRISTATE_TWO | MY_FIRST_FLAG);
Но в случае структуры:
my_func2((struct my_func_opts_s){
.my_first_flag = true,
.my_tristate = MY_TRISTATE_TWO,
});
Но в любом случае, на мой взгляд, строгую безопасность типов стоит печатать. И ведь мне иногда даже больше нравится второй способ, потому что этот код более самодокументирован.
Я проверил сгенерированный дизассемблированный MIPS в обоих случаях (ну, мне пришлось немного изменить функции, чтобы заставить их что-то делать, иначе они оптимизируются компилятором), и сгенерированный код практически идентичен (оптимизация -Os
, что очень часто бывает во встраиваемом мире):
Но этот подход никогда (или почти никогда) не используется. Почему? Я пропустил что-то важное?
enum
вместо#define
для обеспечения безопасности типов? - person André Sassi   schedule 15.09.2014enum
, и иногда я делаю это для некоторой удобочитаемости кода, но, насколько я знаю, нет никакой безопасности типов, которую я мог бы извлечь из этого: я могу легко выполнять побитовоеOR
для элементов из разных перечислений или смешайте значения перечисления и макросы вместе, и компилятор молча разрешит это. О каком типе безопасности вы говорите? - person Dmitry Frank   schedule 15.09.2014