Правила программирования на Си и Си++

       

Вы должны быть всегда способны заменить макрос функцией


Это вариант для макросов правила "не нужно неожиданностей (без сюрпризов) ". Что-то, похожее внешне на функцию, должно действовать подобно функции, даже если это на самом деле макрос. (По этой причине я иногда предпочитаю записывать имя макроса заглавными буквами не полностью, если его поведение сильно напоминает функцию. Хотя я всегда использую все заглавные, если у макроса есть побочные эффекты). При этом возникает насколько вопросов.

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

Следующий код находится в заголовочном файле:

#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, напротив, ложно.

Содержание раздела