Как заставить макрос C ++ вести себя как функция? - PullRequest
46 голосов
/ 02 октября 2008

Допустим, по какой-то причине вам нужно написать макрос: MACRO(X,Y). (Предположим, есть веская причина, по которой вы не можете использовать встроенную функцию.) Вы хотите, чтобы этот макрос эмулировал вызов функции без возвращаемого значения.


Пример 1: Это должно работать как ожидалось.

if (x > y)
  MACRO(x, y);
do_something();

Пример 2. Это не должно приводить к ошибке компиляции.

if (x > y)
  MACRO(x, y);
else
  MACRO(y - x, x - y);

Пример 3: Это должно не компилироваться.

do_something();
MACRO(x, y)
do_something();

Наивный способ написания макроса таков:

#define MACRO(X,Y)                       \
cout << "1st arg is:" << (X) << endl;    \
cout << "2nd arg is:" << (Y) << endl;    \
cout << "Sum is:" << ((X)+(Y)) << endl;

Это очень плохое решение, которое не соответствует всем трем примерам, и мне не нужно объяснять, почему.

Игнорировать то, что на самом деле делает макрос, это не главное.


Теперь я чаще всего вижу написанные макросы, заключая их в фигурные скобки, например:

#define MACRO(X,Y)                         \
{                                          \
  cout << "1st arg is:" << (X) << endl;    \
  cout << "2nd arg is:" << (Y) << endl;    \
  cout << "Sum is:" << ((X)+(Y)) << endl;  \
}

Это решает пример 1, потому что макрос находится в одном блоке операторов. Но пример 2 не работает, потому что мы ставим точку с запятой после вызова макроса. Это заставляет компилятор думать, что точка с запятой является оператором сама по себе, что означает, что оператор else не соответствует никакому оператору if! И, наконец, пример 3 компилируется нормально, хотя точки с запятой нет, потому что блок кода не нуждается в точке с запятой.


Есть ли способ написать макрос, чтобы он прошел все три примера?


Примечание: я отправляю свой собственный ответ как часть принятого способа поделиться подсказкой , но если у кого-то есть лучшее решение, не стесняйтесь разместить его здесь, оно может получить больше голосов чем мой метод. :)

Ответы [ 9 ]

43 голосов
/ 02 октября 2008

Есть довольно умное решение:

#define MACRO(X,Y)                         \
do {                                       \
  cout << "1st arg is:" << (X) << endl;    \
  cout << "2nd arg is:" << (Y) << endl;    \
  cout << "Sum is:" << ((X)+(Y)) << endl;  \
} while (0)

Теперь у вас есть одна инструкция уровня блока, после которой должна стоять точка с запятой. Это ведет себя как ожидалось и желательно во всех трех примерах.

40 голосов
/ 02 октября 2008

Макросы, как правило, следует избегать; всегда предпочитайте им встроенные функции. Любой компилятор, достойный своей соли, должен иметь возможность вставлять небольшую функцию, как если бы это был макрос, а встроенная функция будет учитывать пространства имен и другие области видимости, а также оценивать все аргументы один раз.

Если это должен быть макрос, будет работать цикл while (уже предложенный), или вы можете использовать оператор запятой:

#define MACRO(X,Y) \
 ( \
  (cout << "1st arg is:" << (X) << endl), \
  (cout << "2nd arg is:" << (Y) << endl), \
  (cout << "3rd arg is:" << ((X) + (Y)) << endl), \
  (void)0 \
 )

(void)0 приводит к тому, что оператор оценивается как один из типов void, а использование запятых, а не точек с запятой, позволяет использовать его внутри оператора, а не только как отдельный. Я по-прежнему рекомендую встроенную функцию по множеству причин, наименьшей из которых является область действия и тот факт, что MACRO(a++, b++) будет увеличивать a и b в два раза.

17 голосов
/ 03 октября 2008

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

Ближайшее, о чем я знаю, это:

