воскресенье, 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: может быть это все прописные истины. Но некоторые и их не знают, да и не грех лишний раз повторить любому. :)