Использовать макроопределения иногда удобно. Во многих случаях, конечно, стоит предпочесть использование параметризованных функций (шаблонов) и других механизмов, обеспечивающих проверку типов. Но использование препроцессора и макросов также имеет свою сферу применения.
Макроопределения и их недостаткиНапример, функции отладки и трассировки. Согласитесь, что это довольно удобно: #ifdef USE_MY_TRACE #define TRACE(a) CallTrace(a) #else #define TRACE(a) ((void)0) #endif
| Если USE_MY_TRACE не определено, вызовы CallTrace просто будут исключены на уровне препроцессора, а оптимизация при компоновке просто не включит нигде теперь не используемую функцию CallTrace в конечный исполняемый код программы. Удобно, но... Обратим внимание на семейство макросов TRACE0, TRACE1, TRACE2, TRACE3, объявленных в MFC. Невозможность обойтись одним универсальным макросом объясняется следующими правилами синтаксиса:
1. При объявлении нескольких макросов с одинаковым именем препроцессор использует последнее определение и выводит предупреждения на этапе трансляции.
2. Следует из первого - в отличие от функций, макросы с одинаковыми именами, но различным числом или типом параметров, недопустимы.
3. Синтаксическая запись произвольного числа параметров (многоточие, '...') для макроопределений недопустима и является синтаксической ошибкой.
Умные макроопределенияОказывается, преодолеть названные выше недостатки макросов совершенно несложно. Сделать это можно при помощи простого трюка - использования класса, чем-то напоминающего так называемую идиому функторов. Это класс, для которого определен набор операторов "скобки". Итак, например, вот такой класс: class MacroCall { public:
MacroCall() { }
void operator()(float val) const { printf("Float: %f\r\n", val); }
void operator()(int val) const { printf("Integer: %d\r\n", val); }
void operator() (const char *pszFmt, ...) const { if ( pszFmt == NULL || *pszFmt == 0 ) return;
va_list args; va_start(args, pszFmt);
int size_msgbuf = _vscprintf(pszFmt, args) + 1; char* msgbuf = new char[size_msgbuf]; vsprintf(msgbuf, pszFmt, args);
printf(msgbuf);
delete[] msgbuf; va_end(args); } };
| А теперь объявим макроопределение: #ifdef USE_MACRO #define MYMACRO MacroCall() #else #define MYMACRO __noop #endif
| И, наконец, пример использования: MYMACRO("%s : %d\r\n", "Value", 10); MYMACRO(55); MYMACRO(3.1415926f);
|
Краткое обьяснениеВсё очень просто. Строка вызова макроопределения заменяется препроцессором на вызов Т.е. вызываются конструктор и соответствующий «оператор скобки». Можно записать так: MacroCall().operator()(55);
| Заметим, что вызывается тот «оператор скобки», который соответствует типу и количеству аргументов – соответственно, для типов float и int разные при внешне одинаковом вызове одного и того же макроса: MYMACRO(55); // вызван operator()(int val) MYMACRO(3.1415926f); // вызван operator()(float val) // operator() (const char *pszFmt, ...) MYMACRO("%s : %d\r\n", "Value", 10);
|
Дополнительные замечания1. Обратим внимание на то, что в случае, если макрос MYMACRO не используется, он заменяется на __noop, специально введенный в компиляторе от Microsoft. Согласно документации, он позволяет компилятору правильно «проигнорировать» ненужный теперь список аргументов вызова при произвольном числе аргументов. Однако если компилятор не поддерживает __noop или нечто аналогичное, можно просто определять неиспользуемый макрос как «пустой» или как ((void)0):
или #define MYMACRO ((void)0)
|
2. Сам вызов конструктора тоже может быть использован для дополнительных аргументов. Например: class MacroCallLine { public:
MacroCallLine(int L) : line_num(L) { }
void operator()(const char* msg) const { printf("Line: %d Message: %s\r\n", line_num, msg); }
protected: int line_num; };
| Теперь определим макрос: #define TRACEMSG MacroCallLine(__LINE__)
| Макрос теперь автоматически получает номер строки вызова. И если его использовать где-нибудь в программе: то мы получим что-то вроде следующего: Line: 10 Message: My message
Замечу, что кроме __LINE__, определены также __DATE__, __FILE__ и многое другое, и что особенно ценно на мой взгляд, __FUNCTION__. Замечу, что __FUNCTION__ работает удивительно корректно, возвращая имя класса и имя метода, разделенных '::'. Причем всё вышеназванное работает и для release версии, открывая прекрасные возможности для трассировки и доводки.
И напоследок 1. Еще раз хочу обратить внимание: объявление макроса – это вызов конструктора, возможно с параметрами. Сам макрос не предполагает передачу аргументов. Например, был объявлен макрос в виде: #define MYMACRO MacroCall()
| Если объявить в следующей форме: #define MYMACRO(p) MacroCall(p) // ЭТО УЖЕ ДРУГОЙ ВЫЗОВ
| то это уже другая техника и другой случай, то есть это уже не вызов «оператора скобки», на котором всё и базируется.
2. В классе MacroCall показан пример использования произвольного числа аргументов и форматирования при помощи vsprintf. Подробно обьяснять этот фрагмент я не буду, поскольку эти функции подробно описаны в MSDN. |