Макрос с произвольными параметрами
Использовать макроопределения иногда удобно. Во многих случаях, конечно, стоит предпочесть использование параметризованных функций (шаблонов) и других механизмов, обеспечивающих проверку типов. Но использование препроцессора и макросов также имеет свою сферу применения.
Если USE_MY_TRACE не определено, вызовы CallTrace просто будут исключены на уровне препроцессора, а оптимизация при компоновке просто не включит нигде теперь не используемую функцию CallTrace в конечный исполняемый код программы. Удобно, но...
Обратим внимание на семейство макросов TRACE0, TRACE1, TRACE2, TRACE3, объявленных в MFC. Невозможность обойтись одним универсальным макросом объясняется следующими правилами синтаксиса:
1. При объявлении нескольких макросов с одинаковым именем препроцессор использует последнее определение и выводит предупреждения на этапе трансляции.
2. Следует из первого - в отличие от функций, макросы с одинаковыми именами, но различным числом или типом параметров, недопустимы.
3. Синтаксическая запись произвольного числа параметров (многоточие, '...') для макроопределений недопустима и является синтаксической ошибкой.
А теперь объявим макроопределение:
И, наконец, пример использования:
заменяется препроцессором на вызов
Т.е. вызываются конструктор и соответствующий «оператор скобки». Можно записать так:
Заметим, что вызывается тот «оператор скобки», который соответствует типу и количеству аргументов – соответственно, для типов float и int разные при внешне одинаковом вызове одного и того же макроса:
Однако если компилятор не поддерживает __noop или нечто аналогичное, можно просто определять неиспользуемый макрос как «пустой» или как ((void)0):
или
2. Сам вызов конструктора тоже может быть использован для дополнительных аргументов. Например:
Теперь определим макрос:
Макрос теперь автоматически получает номер строки вызова.
И если его использовать где-нибудь в программе:
то мы получим что-то вроде следующего:
Line: 10 Message: My message
Замечу, что кроме __LINE__, определены также
__DATE__, __FILE__ и многое другое, и что особенно ценно на мой взгляд, __FUNCTION__. Замечу, что __FUNCTION__ работает удивительно корректно, возвращая имя класса и имя метода, разделенных '::'. Причем всё вышеназванное работает и для release версии, открывая прекрасные возможности для трассировки и доводки.
Если объявить в следующей форме:
то это уже другая техника и другой случай, то есть это уже не вызов «оператора скобки», на котором всё и базируется.
2. В классе MacroCall показан пример использования произвольного числа аргументов и форматирования при помощи vsprintf. Подробно обьяснять этот фрагмент я не буду, поскольку эти функции подробно описаны в MSDN.
Макроопределения и их недостатки
Например, функции отладки и трассировки. Согласитесь, что это довольно удобно: #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); |
Краткое обьяснение
Всё очень просто. Строка вызова макроопределения MYMACRO(55); |
MacroCall()(55); |
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 |
#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__) |
Макрос теперь автоматически получает номер строки вызова.
И если его использовать где-нибудь в программе:
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.