Правила программирования на С и С++. Главы 1-6 - Вы должны быть всегда способны заменить макрос функцией
ОГЛАВЛЕНИЕ
81. Вы должны быть всегда способны заменить макрос функцией.
Это вариант для макросов правила "не нужно неожиданностей (без сюрпризов)". Что-то, похожее внешне на функцию, должно действовать подобно функции, даже если это на самом деле макрос. (По этой причине я иногда предпочитаю записывать имя макроса заглавными буквами не полностью, если его поведение сильно напоминает функцию. Хотя я всегда использую все заглавные, если у макроса есть побочные эффекты). При этом возникает насколько вопросов.
Во-первых, макрос не должен использовать переменные, не передаваемые в качестве аргументов. Вот наихудший из возможных способов в таких обстоятельствах:
Следующий код находится в заголовочном файле:
#define end() while (*p) \ ++pа этот - в файле .c: char *f( char *str ){
char *p = str;
end();
// ...
return p;
}
Здесь для сопровождающего программиста имеется несколько неприятных сюрпризов. Во-первых, переменная p явно не используется, поэтому появляется искушение стереть ее, разрушая этим код. Аналогично, программа разрушится, если имя p будет заменено другим. Наконец, будет большой неожиданностью то, что вызов end(), который выглядит внешне как вызов обычной функции, будет модифицировать p.Первая попытка урегулирования этой проблемы может выглядеть следующим образом. В заголовочном файле:
#define end(p) while (*p) \ ++pи в файле .c: char *f( char *str ){
end(str);// ...
return str;
}Но теперь макрос все еще необъяснимо модифицирует str, а нормальная функция С не может работать таким образом. (Функция С++ может, но не должна. Я объясню почему в той главе книги, которая посвящена С++). Для модификации строки str в функции вы должны передать в нее ее адрес, поэтому то же самое должно быть применимо к макросу. Вот третий (наконец-то правильный) вариант, в котором макрос end() попросту заменен функцией с таким же именем. В заголовочном файле: #define end(p) while (*(*p)) \ ++(*p)и в файле .c: char *f( char *str ){
end(?str);// ...
return str;
}Вместо end(?str) будет подставлено: while (*(*?p)) ++(*?p)и *?p - это то же самое, что и p, так как знаки * и ? отменяют друг друга - поэтому макрос в результате делает следующее: while (*(p)) ++(p)Вторая проблема с макросом в роли функции возникает, если вы желаете выполнить в макросе больше, чем одно действие. Рассмотрим такой макрос: #define две_вещи() a();b()if ( x )
две_вещи();else нечто_другое();который будет расширен следующим образом (тут я переформатировал, чтобы сделать происходящее неприятно очевидным): if ( x ) a();b();else
нечто_другое();Вы получаете сообщение об ошибке "у else отсутствует предшествующий оператор if". Вы не можете решить эту проблему, используя лишь фигурные скобки. Переопределение макроса следующим образом: #define две_вещи() { a(); b(); }вызовет такое расширение: if ( x ){
a();b();
};
else
нечто_другое();Эта вызывающая беспокойство точка с запятой - та, что следует после две_вещи() в вызове макроса. Помните, что точка с запятой сама по себе является законным оператором в С. Она ничего не делает, но она законна. Вследствие этого else пытается связаться с этой точкой с запятой, и вы получаете то же самое "у else отсутствует предшествующий оператор if".Не нужно говорить, что несмотря на то, что макрос выглядит подобно вызову функции, его вызов может не сопровождаться точкой с запятой. К счастью, для этой проблемы имеется два настоящих решения. Первое из них использует малоизвестный оператор "последовательного вычисления" (или запятую):
#define две_вещи() ( a(), b() )Эта запятая - та, что разделяет подвыражения в инициализирующей или инкрементирующей частях оператора for. (Запятая, которая разделяет аргументы функции, не является оператором последовательного вычисления). Оператор последовательного вычисления выполняется слева направо и получает значение самого правого элемента в списке (в нашем случае значение, возвращаемое b()). Запись: x = ( a(),b() );означает просто: a();x = b();
Если вам все равно, какое значение имеет макрос, то вы можете сделать нечто-подобное, используя знак плюс вместо запятой. (Выражение: a()+b();в отдельной строке совершенно законно для С, где не требуется, чтобы результат сложения был где-нибудь сохранен). Тем не менее при знаке плюс порядок выполнения не гарантируется; функция b() может быть вызвана первой. Не путайте приоритеты операций с порядком выполнения. Приоритет просто сообщает компилятору, где неявно размещаются круглые скобки. Порядок выполнения вступает в силу после того, как все круглые скобки будут расставлены по местам. Невозможно добавить дополнительные скобки к ((a())+(b())). Оператор последовательного вычисления гарантированно выполняется слева направо, поэтому в нем такой проблемы нет.Я должен также отметить, что оператор последовательного вычисления слишком причудлив, чтобы появляться в нормальном коде. Я использую его лишь в макросах, сопровождая все обширными комментариями, объясняющими, что происходит. Никогда не используйте запятую там, где должна быть точка к запятой. (Я видел людей, которые делали это, чтобы не использовать фигурные скобки, но это страшно даже пересказывать).
Второе решение использует фигурные скобки, но с одной уловкой:
#define две_вещи() \ do \{ \
a(); \b(); \
} while ( 0 )if ( x ) две_вещи();else нечто_другое();что расширяется до: if ( x ) do{
a();b();
} while ( 0 ) ; // ?== точка с запятой связывается с оператором while ( 0 )else нечто_другое();Вы можете также попробовать так: #define две_вещи() \ if ( 1 ) \{ \
a(); \b(); \
} elseно я думаю, что комбинация do с while (0) незначительно лучше.Так как вы можете объявить переменную после любой открытой фигурной скобки, то у вас появляется возможность использования предшествующего метода для определения макроса, имеющего по существу свои собственные локальные переменные. Рассмотрим следующий макрос, осуществляющий обмен значениями между двумя целочисленными переменными:
#define swap_int(x,y) \ do \{ \
int x##y; \x##y = x; \
x = y; \
y = x##y \
} \while (0)
Сочетание ## является оператором конкатенации в стандарте ANSI С. Я использую его здесь для обеспечения того, чтобы имя временной переменной не конфликтовало с любым из имен исходных переменных. При данном вызове: swap(laurel, hardy);препроцессор вначале подставляет аргументы обычным порядком (заменяя x на laurel, а y на hardy), давая в результате следующее имя временной переменной: int laurel##hardy;Затем препроцессор удаляет знаки решетки, давая int laurelhardy;Дополнительная польза от возможности замены макросов функциями заключается в отладке. Иногда вы хотите, чтобы что-то было подобно макросу по эффективности, но вам нужно при отладке установить в нем точку прерывания. Используйте для этого в С++ встроенные функции, а в С используйте следующие: #define _AT_LEFT(this) ((this)->left_child_is_thread ? NULL : (this)->left)#ifdef DEBUG
static tnode *at_left(tnode *this) { return _AT_LEFT(this); }#else# define at_left(this) _AT_LEFT(this)
#endif
Я закончу это правило упоминанием о еще двух причудливых конструкциях, которые иногда полезны в макросе, прежде всего потому, что они помогают макросу расширяться в один оператор, чтобы избежать проблем с фигурными скобками, рассмотренных ранее. Положим, вы хотите, чтобы макрос по возможности расширялся в единственное выражение. Оператор последовательного вычисления достигает этого за счет читаемости, и наряду с ним я никогда не использую формы, показанные в таблице 1, по той же причине - их слишком трудно читать. (Коли на то пошло, я также не использую их в макросах, если я могу достичь желаемого каким-то другим способом). Таблица 1. Макросы, эквивалентные условным операторам.Этот код: | Делает то же самое, что и: |
( a ?? f() ) | if ( a ) f(); |
( b || f() ) | if ( !b ) f(); |
( z ? f() : g()) | if ( z ) f(); else g(); |