× К оглавлению На главную Об авторе

Дата и время публикации :

Дата и время модификации:

Или как сделать вычисления в макросах безопасными


1. Что это такое

Оператор-выражение (expression statement) представляет собой составное выражение, состоящие из операторов и иных выражений, обрамленных скобками, которые тем самым позволяют объединять в одно выражение операторы циклов, множественного выбора switch и локальные переменные с последующим присвоением вычисленного значения.

2. Использование

2.1 Оператор-выражение обрамляется фигурными скобками с последующим окаймлением круглыми (строки 6,7 листинга 1), что позволяет его использовать непосредственно в тексте программы или через директиву #define (строка 5 листинга 1).

Листинг 1.Файл stmt_expr_max.c

 1 #include <stdio.h> 
 2 
 3 #define COMP_MAX(a,b) ((a) > (b) ? (a) : (b))  /* UNSAFE */
 4
 5 #define STMT_EXPR_COMP_MAX(a,b)  		    /* SAFE */ \
 6         ({ int _a =(a), _b=(b), _z;                         \
 7	   _z = COMP_MAX((_a),(_b)); _z; }) 
 8  
 9 int main (void)
10 {
11    int n = 1;
12    int retvalue = STMT_EXPR_COMP_MAX(++n,++n);
13
14    printf ( "retvalue:= STMT_EXPR_COMP_MAXIMUN(%d,%d)=> %d\n", n-1, n, retvalue );	  
15
16    return retvalue;
17 }

При этом, само выражение делится на несколько частей с использованием оператора-разделителя (;), в последнем — возвращается значение операнда (_z) в качестве результирующего значения всей конструкции.

В обратном случае, если возвращаемое значение отсутствует, выражение фактически относится к типу void и соответствует другим видам выражений, обрамленных фигурными скобками, например при использовании метода оптимизации поглощения точек с запятой (Swallowing the Semicolon), как показано в листинге 1а.

Примечание В процессе сравнения операнда и переменной было установлено, что в операторах-выражения не только возвращаемые значения сохраняются в операндах, но и объявляемые в его начале, содержащие значения передаваемых аргументов, как показано в строке 6 листинга 1

Листинг 1a.Пример из учебника

 1
 2 #define SKIP_SPACES(p, limit)     \
 3  { char *lim_p = (limit);         \
 4    while (p < lim_p) {            \
 5    if (*p++ != ' ') {             \
 6      p--; break; }}}
 7
 8 if (*p != 0)
 9 SKIP_SPACES (p, lim);
10 else …

В тоже время, как показано в листинге 1, оператор-выражение так же может использоваться для устранения побочных эффектов(side effects), к которым приводят «небезопасные», функциональные (function-like — функции-подобного) макроопределения, такие как COMP_MAX().

Для этого, осуществляется подстановка в строке 7 объявленного в строке 3 макроса COMP_MAX(). Тем самым устраняется неопределенное поведение (undefined behavior) в точке использования макроса COMP_MAX(), которое заключается в двойной модификации одной переменной n в строке 12 с непредвиденным результатом – вместо ожидаемой двойки на выходе будет тройка, как показано в листинге 1б.

Листинг 1бСборка и выполнение файла stmt_expr_max.c

[user@home] ~$ gasrunparts gnuc_stmt_expr_max
Compile "/usr/local/share/gasrunparts/examples/stmt_expr_max.c" in dir "/home/user/Проекты/Gasrunparts"
Running "stmt_expr_max" in dir "/home/user/Проекты/Gasrunparts"
invalue: 1 => STMT_EXPR_COMP_MAXIMUN(2,3)=> 3
done.

В тоже время, как показано в листинге 1, оператор-выражение так же может использоваться для устранения побочных эффектов(side effects), к которым приводят "небезопасные", функциональные (function-like — функции-подобного) макроопределения, такие как COMP_MAX(). Для этого, осуществляется подстановка в строке 7 объявленного в строке 3 макроса COMP_MAX(). Тем самым устраняется неопределенное поведение (undefined behavior) в точке использования макроса COMP_MAX(), которое заключается в двойной модификации одной переменной n в строке 12 с непредвиденным результатом – вместо ожидаемой двойки на выходе будет тройка, как показано в листинге 1б.

