Правила программирования на С и С++. Главы 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(); 

Первые два выражения опираются на тот факт, что вычисления в выражении с использованием операций ?? и || гарантированно осуществляются слева направо и прекращаются сразу, как только устанавливается истина или ложь. Возьмем для примера выражение a ?? f(). Если a ложно, то тогда не важно, что возвращает f(), так как все выражение ложно, если любой из его операндов значит ложь. Следовательно, компилятор никогда не вызовет f(), если a ложно, но он должен вызвать f(), если a истинно. То же самое применимо и к b, но здесь f() вызывается, если b, напротив, ложно.