воскресенье, 7 сентября 2008 г.

О макросах бедных замолвите слово...

Макросы конечно опасны. Так же, как опасны и многие другие, весьма повседневные вещи. И всем понятно, что нет никакой необходимости применять макросы препроцессора в C++. Но здесь речь пойдет не о C++, хотя во второй части, посвященной рефакторингу, я немного коснусь и его. Совсем чуть-чуть. Мог бы даже и не говорить об этом во вступлении.

Но если говорить о C, то в некоторых случаях без макросов просто никуда...

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

Проблема возникает тогда, когда типы заранее неизвестны. Обычно это указывает на весьма широкое использование какой либо мелкой функции, типа max. В C++ для этого дела обычно применяют шаблонные функции. Но си не имеет шаблонов. В си, для таких случаев, альтернативы макросам просто нету.

Но при программировании макросов есть много подводных камней. Они многократно описаны, переписывать это я не буду. Скажу лишь что лучше всего избегать любых побочных эффектов в макросах. Макросы должны вести себя как соответствующие функции. Это упростит понимание и поддержку.

За исключением одного момента. Есть в препроцессоре две переменных - __FILE__ и __LINE__. Использование их в инлайн функции сводит смысл их использования практически к нулю. Поэтому я позволяю себе использовать макроопределение, которое помимо параметров использует эти две переменных, но привязывает их к месту вызова макроса. Хотя с другой стороны это трудно назвать побочным эффектом, поскольку состояние выполнения никак не меняется. Но если попытаться заменить макрос на функцию, то соответствующая информация просто не будет доступна.

Вот совсем недавно ковырялся тут со своим проектам. У меня там широко используются утверждения (asserton). Я конечно стараюсь чтобы выражения в утверждениях были понятны, но в отрыве от контекста они обычно ни о чем не говорят. Поэтому я дополнил утверждения текстовой строкой, которая выводится в случае нарушения. После этого я заметил один момент.

Выражение assert(x < y) содержит в себе условие допустимости выполнения. И это вполне адекватно воспринимается. Но если попытаться дополнить его текстовой строкой, которая должна отображаться в случае нарушения условия - assert(x < y, "слишком большой x"), То все становится с ног на голову. Почему же x большой, если он меньше y? Долго думал во что мне переименовать assert, чтобы можно было использовать обратную логику утверждений, но потом бросил это дело, ибо assert - это просто утверждение.

Но логику выражений я твердо решил перевернуть (мои утверждения носят название STUB_ASSERT, но это почти ничего не меняет). А поскольку недвано начитался Фаулера, то делать это начал по всем правилам. :)

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

Старый макрос я хотел бы объявить устаревшим, я всегда так делаю с функциями, чтобы обнаружить места где он используется, но есть проблема. В препроцессоре почти невозможно объявить макроопределеение устаревшим. Поэтому я превратил его в функцию, при этом я потерял возможность видеть файл/строку сбоя, но поскольку я всеравно собирался его удалять - мне это всеравно.

После чего я, долго и нудно, менял все функции на новый вариант, паралельно переворачивая логику утверждений. Поскольку функция использовалась широко - это была долгая работа, но поскольку старая функция существовала независимо от новой - я мог производить эту модификацию поэтапно. После того, как старый вариант исчез из обращения - я просто взял с помощью sed переименовал все новые функции на старые: sed -i -e 's/REAL_ASSERT/STUB_ASSERT/g'. И получил старое имя макроса и новую логику его использования. Все встало на свои места и стало логичным. Теперь главное не начать использовать новую логику по старому. :)

А теперь немного про рефакторинг. Большинство методов рефакторинга ориентируются исключительно на объектность. Но если смотреть свысока, то можно заметить что си имеет модули. Каждый модуль имеет свое состояние - свои статические переменные. Которые так же как и в C++ стоит делать приватными - статическими. Правило сокрытия информации распространяется на модули си практически так же как и на классы. То есть все, что не экспортируется лучше делать статическим. Это, просто-напросто, уменьшает количество сущностей, взаимоотношения которых нужно отслеживать программисту. Видя, что функция приватная/статическая мы сразу же, без лишних умственных напряжений, понимаем где она может быть использована. Рефакторинг в си может выполняться путем перемещения функций между модулями или например делением одного модуля на несколько по смыслу функций.

И вот еще один момент. Выделение метода в C++ происходит обычно в тот же класс. В си выделение метода будет происходить в модуль. Новая функция, естественно, имеет локальную область видимости - то есть объявляется как статик. Такое можно применять даже в C++ если например стоит задача минимизировать количество изменений, но для исправления ошибки требуется вынести часть кода из функции, или просто добавить в функцию некоторое новое поведение, не меняя при этом описание класса, которое обычно располагается в другом файле. В дальнейшем, при проведении планового рефакторинга, эту функцию легко можно включить в объект.

PS: может быть это все прописные истины. Но некоторые и их не знают, да и не грех лишний раз повторить любому. :)

6 коммент.:

Анонимный комментирует...

Макросы конечно опасны. Так же, как опасны и многие другие, весьма повседневные вещи
Да, хорошо сказал :-) Сразу приходит на ум кухонный нож... Не умеешь - старайся не пользоваться.

Есть в препроцессоре две переменных - __FILE__ и __LINE__
Макросы. Я бы понял, если бы сказал это про __func__ (__FUNCTION__ в MSVC), все-таки это не макрос...


Да, хорошая статья. Тема про assert заставила меня сильно задуматься.

mega комментирует...

макросы __FILE__ и __LINE__ в функциях можно использовать точно так же как и в препроцессоре, причем без особых проблем:

#define my_func( p1, p2 ) __my_func( p1, p2, TEXT( __FILE__ ), __LINE__ )

void __my_func( int p1, int p2, LPCTSTR lpszFile, int Line ){
...
}
просто конечный вызов будет через макрос my_func, это нормальная практика

Андрей Валяев комментирует...

Дык это и есть использование в макросе :)

Если ты напишешь
void __my_func( int p1, int p2, LPCTSTR lpszFile = __FILE__, int Line = __LINE__) {}

Это тоже будет работать, но только совсем не так, как хочется :)

Собственно так я и написал в посте.

mega комментирует...

>>Это тоже будет работать, но только совсем не так, как хочется :)

это разные вещи: параметры по-умолчанию будут ссылаться на место, где они были объявлены, а макрос - на место, где он был вызван, т.е. - как раз там где надо, для чего он и был написан

Андрей Валяев комментирует...

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

Это вынуждает применять макрос. Который разворачивается в указанном месте, обеспечивая правильные значения дефайнов. Тем самым ведет себя совершенно ни как функция, что собственно не есть гуд.

То есть желание сделать макросы работающими и выглядящими как функции разбивается о камни позиционнозависимых макроопределений. :)

Вот где-то такая мысль была.

mega комментирует...

понятно, меня просто возмутила такая категоричность: "сводит смысл их использования практически к нулю"

проблема-то, как я понимаю, в том, что функция под макросом не раскроет свои параметры, или какие-то мета данные автодокументации, например: комментарии или названия параметров, что конечно накрывает множество подсказок, которым мы выделяем особое место, тем более при рефакторинге, человеку это свойственно, он же не машина

только ведь это болезнь не конкретно макросов

у нас просто еще недостаточно таких инструментов, позволяющих смотреть в контекст самого кода,
а это уже несколько другая тема

нет абсолютной вины архитектора в том, что сборка его изделия была некачественно произведена :)