2.2 Для наглядности рассмотрим пример отдельного использования макроса COMP_MAX() в листинге 2.

Листинг 2.Файл unsafe_fmacros_max.c

 1 #include <stdio.h> 
 2 
 3 #define COMP_MAX(a,b) ((a) > (b) ? (a) : (b))  /* UNSAFE */
 4  
 5 int main (void)
 6 {
 7    int n = 1;
 8    return COMP_MAX(++n,++n);
 9 }

После подстановки COMP_MAX() в строке 8, реализуется код, представленный в листинге 2а.

Листинг 2а.Результат подстановки макроса COMP_MAX()

 8    return ((++n) > (++n)        ?      (++n) :  (++n));
                 ^(a)=2  ^(b)=3    ^EQPТ     ^(a)=2   ^(b)=3

Как следствие, «раскрытый» код, результата подстановки функционального макроса COMP_MAX(), который имеет два ветвления, разделенного точкой следования (equence point–EQPT), в роли которой выступает операнд (?).

Примечание 1. Стандарт С определяет порядок в котором код программы имеет ветвление по точкам следования, которые упорядочивают выполняемые части программы – на исполняемые до встречи с точкой следования и на те, которые будут выполнены после неё. 2. Встреча с точкой следования случится после вычисления всего выражения, но в тоже время не являющегося частью огромного выражения, и нахождения первого операнда (&&) ,(||) ,(?), (:) или оператора точки с запятой (;), но до вызова функции (после вычисления её аргументов и разметки её выражений) и некоторых других частей. 3. Соответственно, все побочные эффекты для выполненного куска кода уже случились, а для исполняемого после точки следования ещё нет. 4. Для выявления побочных эффектов до встречи с точкой следования следует использовать опцию -Wsequence-point совместно с -Werror компилятора GCC. 5. Функциональные макросы, приводящие к побочным эффектам неопределенного поведения, должны помечаться в комментариях как /*UNSAFE*/. Соответственно, те которые не имеют их можно помечать в комментариях /*SAFE*/.

Поэтому переменная n модифицируется дважды, но никак не четырежды, во время сравнения операндов (а) и (b) в первой части, и второй, которую ещё предстоит выполнить.

Следовательно побочный эффект выражается в виде неопределенного состояния переменной n, что иллюстрируется выводом на печать результатов компиляции файла unsafe_fmacros_max.c, как показано ниже.

[root@home] ~$ gasrunparts gnuc_unsafe_macros_max
Compile "/usr/local/share/gasrunparts/examples/unsafe_fmacros_max.c" in dir "/home/user/Проекты/Gasrunparts"
/usr/local/share/gasrunparts/examples/unsafe_fmacros_max.c: In function ‘main’:
/usr/local/share/gasrunparts/examples/unsafe_fmacros_max.c:8:30: error: operation on ‘n’ may be undefined [-Werror=sequence-point]
 /*8*/    return COMP_MAX(++n,++n);
                              ^
/usr/local/share/gasrunparts/examples/unsafe_fmacros_max.c:3:37: note: in definition of macro ‘COMP_MAX’
 /*3*/ #define COMP_MAX(a,b) ((a) > (b) ? (a) : (b))  /* UNSAFE */
                                     ^
cc1: all warnings being treated as errors
error: can't compile THIS!
Failure

3. Ограничения

3.1 Нельзя встраивать операторы-выражения в константные, как к таковым относятся перечисляемые константы, задаваемые оператором enum, размерности битовых полей и начальных значений статических переменных.

4. Библиография


4.1 GNU C Reference. Statements and Declarations in Expressions

4.2 GNU C Reference. Function-like Macros

4.3 GNU C Reference. Operator Precedence Problems

4.4 GNU C Reference. Warning Options

4.5 GNU C Reference. Frequently Reported Bugs

4.6 АЛЁНА C++. ПРОГРАММИРОВАНИЕ ДЛЯ ПРАГМАТИКОВ. Точки следования (equence point)

4. GNU C Reference. Swallowing the Semicolon