#define MACRO(X,Y) \
do { \
    auto MACRO_tmp_1 = (X); \
    auto MACRO_tmp_2 = (Y); \
    using std::cout; \
    using std::endl; \
    cout << "1st arg is:" << (MACRO_tmp_1) << endl;    \
    cout << "2nd arg is:" << (MACRO_tmp_2) << endl;    \
    cout << "Sum is:" << (MACRO_tmp_1 + MACRO_tmp_2) << endl; \
} while(0)

Это делает следующее:

  • Работает корректно в каждом из указанных контекстов.
  • Оценивает каждый из его аргументов ровно один раз, что является гарантированной функцией вызова функции (при условии, что в обоих случаях нет исключений ни в одном из этих выражений).
  • Действует на любые типы, используя «auto» из C ++ 0x. Это еще не стандартный C ++, но нет другого способа получить переменные tmp, требуемые правилом одиночной оценки.
  • Не требует, чтобы вызывающая сторона импортировала имена из пространства имен std, как это делает исходный макрос, но функция этого не делает.

Тем не менее, он по-прежнему отличается от функции тем, что:

  • В некоторых случаях недопустимого использования он может выдавать различные ошибки или предупреждения компилятора.
  • Плохо, если X или Y содержат значения 'MACRO_tmp_1' или 'MACRO_tmp_2' из окружающей области видимости.
  • Связанный с пространством имен std: функция использует свой собственный лексический контекст для поиска имен, тогда как макрос использует контекст своего сайта вызова. Там нет никакого способа написать макрос, который ведет себя как функция в этом отношении.
  • Его нельзя использовать в качестве выражения возврата функции void, которую может использовать выражение void (например, решение с запятой). Это еще более проблематично, когда желаемый тип возвращаемого значения не является пустым, особенно когда используется как lvalue. Но решение с запятыми не может включать использование объявлений, потому что они - операторы, поэтому выберите один или используйте расширение ({...}) GNU.
12 голосов
/ 21 декабря 2011

Вот ответ от libc6! Взглянув на /usr/include/x86_64-linux-gnu/bits/byteswap.h, я нашел трюк, который вы искали.

Несколько критиков предыдущих решений:

  • Решение Кипа не позволяет вычислять выражение , которое, в конце концов, часто необходимо.
  • Решение Коппро не позволяет присваивать переменную , так как выражения являются отдельными, но может вычисляться как выражение.
  • Решение Стива Джессопа использует ключевое слово C ++ 11 auto, это нормально, но не стесняйтесь использовать вместо него известный / ожидаемый тип .

Хитрость заключается в том, чтобы использовать как конструкцию (expr,expr), так и область действия {}:

#define MACRO(X,Y) \
  ( \
    { \
      register int __x = static_cast<int>(X), __y = static_cast<int>(Y); \
      std::cout << "1st arg is:" << __x << std::endl; \
      std::cout << "2nd arg is:" << __y << std::endl; \
      std::cout << "Sum is:" << (__x + __y) << std::endl; \
      __x + __y; \
    } \
  )

Обратите внимание на использование ключевого слова register, это только подсказка компилятору. Параметры макроса X и Y (уже) заключены в круглые скобки и приведено к ожидаемому типу. Это решение работает должным образом с пре- и постинкрементным увеличением, поскольку параметры оцениваются только один раз.

В качестве примера, даже если он не был запрошен, я добавил оператор __x + __y;, который позволяет сделать весь блок для оценки в качестве этого точного выражения.

Безопаснее использовать void();, если вы хотите убедиться, что макрос не будет преобразовываться в выражение, поэтому является недопустимым там, где ожидается rvalue.

Однако , решение не соответствует ISO C ++ , как будет жаловаться g++ -pedantic:

warning: ISO C++ forbids braced-groups within expressions [-pedantic]

Чтобы немного отдохнуть g++, используйте (__extension__ OLD_WHOLE_MACRO_CONTENT_HERE), чтобы новое определение гласило:

