Использовать макроопределения иногда удобно. Во многих случаях, конечно, стоит
предпочесть использование параметризованных функций (шаблонов) и других
механизмов, обеспечивающих проверку типов. Но использование препроцессора и
макросов также имеет свою сферу применения. Например, функции отладки и
трассировки. Согласитесь, что это довольно удобно:
Если
USE_MY_TRACE не определено, вызовы CallTrace просто будут исключены на уровне
препроцессора, а оптимизация при компоновке просто не включит нигде теперь не
используемую функцию CallTrace в конечный исполняемый код программы. Удобно,
но... Обратим внимание на семейство макросов TRACE0, TRACE1, TRACE2, TRACE3,
объявленных в MFC. Невозможность обойтись одним универсальным макросом
объясняется следующими правилами синтаксиса:
1. При объявлении нескольких
макросов с одинаковым именем препроцессор использует последнее определение и
выводит предупреждения на этапе трансляции.
2. Следует из первого - в
отличие от функций, макросы с одинаковыми именами, но различным числом или типом
параметров, недопустимы.
3. Синтаксическая запись произвольного числа
параметров (многоточие, '...') для макроопределений недопустима и является
синтаксической ошибкой.
Умные
макроопределения.
Оказывается, преодолеть названные выше
недостатки макросов совершенно несложно. Сделать это можно при помощи простого
трюка - использования класса, чем-то напоминающего так называемую идиому
функторов. Это класс, для которого определен набор операторов "скобки". Итак,
например, вот такой класс:
Т.е.
вызываются конструктор и соответствующий «оператор скобки». Можно записать
так:
MacroCall().operator()(55);
Заметим,
что вызывается тот «оператор скобки», который соответствует типу и количеству
аргументов – соответственно, для типов float и int разные при внешне одинаковом
вызове одного и того же макроса:
1. Обратим внимание на то, что в случае, если макрос
MYMACRO не используется, он заменяется на __noop, специально введенный в
компиляторе от Microsoft. Согласно документации, он позволяет компилятору
правильно «проигнорировать» ненужный теперь список аргументов вызова при
произвольном числе аргументов. Однако если компилятор не поддерживает __noop
или нечто аналогичное, можно просто определять неиспользуемый макрос как
«пустой» или как ((void)0):
#define MYMACRO
или
#define MYMACRO ((void)0)
2.
Сам вызов конструктора тоже может быть использован для дополнительных
аргументов. Например:
Макрос
теперь автоматически получает номер строки вызова. И если его использовать
где-нибудь в программе:
TRACEMSG("My message");
то
мы получим что-то вроде следующего: Line: 10
Message: My message
Замечу, что кроме __LINE__, определены также __DATE__, __FILE__ и
многое другое, и что особенно ценно на мой взгляд, __FUNCTION__. Замечу, что __FUNCTION__ работает удивительно корректно,
возвращая имя класса и имя метода, разделенных '::'. Причем всё вышеназванное
работает и для release версии, открывая прекрасные возможности для трассировки и
доводки.
И
напоследок.
1. Еще раз хочу обратить внимание: объявление макроса
– это вызов конструктора, возможно с параметрами. Сам макрос не предполагает
передачу аргументов. Например, был объявлен макрос в виде:
#define MYMACRO MacroCall()
Если
объявить в следующей форме:
#define MYMACRO(p) MacroCall(p) // ЭТО УЖЕ ДРУГОЙ ВЫЗОВ
то
это уже другая техника и другой случай, то есть это уже не вызов «оператора скобки», на котором всё и
базируется.
2. В классе MacroCall показан пример использования
произвольного числа аргументов и форматирования при помощи vsprintf. Подробно
обьяснять этот фрагмент я не буду, поскольку эти функции подробно описаны в
MSDN.