Вы должны быть всегда способны заменить макрос функцией
Это вариант для макросов правила "не нужно неожиданностей (без сюрпризов) ". Что-то, похожее внешне на функцию, должно действовать подобно функции, даже если это на самом деле макрос. (По этой причине я иногда предпочитаю записывать имя макроса заглавными буквами не полностью, если его поведение сильно напоминает функцию. Хотя я всегда использую все заглавные, если у макроса есть побочные эффекты). При этом возникает насколько вопросов.
Во-первых, макрос не должен использовать переменные, не передаваемые в качестве аргументов. Вот наихудший из возможных способов в таких обстоятельствах:
Следующий код находится в заголовочном файле:
#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 two_things() a();b()
if( x )
two_things();
else
something_else();
который будет расширен следующим образом (тут я переформатировал, чтобы сделать происходящее неприятно очевидным):
if ( x )
a();
b();
else
something_else();
Вы получаете сообщение об ошибке "у else отсутствует предшествующий оператор if". Вы не можете решить эту проблему, используя лишь фигурные скобки. Переопределение макроса следующим образом:
#define two_things() { a(); b(); }
вызовет такое расширение:
if( x )
{
a();
b();
}
;
else
something_else();
Эта вызывающая беспокойство точка с запятой — та, что следует после two_things() в вызове макроса. Помните, что точка с запятой сама по себе является законным оператором в Си. Она ничего не делает, но она законна. Вследствие этого else пытается связаться с этой точкой с запятой, и вы получаете то же самое "у else отсутствует предшествующий оператор if".
Не нужно говорить, что, несмотря на то, что макрос выглядит подобно вызову функции, его вызов может не сопровождаться точкой с запятой. К счастью, для этой проблемы имеется два настоящих решения. Первое из них использует малоизвестный оператор "последовательного вычисления" (или запятую):
#define two_things() ( a(), b() )
Эта запятая — та, что разделяет подвыражения в инициализирующей или инкрементирующей частях оператора for. (Запятая, которая разделяет аргументы функции, не является оператором последовательного вычисления). Оператор последовательного вычисления выполняется слева направо и получает значение самого правого элемента в списке (в нашем случае значение, возвращаемое b()). Запись:
x = ( a(),b() );
означает просто:
a();
x = b();
Если вам все равно, какое значение имеет макрос, то вы можете сделать нечто подобное, используя знак плюс вместо запятой. (Выражение:
a()+b();
в отдельной строке совершенно законно для Си, где не требуется, чтобы результат сложения был где-нибудь сохранен). Тем не менее, при знаке плюс порядок выполнения не гарантируется; функция b()
может быть вызвана первой. Не путайте приоритеты операций с порядком выполнения. Приоритет просто сообщает компилятору, где неявно размещаются круглые скобки. Порядок выполнения вступает в силу после того, как все круглые скобки будут расставлены по местам. Невозможно добавить дополнительные скобки к ((a())+(b())). Операций последовательного вычисления гарантированно выполняется слева направо, поэтому в нем такой проблемы нет.
Я должен также отметить, что операция последовательного вычисления слишком причудлива, чтобы появляться в нормальном коде. Я использую ее лишь в макросах, сопровождая все обширными комментариями, объясняющими, что происходит. Никогда не используйте запятую там, где должна быть точка к запятой. (Я видел людей, которые делали это, чтобы не использовать фигурные скобки, но это страшно даже пересказывать).
Второе решение использует фигурные скобки, но с одной уловкой:
#define two_things() \
do \
{ \
a(); \
b(); \
} while( 0 )
if( x )
two_things();
else
something_else();
что расширяется до:
if( x )
do
{
a();
b();
} while ( 0 ) ; // == точка с запятой связывается с
// оператором while ( 0 )
else
something_else();
Вы можете также попробовать так:
#define two_things() \
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(); |
Первые два выражения опираются на тот факт, что вычисления в выражении с использованием операций и ||
гарантированно осуществляются слева направо и прекращаются сразу, как только устанавливается истина или ложь. Возьмем для примера выражение a f(). Если a
ложно, то тогда не важно, что возвращает f(), так как все выражение ложно, если любой из его операндов значит ложь. Следовательно, компилятор никогда не вызовет f(),
если a
ложно, но он должен вызвать f(),
если a истинно. То же самое применимо и к b, но здесь f()
вызывается, если b, напротив, ложно.