Ознакомьтесь с нашей политикой обработки персональных данных
23:02 

Макросы в Си/Си++: голактеко опасносте

zHz00
Описаны грабли, касающиеся макроопределений (#define) в языках Си/Си++.


Значит, макросы бывают с параметрами и без параметров. Без параметров это так:

#define IDENTIFIER REPLACE_TEXT

Всё. В тексте теперь все идентификаторы IDENTIFIER заменяются на REPLACE_TEXT. Разумеется, препроцессор достаточно умён, чтобы не заменять "IDENTIFIER" на "REPLACE_TEXT", а также не заменять IDENTIFIER1334 на REPLACE_TEXT1334. Т.е. считаются только отдельные лексемы. Названия идентификаторов для замены рекомендуют писать большими буквами, чтобы видно было, где тут макрос. Потому что с макросами можно попасть в биду.

С параметрами -- поинтереснее. Классический пример:

#define SQR(X) (X)*(X)

Теперь везде в тексте SQR(*) будет заменена на то, что указано. То есть вместо аргумента будет подставлен его квадрат. Например:

SQR(2) → (2)*(2)
SQR(a) → (a)*(a)
SQR(pData->m_nID+printf("3")+getchar()) → (pData->m_nID+printf("3")+getchar())*(pData->m_nID+printf("3")+getchar())

В последнем случае printf("3") и getchar() вызовутся по два раза. Этот эффект тоже следует учитывать.

Вроде всё круто, но мы почему-то написали параметр икс в скобках? Почему? Обязательно ли так писать? Формально -- нет. Вполне можно писать и так:

#define SQR(X) X*X

Но такой макрос может не всегда работать верно. Например:

SQR(2+2) → 2+2*2+2

Приоритет умножения выше, поэтому сначала перемножатся две двойки, а потом ещё две двойки добавятся. Итого: 8. А 4 в квадрате это 16. Поэтому аргументы всегда надо в выражении для замены указывать в скобках. Тогда бы вычислялось выражение (2+2)*(2+2), т.е. 16.

Замечу, что все подстановки осуществляются чисто текстово и происходит это ещё до непосредственной компиляции.

Какие ещё есть подводные камни? Далеко ходить не надо, обнаружил вчера.

Программа не работает. Симптомы: программа вылетает с Access Violation, если запущена без отладчика и в Release верии, иначе всё ок (почему иначе всё ок -- так и не понял).

Косвенные проверки показали, что происходит всё из-за выхода индекса за границы массива. Увеличил на 1 размер одного из массивов, проблема исчезла. Ну ок. Ищем теперь источник.
Написал функцию _check и добавил её во все места, где у массивов использовалась индексация. Т.к. ошибка проявлялась только без отладчика, пришлось вот так извращаться.

inline _check(int expr, int max, int t, int l)
{
char s[40];
sprintf("%d>=%d (LN %d, CV %d)",expr,max,l,t);
if(expr>=max)
AfxMessageBox(s);
return expr;
}

#define _C(x,y,z) _check((x),(y),(z),__LINE__)

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

В программе массив был одномерным, но эмулировал двухмерный, т.к. ему вместо индексов подавались значения вида x*n+y. Которые были заменены на _C(x*n+y,ARRAY_SIZE,x). Вместо x по очереди подставлялись другие переменные, чтобы узнать, чему они равны.

Оказалось, что CV -- control value для разных переменных в совокупности дают значения в пределах массива (т.е. всё ок), однако итог вычисления выражения x*n+y -- ЗА границами. То есть, если значение выражения считать на бумажке по тем значениям переменных, что выдаётся в сообщении, результат отличается от посчитанного компьютером. Нипанятна. И тут я замечаю макрос SWAP (ну да, большими буквами же имя!). Иду смотреть, что это за зверь. А он такой:

#define SWAP(a,b) tempr=(a);(a)=(b);(b)=tempr

Видимо, меняет значения двух переменных местами. Вроде ок, да? И аргументы в скобочках. Но несколько операторов в одну строку. И используется переменная tempr (это ограничивает применимость макроса функциями, где эта переменная об'явлена). Пишу ему замену inline-функцией. Ошибка пропадает. Да, дело было в макросе!
А потом смотрю как он применяется. Там что-то вроде этого:

for(x=0;x<N;x++)//чему равно N -- значения не имеет
SWAP(array[expr1],array[expr2]);

Раскроем определение макроса:

for(x=0;x<N;x++)//чему равно N -- значения не имеет
tempr=(array[expr1]);(array[expr1])=(array[expr2]);(array[expr2])=tempr;

А чтобы было совсем понятно, что происходит, сделаем правильные отступы:

for(x=0;x<N;x++)//чему равно N -- значения не имеет
tempr=(array[expr1]);
(array[expr1])=(array[expr2]);
(array[expr2])=tempr;

То есть, внутри цикла исполняется только первый оператор макроопределения (замена текстовая, фигурных скобок нет)! Остальные исполняются один раз после цикла (где и происходит выход за границы). Как же решить эту проблему? Можно поставить циклу операторные скобки. Можно заменить макрос встраиваемой функцией. Но можно и исправить сам макрос. Стандартный трюк тут такой:

#define SWAP(a,b) do{tempr=(a);(a)=(b);(b)=tempr;}while(0)

Видите, мы добавили операторные скобки вокруг макроса. Теперь при любом раскладе он будет выполняться неделимо. Скобки эти использует цикл с постусловием, а это значит, что он будет выполнен минимум один раз. Заведомо ложное условие означает, что второй раз он выполнен не будет. Это то, что нам надо. Теперь при раскрытии получается:

for(x=0;x<N;x++)//чему равно N -- значения не имеет
do{tempr=(array[expr1]);(array[expr1])=(array[expr2]);(array[expr2])=tempr;}while(0);

А с отступами:

for(x=0;x<N;x++)//чему равно N -- значения не имеет
do
{
tempr=(array[expr1]);
(array[expr1])=(array[expr2]);
(array[expr2])=tempr;
}while(0);

Внутри параметрического цикла ровно один оператор -- это цикл с пост-условием.

TL;DR:
1) В Си++ есть inline-функции. Они лучше макросов и полностью их заменяют. Лучше использовать их. В Си придётся использовать макросы;
2) При использовании макросов их имена следует писать большими буквами;
3) Аргументы в параметрических макросах в выражении-подстановке писать надо в скобках;
4) Если несколько операторов в макросе идут подряд, их нужно оформить в операторные скобки, например в цикл do-while;
5) Подстановка осуществляется чисто текстово, поэтому если аргумент содержит вызов функции и в выражении-подстановке присутствует несколько раз, функция тоже будет вызвана несколько раз. А в inline-функциях этого не произойдёт.

@темы: Борьба с техникой, Программирование, Статьи

URL
Комментарии
2014-08-05 в 16:03 

1) В Си++ есть inline-функции. Они лучше макросов и полностью их заменяют. Лучше использовать их. В Си придётся использовать макросы;

Справедливости ради следует отметить, что в C99 ключевое слово таки добавили.

URL
   

Untitled

главная