Надлежащая практика программирования для определений макросов (#define) в C

Например, никогда не определяйте такой макрос:

#define DANGER 60 + 2

Это может быть потенциально опасно, когда мы выполняем такую ​​операцию:

int wrong_value = DANGER * 2; // Expecting 124

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

#define HARMLESS (60 + 2)

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

Спасибо за ваше время!


person Srikanth    schedule 26.11.2008    source источник
comment
Это не вопрос. Это заявление.   -  person BIBD    schedule 26.11.2008


Ответы (11)


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

#define MIN(a,b)  a < b ? a : b     // WRONG  

int i = MIN(1,2); // works
int i = MIN(1,1+1); // breaks

#define MIN(a,b)  (a) < (b) ? (a) : (b)   // STILL WRONG

int i = MIN(1,2); // works
int i = MIN(1,1+1); // now works
int i = MIN(1,2) + 1; // breaks

#define MIN(a,b)  ((a) < (b) ? (a) : (b))   // GOOD

int i = MIN(1,2); // works
int i = MIN(1,1+1); // now works
int i = MIN(1,2) + 1; // works

Однако MIN(3,i++) все еще сломан...

Лучшее правило - использовать #defines только тогда, когда НИКАКОЙ ДРУГОЙ ПОДХОД НЕ РАБОТАЕТ! Я знаю, что вы спрашиваете о C вместо C++, но все же имейте в виду.

person Roddy    schedule 26.11.2008
comment
Возможно, вам следует добавить примечание об использовании перечисления вместо определения числовых констант с помощью #define. - person Jonathan Leffler; 26.11.2008
comment
ак! МИН(3, я++)! Я никогда не рассматривал этот случай... Отладка должна быть отстойной! - person Adam Davis; 26.11.2008
comment
Конечно, исправление тривиально — сделайте это функцией или уберите приращение из этой строки кода. Я не согласен с тем, что макросы не следует использовать, за исключением случаев, когда никакой другой подход не работает. Как и любые другие макросы инструментов, они могут быть опасны при неправильном использовании, но при правильном выполнении они могут сократить время разработки. - person Adam Davis; 26.11.2008

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

 #define DOIT(x) do { x } while(0)

Данная форма имеет следующие преимущества:

  1. Нужна завершающая точка с запятой
  2. Он работает с вложением и фигурными скобками, например. с если/иначе
person unwind    schedule 26.11.2008
comment
А что, если x — это всего лишь один оператор? Например, foobar = 42, нам все еще нужно заключать его в do{}while(0), или достаточно просто заключить его в круглые скобки? - person Denilson Sá Maia; 03.09.2011
comment
Он вам все равно понадобится, если вы хотите: 1) макрос требовал завершающую точку с запятой и 2) использовал его как отдельную инструкцию в однострочном операторе if. - person luis.espinal; 06.11.2011
comment
Проверьте эту ссылку. Это очень хорошо объясняется: kernelnewbies.org/FAQ/DoWhile0 - person luis.espinal; 06.11.2011

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

#define MAX(x, y) ((x) > (y) ? (x) : (y))

Избегайте написания макросов, которые многократно оценивают свои аргументы. Такие макросы не будут вести себя должным образом, если аргументы имеют побочные эффекты:

MAX(a++, b);

Будет оценивать a++ дважды, если a больше, чем b.


Используйте имена макросов в ЗАГЛАВНЫХ РЕГИСТРАХ, чтобы было ясно, что это макрос, а не функция, чтобы различия можно было учитывать соответствующим образом (еще одна общая хорошая практика — не передавать аргументы, которые имеют побочные эффекты, функциям).


Не используйте макросы для переименования таких типов:

#define pint int *

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

pint a, b;

Вместо этого используйте typedefs.

person Robert Gamble    schedule 26.11.2008

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

static const int DANGER = 60 + 2;
person John Dibling    schedule 26.11.2008
comment
Будет ли это действительно работать в стандартном C? В заголовочном файле? - person Roddy; 26.11.2008
comment
@Roddy: нет, это не будет работать в C в заголовке - вместо этого используйте перечисление. - person Jonathan Leffler; 26.11.2008
comment
@John: это зависит от контекста, уместно ли это. - person Steve Melnikoff; 20.03.2009

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

#define LESS_THAN(X,Y) (((X) < (Y) ? (X) : (Y))
person EvilTeach    schedule 26.11.2008

Ответ на макросы MAX/MIN, взятый из взломов GCC в ядро Linux:

#define min(x, y) ({                       \
        typeof(x) _min1 = (x);             \
        typeof(y) _min2 = (y);             \
        (void) (&_min1 == &_min2);         \
        _min1 < _min2 ? _min1 : _min2; })
