Макрос с произвольными параметрами

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

Макроопределения и их недостатки

Например, функции отладки и трассировки. Согласитесь, что это довольно удобно:
#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.