Страница 89 из 93 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, напротив, ложно. |