person dalle    schedule 26.11.2008
comment
typeof является расширением gcc и не должен использоваться в переносимом коде. - person Robert Gamble; 26.11.2008
comment
Согласен с Робертом. Кто-нибудь хочет объяснить, для чего нужны приведение и выражение пустоты? - person Jonathan Leffler; 26.11.2008
comment
Кроме того, использование ({ ...; foo;}) для вычисления foo. Это новое на мне. - person Mike Dunlavey; 26.11.2008
comment
Интересно, пытаются ли приведение и выражение void сказать компилятору, что _min1 и _min2 должны быть совместимыми скалярами, а не быть в регистрах? - person Mike Dunlavey; 26.11.2008
comment
использование ({ ...; foo;}) является еще одним расширением GCC. Вот почему это хак GCC, а не хак C. Не уверен, что еще, но пустота останавливает вас, используя один подписанный и один неподписанный аргумент. Типы x и y будут совместимы, но типы их указателей — нет. - person Steve Jessop; 26.11.2008
comment
@onebyone: спасибо за объяснение пустоты. - person Jonathan Leffler; 27.11.2008

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

#define MAX 10

может легко конфликтовать с другим кодом, поэтому:

#define MYPROJECT_MAX 10

или что-то еще более уникальное, было бы лучше.

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

person DarthPingu    schedule 26.11.2008

Для многострочных макросов используйте do { } while (0):

#define foo(x) do {  \
    (x)++;           \
    printf("%d", x); \
} while(0)

Вы сделали

#define foo(x) {     \
    (x)++;           \
    printf("%d", x); \
}

вместо,

if (xyz)
    foo(y);
else
    foo(z);

потерпел бы неудачу.

Также будьте осторожны при введении временных переменных в макросы:

#define foo(t) do {    \
    int x = (t);       \
    printf("%d\n", x); \
} while(0)

int x = 42;
foo(x);

напечатает 0, а не 42.

Если у вас есть сложное выражение, которое должно возвращать значение, вы можете использовать оператор запятой:

#define allocate(foo, len) (foo->tmp = foo->head, foo->head += len, foo->tmp)
person sanjoyd    schedule 04.05.2011

Отмените определение макросов.

Ваш #defines должен совпадать с #undef. Это предотвращает засорение препроцессора и воздействие на непреднамеренные фрагменты кода.

person lillq    schedule 16.01.2009

Если вы внимательны и опытны, вы можете выполнить СУХОЙ (не повторяйте себя) код, используя макросы в качестве простых генераторов кода. Вам нужно объяснять другим программистам, что вы делаете, но это может сэкономить много кода. Например, метод списка макросов:

// define a list of variables, error messages, opcodes
// or anything that you have to write multiple things about
#define VARLIST \
    DEFVAR(int, A, 1) \
    DEFVAR(double, B, 2) \
    DEFVAR(int, C, 3) \

// declare the variables
#define DEFVAR(typ, name, val) typ name = (val);
    VARLIST
#undef  DEFVAR

// write a routine to set a variable by name
void SetVar(string varname, double value){
    if (0);
    #define DEFVAR(typ, name, val) else if (varname == #name) name = value;
        VARLIST
    #undef  DEFVAR
    else printf("unrecognized variable %s\n", varname);
}

// write a routine to get a variable's value, given its name
// .. you do it ..

Теперь, если вы хотите добавить новую переменную, удалить одну или переименовать ее, это редактирование в 1 строку.

person Mike Dunlavey    schedule 26.11.2008
comment
Я надеюсь, что мне никогда не придется поддерживать какой-либо код, написанный вами с использованием этой идиомы. - person Jonathan Leffler; 26.11.2008
comment
Как я уже сказал, для этого требуется объяснение, которое верно для всего, кроме кода с наименьшим общим знаменателем. Если вы хотите генерировать код, это способ. - person Mike Dunlavey; 26.11.2008
comment
есть места, где это приемлемо. Но нужно быть очень осторожным, используя эту технику. - person Ilya; 26.11.2008
comment
Верно. Не каждая полезная идея популярна или хорошо известна. - person Mike Dunlavey; 26.11.2008

Посмотрите, как я ненавижу это:

void bar(void) {
    if(some_cond) {
        #define BAZ ...
        /* some code */
        #undef BAZ
    }
}

Всегда ставьте их так:

void bar(void) {
    if(some_cond) {
#define BAZ ...
        /* some code */
#undef BAZ
    }
}
person Johannes Schaub - litb    schedule 26.11.2008
comment
Хотя я согласен с вами, это не имеет ничего общего с определениями макросов. - person Robert Gamble; 26.11.2008
comment
правильно. я исправил это. Спасибо :) - person Johannes Schaub - litb; 26.11.2008
comment
Хотел бы я, чтобы мои жалобы были такими большими :-) - person Mike Dunlavey; 28.11.2008
comment
ну, мне это не нравится :) то же самое с ярлыками goto - person Johannes Schaub - litb; 28.11.2008