Блог пользователя yermak0v

Автор yermak0v, 10 лет назад, По-русски

Всем привет.
После того как yeputons разочаровал меня сказав, что эту задачу нельзя решить адекватным способом, и дал ссылку на полезные данные по макросам, я решил, что этим всем нужно поделиться со всеми.
Значит приступим.
Для тех, кто не в курсе, макрос — это фрагмент кода, которому дается имя. Когда это имя используется — оно заменяется на содержание макроса. Есть два типа макросов: обычные и функциональные(на английском они называются object-like и function-like, но я бы назвал их именно так).
Обычный макрос — это простой идентификатор, который будет заменен фрагментом кода. В основном используется как константа, или чтобы дать имя списку чисел и/или константным значениям других типов.
Пример 1.
#define MAXN 1000
Теперь всегда когда мы будем писать MAXN будет использоваться число 1000.
Пример 2.
#define nums 1, 2, 3 Теперь вместо слова nums будет подставляться 1, 2, 3
Пример 3.
#define mc my_class_with_very_very_long_name
Теперь вместо написания иногда очень долгих имен типов мы просто напишем mc.
Функциональный макрос — который будет использоваться как функция. Имеет очень много возможностей.
Пример 4.
#define abs(x) ((x)>=0?(x):-(x))
Теперь мы можем использовать функцию abs(x), при чем x может быть любого типа для которого будет работать сравнение x>=0.
Пример 5.
#define max(a,b) ((a)>(b)?(a):(b))
Такой макрос можно спокойно (ну почти спокойно... См. Предупреждение 3.) использовать для нахождения максимума, и как и в прошлом примере для корректности работы необходимо, чтобы выражение (a)>(b) имело смысл.
Предостережение 1.
Все переменные имеет смысл, а иногда даже нужно, брать во скобки, чтобы избежать проблем со старшинством операций. Например, если в качестве аргумента пойдет выражение с битовыми операциями.

Макросы могут вызывать друг друга, и в отличии от функций макрос может вызывать макрос, который описан позже него. Но с рекурсией макросы не дружат — ни с прямой, ни с непрямой.
<a name="stringification>Перевод имени переменной в строку (англ. stringification. Кто-то может перевести это слово?). Можно получить имя переменной как строку.
Пример 6.
#define id(x) #x
Тут я немного о ней рассуждал. Единственное скажу — поистине магическая штука...
Конкатенация строки к имени переменной. Сразу перейдем к примеру.
Пример 7.
#define get(name) (get_##name())
Очень полезно в том же самом ООП, когда методов геттеров много. Хотя по сути жизнь и без этого прекрасна.
Очень много стандартных макросов в С++. Думаю очень много внимания этому уделять не стоит. Кто хочет — почитайте тут.
Переопределение и удаление макросов. Переопределение производится новым вызовом директивы #define name, где name имя уже используемого макроса. Макрос можно переопределить в обычный или функциональный независимо от того, каким он был до этого. Удаление макроса используется при помощи директивы #undef name, где name имя макроса.
Пример 8.
#define func 2 — обычный макрос, который возвращает 2
#undef func — теперь func это обычное имя переменной
#define func 5 — снова обычный макрос
#define func(x) ((x)+5) — переопределение в функциональный
Макрос может быть переопределен даже во время его использования.
Пример 9.

#define f(a,b) ((a)*(b))
...  
f(2,  
#undef f  
#define f 3  
f)  

