Redirect to new edition!
Дата и время публикации:
Или как сделать вычисления в макросах безопасными
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а.
Листинг 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), в роли которой выступает операнд (?).
Поэтому переменная 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)