#define MACRO(X,Y) \
  (__extension__ ( \
    { \
      register int __x = static_cast<int>(X), __y = static_cast<int>(Y); \
      std::cout << "1st arg is:" << __x << std::endl; \
      std::cout << "2nd arg is:" << __y << std::endl; \
      std::cout << "Sum is:" << (__x + __y) << std::endl; \
      __x + __y; \
    } \
  ))

Чтобы еще больше улучшить мое решение, давайте используем ключевое слово __typeof__, как видно из MIN и MAX в C :

#define MACRO(X,Y) \
  (__extension__ ( \
    { \
      __typeof__(X) __x = (X); \
      __typeof__(Y) __y = (Y); \
      std::cout << "1st arg is:" << __x << std::endl; \
      std::cout << "2nd arg is:" << __y << std::endl; \
      std::cout << "Sum is:" << (__x + __y) << std::endl; \
      __x + __y; \
    } \
  ))

Теперь компилятор определит соответствующий тип. Это тоже расширение gcc.

Обратите внимание на удаление ключевого слова register, так как это будет следующее предупреждение при использовании с типом класса:

warning: address requested for ‘__x’, which is declared ‘register’ [-Wextra]
6 голосов
/ 04 ноября 2016

C ++ 11 принес нам лямбды, которые могут быть невероятно полезны в этой ситуации:

#define MACRO(X,Y)                              \
    [&](x_, y_) {                               \
        cout << "1st arg is:" << x_ << endl;    \
        cout << "2nd arg is:" << y_ << endl;    \
        cout << "Sum is:" << (x_ + y_) << endl; \
    }((X), (Y))

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

4 голосов
/ 02 октября 2008

Создать блок, используя

 #define MACRO(...) do { ... } while(false)

Не добавлять; через некоторое время (false)

2 голосов
/ 02 октября 2008

Ваш ответ страдает от проблемы множественной оценки, поэтому (например)

macro( read_int(file1), read_int(file2) );

сделает что-то неожиданное и, вероятно, нежелательное.

0 голосов
/ 02 октября 2008

Как уже упоминалось, вы должны избегать макросов, когда это возможно. Они опасны при наличии побочных эффектов, если макро аргументы оцениваются более одного раза. Если вам известен тип аргументов (или вы можете использовать функцию C ++ 0x auto), вы можете использовать временные фильтры для принудительного выполнения одиночной оценки.

Другая проблема: порядок, в котором происходят множественные оценки, может не соответствовать ожидаемому!

Рассмотрим этот код:

#include <iostream>
using namespace std;

int foo( int & i ) { return i *= 10; }
int bar( int & i ) { return i *= 100; }

#define BADMACRO( X, Y ) do { \
    cout << "X=" << (X) << ", Y=" << (Y) << ", X+Y=" << ((X)+(Y)) << endl; \
    } while (0)

#define MACRO( X, Y ) do { \
    int x = X; int y = Y; \
    cout << "X=" << x << ", Y=" << y << ", X+Y=" << ( x + y ) << endl; \
    } while (0)

int main() {
    int a = 1; int b = 1;
    BADMACRO( foo(a), bar(b) );
    a = 1; b = 1;
    MACRO( foo(a), bar(b) );
    return 0;
}

И он выводится как скомпилированный и запущенный на моей машине:

X=100, Y=10000, X+Y=110
X=10, Y=100, X+Y=110
0 голосов
/ 02 октября 2008

Если вы готовы принять практику всегда использовать фигурные скобки в своих операторах if,

Ваш макрос просто пропустил бы последнюю точку с запятой:

#define MACRO(X,Y)                       \
cout << "1st arg is:" << (X) << endl;    \
cout << "2nd arg is:" << (Y) << endl;    \
cout << "Sum is:" << ((X)+(Y)) << endl

Пример 1: (компилируется)

if (x > y) {
    MACRO(x, y);
}
do_something();

Пример 2: (компилируется)

if (x > y) {
    MACRO(x, y);
} else {
    MACRO(y - x, x - y);
}

Пример 3: (не компилируется)

do_something();
MACRO(x, y)
do_something();
...