Такой код спокойно возвратит 6. Компилятор при этом будет молчать.
Макросы с переменным числом аргументов. Принцип работы примерно такой же, как и в функциях и/или шаблонах с переменным числом аргументов. Примеры тут. От себя, к сожалению, ничего не добавлю.
Предостережение 2.
Не желательно передавать аргументами макроса функции. Лучше передать ее результат. Ибо если та переменная в макросе используется больше раза — то функция вызовется очень много раз.
Пример 10.
Вспомним макрос max и попробуем запустить такой код max(x, func(y)). Этот код после окончательной замены будет выглядеть вот так: ((x)>(func(y))?(x):(func(y)). Как видим func(y) вызывается два раза. В худшем случае это может существенно повлиять на время работы.
Перевод строки в макросе. Если нужно записать макрос в нескольких строках можно использовать перевод строки \. Думаю к этому можно обойтись и без примера.
Борьба с нежелательной точкой с запятой.
Пример 11.
Допустим у нас был такой код:

#define cnt(x, y)\  
{\  
y = 0;\  
while (x > 0) {\  
    ++y;\  
    x /= 10;\  
}  

Этот код узнает сколько цифр в числе x и записывает результат в y (кстати в этом примере скобки не нужны, так как отправь мы в этот макрос не переменную, код не будет компилироваться). Тогда, если вставить его перед else, компилятор заругается на синтаксическую ошибку. Эта проблема решается вот так:

#define cnt(x, y)\  
do {\  
y = 0;\  
while (x > 0) {\  
    ++y;\  
    x /= 10;\  
} while (0)  

И теперь после него можно всегда ставить точку с запятой.
И еще несколько примеров.
Пример 12.
#define forn(i,a,b) for(int i = (a); i < (b); ++i)
Самый частый макрос, для упрощения написания циклов.
Пример 13.
#define sum(a,b) ((a)+(b))
Бесполезный, но прикольный макрос, для нахождения оператора + двух переменных. Сумма для чисел, конкатенация для строк и т.д.
Надеюсь эта запись будет полезна для вас.
UPD. Предупреждение 3.
Спасибо hellman_ за пример с инкрементом.
Даже скобки не всегда могут от всего спасти. Поэтому можно либо следить за всеми переменными, или более реальный вариант — использовать такой или похожий макрос:
#define max(a,b) ((___x = (a)) > (___y = (b)) ? x : y)
Но тогда переменные ___x и ___y придется описать сразу. И тогда вылезают проблемы, ибо ___x и ___y у нас статистического типа и т.д. Поэтому полноценного варианта с помощью макроса я не знаю. Но в большинстве случаев первый работает очень хорошо. Могу посоветовать просто не кидать туда никакие выражения.

  • Проголосовать: нравится
  • +12
  • Проголосовать: не нравится

»
10 лет назад, # |
  Проголосовать: нравится +10 Проголосовать: не нравится

про variadic макросы я лишь добавлю этот блог: дебаг-функция всегда полезна, а ты наверняка бросишься ее писать)
и еще полезно посмотреть что делает препроцессор, удобно ловить ошибки в макросах: g++ -std=c++11 -E main.cpp (для быстроты и "чистоты" лучше убирать все include перед запуском)

»
10 лет назад, # |
Rev. 2   Проголосовать: нравится +23 Проголосовать: не нравится

Такой макрос можно спокойно использовать для нахождения максимума

С макросами нельзя быть спокойным, например max(i++, j) может один или два раза инкрементировать i. У этого же свойства есть проблема неоптимальности: max(heavy_func(), 0) может вызвать heavy_func() два раза. Такие штуки желательно писать на шаблонах, хотя и с ними есть проблемы — иногда приходится в ручную приводить тип (типа max(1, (int)1.0), не помню нужно ли с int и long long).

Зато на макросах можно делать удобные дебажные штуки, типа:

EVARS(a, b, curval[a], curval[b]);

Вывод (210 это номер строки):

#210: a=4; b=2; curval[a]=2; curval[b]=4;

(Код можно глянуть в моих сабмитах, я его у кого-то утащил и переделал под себя, по моему из статьи про трюки C++).

Ещё, чтобы избавиться из-за проблемы перевычисления границы в цикле (типа простой forn(i,0,strlen(s)) будет каждый раз вычислять strlen(s)), можно сохранять граничное значение в дополительной переменной:

#define CONCAT3_NX(x, y, z) x ## y ## z
#define CONCAT3(x, y, z) CONCAT3_NX(x, y, z)
#define VAR(name) CONCAT3(__tmpvar__, name, __LINE__)
#define TYPE(x) __typeof(x)

#define FOR(i, s, n)  for (TYPE(n) i=(s),   VAR(end)=(n);  i <  VAR(end);  i++)

Тут VAR(x) делает переменную типа __tmpvar__x_123, чтобы не было коллизий.

  • »
    »
    10 лет назад, # ^ |
    Rev. 4   Проголосовать: нравится +5 Проголосовать: не нравится

    На счет max(i++,j) — все переменные в скобки взяты не зря.
    На счет max(heavy_func(x), 0) — в конце есть предупреждение.
    А на счет последнего — реально полезные макросы.

    • »
      »
      »
      10 лет назад, # ^ |
      Rev. 2   Проголосовать: нравится +10 Проголосовать: не нравится

      Ничем тебе скобки не помогут от того, что i++ развернется два раза

      upd: нашел предупреждение про вызов 2 раза, но, кажется, его логичнее подвинуть к тому месту, где описана max()

»
10 лет назад, # |
  Проголосовать: нравится +75 Проголосовать: не нравится

Это ужасно!
Единственное, имеющее смысл из всего перечисленного — использование "#" для вывода имени переменной.
Все остальное можно написать на порядок надежнее с использование шаблонов, функций, типизированных констант и лямбд: в этом случае хотя бы предупреждение компилятора при несовпадении типов будет, может и спасти от чего-нибудь.
Абсолютно непонятно, почему в примере 11 не написать бы соответствующую функцию?
И непонятно, почему бы не использовать для вычислении abs, min и max вот это:

template<typename T> inline T Abs(const T a) { return a < 0 ? -a : a; }
template<typename T> inline T Min(const T a, const T b) {return a < b ? a : b;}
template<typename T> inline T Max(const T a, const T b) {return a > b ? a : b;}

В комментариях верно заметили про подводные камни с многократным вычислением значения у
#define abs(x) ((x)>=0?(x):-(x))

Мое личное мнение, что так писать нельзя, а за злоупотребление следует отрывать руки.

  • »
    »
    10 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

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

    • »
      »
      »
      10 лет назад, # ^ |
        Проголосовать: нравится -9 Проголосовать: не нравится

      Если вы не сторонник частого их использования, то попробуйте написать статью о том, как реализовать все написанное без макросов.
      На мой взгляд, это будет гораздо более полезная и "окультуривающая" статья. :)
      Даже многими любимый FORN можно реализовать на шаблоне и лямбде, а код, его использующий, будет не сильно больше.

      • »
        »
        »
        »
        10 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Даже многими любимый FORN можно реализовать на шаблоне и лямбде, а код, его использующий, будет не сильно больше.

        Не согласен. В лямбде придется явно захватывать все внешние переменные, которые будут использоваться в цикле.

        P.S. Я ни в коей мере не являюсь сторонником использования макроса FORN, даже наоборот.

        • »
          »
          »
          »
          »
          10 лет назад, # ^ |
            Проголосовать: нравится +18 Проголосовать: не нравится

          В лямбде придется явно захватывать все внешние переменные, которые будут использоваться в цикле.

          а как же "[&]" ?

      • »
        »
        »
        »
        10 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        Не могу понять, как можно FORN реализовать без макроса?

        • »
          »
          »
          »
          »
          10 лет назад, # ^ |
          Rev. 4   Проголосовать: нравится 0 Проголосовать: не нравится

          Как-то так.

          #include <iostream>
          
          template <class T1, class T2>
          void forn(T1 a, T1 b, T2 f) { for (T1 i = a; i != b; ++i) f(i); }
          
          int main()
          {
            forn(0, 10, [&](int i) { std::cout << i << std::endl; });
            return 0;
          }
          
          
        • »
          »
          »
          »
          »
          10 лет назад, # ^ |
            Проголосовать: нравится -8 Проголосовать: не нравится

          Было:

          #define FORN(i, n) for (int i = 0; i < n; i++)
          
          int MacrosUsage()
          {
          	int s = 0;
          	FORN(i, 20) { s += i; }
          	return s;
          }
          


          Стало:

          template<typename Func>
          void ForN(const int n, Func& func)
          {
          	for (int i = 0; i < n; i++)
          		func(i);
          }
          
          int TemplateUsage()
          {
          	int s = 0;
          	ForN(20, [&](int i) { s += i; });
          	return s;
          }
          

          Можно пойти дальше и, например, параметризовать тип счетчика, добавить параметр "от какого значения начинать" и др.

          • »
            »
            »
            »
            »
            »
            10 лет назад, # ^ |
            Rev. 2   Проголосовать: нравится +8 Проголосовать: не нравится

            Согласись, менее удобно использовать, чем макрос?

            (Хотя я не сторонник макросов тоже, да)

            • »
              »
              »
              »
              »
              »
              »
              10 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              Лично для меня разница в "[&](int i)" и ")" незначительна по сравнению с тем, какие проблемы можно огрести, если неудачно заиспользовать макрос.

          • »
            »
            »
            »
            »
            »
            10 лет назад, # ^ |
            Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

            Функциональности намного меньше чем у макроса. Например, до меня до сих пор не дошло как сделать вложеные циклы.

            • »
              »
              »
              »
              »
              »
              »
              10 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              В одной лямбде использовать параметр i, в другой — j.

            • »
              »
              »
              »
              »
              »
              »
              10 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится
              ForN(20, [&](int i) { 
                  ForN(42, [&](int j) {
                      i + j;
                  }); 
              });
              
              • »
                »
                »
                »
                »
                »
                »
                »
                10 лет назад, # ^ |
                Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

                Пойду-ка я разбираться с лямбдами...

              • »
                »
                »
                »
                »
                »
                »
                »
                10 лет назад, # ^ |
                  Проголосовать: нравится 0 Проголосовать: не нравится

                А вот и нет: будет ошибка компиляции :)
                Сходу даже и не знаю, как малой правкой заставить компилироваться.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                  Rev. 3   Проголосовать: нравится 0 Проголосовать: не нравится

                  надо сказать у меня и ваш пример не компилится. А если убрать & в определении ForN (или дописать const) — тогда компилится и мой и ваш

                  PS: это, конечно, запрещает модифицировать переданный callable объект, но, надеюсь, никто не собирался так делать

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                  Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

                  Да, виноват: нужно было писать "const Func&".
                  С такой правкой оба варианта работают.
                  MSVS сильно расслабляет в этом плане: позволяет временную переменную передавать в неконстантную ссылку.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Я собирался. Ну то есть, хотелось бы таки найти полное соответсвие макросу.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Слабо себе представляю эквивалентый макрос, который вы имеете ввиду.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Мы походу друг друга не поняли. Я имею ввиду написать полноценную функцию для цикла, которая будет работать в любом случае: и для вложеных циклов и т.д.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Ну я сходу не вижу вещей. которые не работают с шаболном, но работают с макросом

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Ну я имею ввиду изменение callable объекта, которое работать не хочет. Или я снова что-то не так понял?

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  А что вы понимаете под этой фразой (в контексте решения с макросом)?

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Ну насколько я понял там переменная i будет записана с const. Тогда в случаях когда ее нужно изменить мы ничего сделать не сможем. Но иногда очень даже нужно перепрыгнуть через одну итерацию, без всяких извращений.
                  P.S. в таком случае эта функция будет больше похожа на pascal'евский for

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                  Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

                  скорее всего, если написать

                  [](int& i){} то будет работать

                  (Хотя я плохо представляю случай, когда это нужно, если у нас все равно делается ++i)

                  А вот с breakом сложнее — пока не знаю, как реализовать удобнее, чем "i = МНОГО; return"

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  И во вложеных циклах тоже?

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  А почему бы нет.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится
                  Func&& func
                  
                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  А вот с breakом сложнее — пока не знаю, как реализовать удобнее, чем "i = МНОГО; return"

                  Можно сделать так, чтобы "return true" из "Func" было равносильно continue, а "return false" из "Func" — break-у.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится +5 Проголосовать: не нравится

                  Я если нужно сделать return из функции в которой мы сейчас находимся? В main'е вроде понятно — exit, и нет проблем, а вот если где-то в функции — то уже как-то проблемы вылезают...

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Да, этот пункт я не учел. Шаблон в данном случае бессилен.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

                  Вот в этом случае макрос forn и пригодится, но по сути, сам я такие макросы не использую, и призывать кого-то их использовать не буду.

                • »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  »
                  10 лет назад, # ^ |
                    Проголосовать: нравится 0 Проголосовать: не нравится

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

          • »
            »
            »
            »
            »
            »
            10 лет назад, # ^ |
              Проголосовать: нравится +16 Проголосовать: не нравится

            В таком подходе есть один большой недостаток: невозможность использования операторов break и continue.
            Макрос FORN используется в олимпиадном программировании для ускорения набора кода. Никаких дополнительных проблем, которых не было бы в обычной конструкции цикла for (int i = 0; i < n; i++) при этом не возникает.
            Ваша же шаблонная конструкция в написании ничем не проще стандартного цикла и в то же время, если не учитывать оптимизатор, будет работать значительно дольше.

            • »
              »
              »
              »
              »
              »
              »
              10 лет назад, # ^ |
                Проголосовать: нравится 0 Проголосовать: не нравится

              continue = return

              А вот return и break уже не ясно, как реализовывать.

            • »
              »
              »
              »
              »
              »
              »
              10 лет назад, # ^ |
                Проголосовать: нравится +3 Проголосовать: не нравится

              Макрос FORN используется в олимпиадном программировании для ускорения набора кода

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

        • »
          »
          »
          »
          »
          10 лет назад, # ^ |
            Проголосовать: нравится +8 Проголосовать: не нравится

          Можно посмотреть у меня в коде почти по любой задаче. Получается гораздо длиннее, но написал 1 раз и забил

          В коде вызывается примерно так:


          for(int i: range(n)) for(int j: range(i, n)) cout << i + j << "\n";
          • »
            »
            »
            »
            »
            »
            10 лет назад, # ^ |
              Проголосовать: нравится 0 Проголосовать: не нравится

            Ну это уже больше на Python-like циклы смахивает.

            • »
              »
              »
              »
              »
              »
              »
              10 лет назад, # ^ |
                Проголосовать: нравится +5 Проголосовать: не нравится

              Ну ими и вдохновлено, конечно.

              Главное, что я вижу в макросе — написание переменной цикла 1 раз и уменьшение кол-ва пунктуации (так что не напишешь +i вместо ++i) он дает