define

Язык C++ открывает обширные возможности для того, чтобы обходиться без макросов. Так давайте попробуем использовать макросы как можно реже!

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

BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT )
  //{{AFX_MSG_MAP(efcDialog)
  ON_WM_CREATE()
  ON_WM_DESTROY()
  //}}AFX_MSG_MAP
END_MESSAGE_MAP()

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

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

Примечание. Этот текст писался как гостевой пост для блога «Simplify C++». Русский вариант статьи решил опубликовать здесь. Собственно, пишу это примечание для того, чтобы избежать вопрос от невнимательных читателей, почему статья не помечена как «перевод» :). А вот, собственно, гостевой пост на английском языке: "Macro Evil in C++ Code".

Первое: код с макросами притягивает к себе баги


Я не знаю, как объяснить причины этого явления с философской точки зрения, но это так. Более того, баги, связанные с макросами, часто очень сложно заметить, проводя code review.

Такие случаи я неоднократно описывал в своих статьях. Например, подмена функции isspace вот таким макросом:

#define isspace(c) ((c)==' ' || (c) == '\t')

Программист, использовавший isspace, полагал, что использует настоящую функцию, которая считает пробельными символами не только пробелы и табы, но также и LF, CR и т.д. В результате получается, что одно из условий всегда истинно и код работает не так, как предполагалось. Эта ошибка из Midnight Commander описана здесь.

Или как вам вот такое сокращение написания функции std::printf?

#define sprintf std::printf

Думаю, читатель догадывается, что это был весьма неудачный макрос. Найден он был, кстати, в проекте StarEngine. Подробнее об этом можно прочитать здесь.

Можно возразить, что в этих ошибках виноваты программисты, а не макросы. Это так. Естественно, в ошибках всегда виноваты программисты :).

Важно, что макросы провоцируют ошибки. Получается, что макросы нужно использовать с повышенной аккуратностью или вообще не использовать.

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

Библиотека ATL предоставляет для конвертации строк такие макросы, как A2W, T2W и так далее. Однако мало кто знает, что эти макросы очень опасно использовать внутри циклов. Внутри макроса происходит вызов функции alloca, которая на каждой итерации цикла будет вновь и вновь выделять память на стеке. Программа может делать вид, что корректно работает. Стоит только программе начать обрабатывать длинные строки или увеличится количество итераций в цикле, так стек может взять и закончиться в самый неожиданный момент. Подробнее про это можно прочитать в этой мини-книге (см. главу «Не вызывайте функцию alloca() внутри циклов»).

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

Не могу я пройти и мимо подобных попыток сокращать код с помощью макросов:

void initialize_sanitizer_builtins (void)
{
  ....
  #define DEF_SANITIZER_BUILTIN(ENUM, NAME, TYPE, ATTRS)   decl = add_builtin_function ("__builtin_" NAME, TYPE, ENUM,              BUILT_IN_NORMAL, NAME, NULL_TREE);    set_call_expr_flags (decl, ATTRS);            set_builtin_decl (ENUM, decl, true);

  #include "sanitizer.def"

  if ((flag_sanitize & SANITIZE_OBJECT_SIZE)
      && !builtin_decl_implicit_p (BUILT_IN_OBJECT_SIZE))
    DEF_SANITIZER_BUILTIN (BUILT_IN_OBJECT_SIZE, "object_size",
         BT_FN_SIZE_CONST_PTR_INT,
         ATTR_PURE_NOTHROW_LEAF_LIST)
  ....
}

Только первая строка макроса относится к оператору if. Остальные строки будут выполняться независимо от условия. Можно сказать, что эта ошибка из мира C, так как она была найдена мною с помощью диагностики V640 внутри компилятора GCC. Код GCC написан в основном на C, а в этом языке без макросов обходиться тяжело. Однако согласитесь, что этот не тот случай. Здесь вполне можно было сделать настоящую функцию.

Второе: усложняется чтение кода


Если вы сталкивались с проектом, который весь пестрит макросами, состоящими из других макросов, то вы понимаете, какой это ад — разбираться в подобном проекте. Если не сталкивались, то поверьте на слово, это грустно. В качестве примера тяжёлого для восприятия кода могу привести уже упомянутый ранее компилятор GCC.

По легенде, компания Apple вложилась в развитие проекта LLVM как альтернативного варианта GCC по причине слишком большой сложности кода GCC из-за этих самых макросов. Где я читал про это, я не помню, поэтому proof-ов не будет.

Третье: писать макросы сложно


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

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

#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
m = MIN(ArrayA[i++], ArrayB[j++]);

Конечно, для таких случаев давно придуманы обходные трюки и макрос можно реализовать безопасно:

#define MAX(a,b)    ({ __typeof__ (a) _a = (a);        __typeof__ (b) _b = (b);      _a > _b ? _a : _b; })

Только вопрос, а нужно ли нам всё это в C++? Нет, в C++ есть шаблоны и другие способы построить эффективный код. Так почему я продолжаю встречать подобные макросы в C++ программах?

Четвёртое: усложняется отладка


Есть мнение, что отладка — это для слабаков :). Это, конечно, интересно обсудить, но с практической точки зрения отладка полезна и помогает находить ошибки. Макросы усложняют этот процесс и однозначно замедляют поиск ошибок.

Пятое: ложные срабатывания статических анализаторов


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

Беда с макросами в том, что анализаторы просто не могут отличить корректный хитрый код от ошибочного кода. В статье про проверку Chromium описан один из таких макросов.

Что делать?


Давайте не использовать макросы в C++ программах без крайней на то необходимости!

C++ предоставляет богатый инструментарий, такой как шаблонные функции, автоматический вывод типов (auto, decltype), constexpr functions.

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

Кто-то может возразить, что код с функцией менее эффективен. Это тоже только «отмазка».

Компиляторы сейчас отлично инлайнят код, даже если вы не написали ключевое слово inline.

Если же речь идёт о вычислении выражений на этапе компиляции, то и здесь макросы не нужны и даже вредны. Для тех же целей намного лучше и безопаснее использовать constexpr.

Поясню на примере. Перед вами классическая ошибка в макросе, который я позаимствовал из кода FreeBSD Kernel.

#define ICB2400_VPOPT_WRITE_SIZE 20

#define  ICB2400_VPINFO_PORT_OFF(chan)   (ICB2400_VPINFO_OFF +                   sizeof (isp_icb_2400_vpinfo_t) +      (chan * ICB2400_VPOPT_WRITE_SIZE))          // <=

static void
isp_fibre_init_2400(ispsoftc_t *isp)
{
  ....
  if (ISP_CAP_VP0(isp))
    off += ICB2400_VPINFO_PORT_OFF(chan);
  else
    off += ICB2400_VPINFO_PORT_OFF(chan - 1); // <=
  ....
}

Аргумент chan используется в макросе без обёртывания в круглые скобки. В результате, на константу ICB2400_VPOPT_WRITE_SIZE умножается не выражение (chan — 1), а только единица.

Ошибка не появилась бы, если вместо макроса была написана обыкновенная функция.

size_t ICB2400_VPINFO_PORT_OFF(size_t chan)
{
  return   ICB2400_VPINFO_OFF
         + sizeof(isp_icb_2400_vpinfo_t)
         + chan * ICB2400_VPOPT_WRITE_SIZE;
}

С большой вероятностью современный C и C++ компилятор самостоятельно выполнит подстановку (inlining) функции, и код будет столь же эффективен, как и в случае макроса.

При этом код стал более читаемым, а также избавленным от ошибки.

Если известно, что входным значением всегда является константа, то можно добавить constexpr и быть уверенным, что все вычисления произойдут на этапе компиляции. Представим, что это язык C++ и что chan — это всегда некая константа. Тогда функцию ICB2400_VPINFO_PORT_OFF полезно объявить так:

constexpr size_t ICB2400_VPINFO_PORT_OFF(size_t chan)
{
  return   ICB2400_VPINFO_OFF
         + sizeof(isp_icb_2400_vpinfo_t)
         + chan * ICB2400_VPOPT_WRITE_SIZE;
}

Profit!

Надеюсь, мне удалось вас убедить. Желаю удачи и поменьше макросов в коде!

Комментарии (71)


  1. jknight
    20.03.2019 22:35

    Я вот знаю как минимум один повод вовсю использовать макросы в современных приложениях. Активно использовал в проектах, работающих с оборудованием по бинарным протоколам: Х-macro. Сложно переоценить, насколько эффективными они бывают, когда нужно описать последовательный набор похожих друг на друга последовательностей данных с различной обработкой их элементов. Объем кодогенерации реально спасает. Всем рекомендую.

    EDIT: вот очень годные примеры использования.


    1. NightShad0w
      20.03.2019 22:51

      Насколько же неэффективна отладка такого кода. И обучение дисциплине и сдержанности разработчиков тоже тратой времени становится. Рекуррентные шаблоны и вменяемая декомпозиция логики гораздо безопаснее.


      1. TargetSan
        20.03.2019 23:35

        К сожалению, X-macro не всегда можно адекватно заменить на шаблонный код. Классическое применение — функция-маппер вариантов перечисления в строку и обратно. А ещё перечисление может "внутри" мапиться на что-нибудь нетривиальное. Справедливости ради, это действительно один из немногих случаев, когда макросы реально полезны.


      1. DrZlodberg
        21.03.2019 08:43

        Отладку усложняют не только такие «устаревшие» вещи, но и вполне новомодные. Недавно получил в наследство проект (правда на C#, как с этим обстоит дело в С++ не в курсе) в котором изрядная часть кода — это лямбды (местами довольно большие). С т.з. отладчика лямда — это один оператор (если в дизассемблер не лезть), что очень «облегчает» их отладку.

        Да, а что с отладкой шаблонного кода? Мало с ними сталкивался, так что интересно, что меня там ждёт…


        1. IGR2014
          21.03.2019 13:56

          Я думаю, у вас тут скорее проблема инструмента, чем языка. Вы и сами на это указали:

          С т.з. отладчика лямда — это один оператор (если в дизассемблер не лезть), что очень «облегчает» их отладку.

          P.S. В С++ (по крайней мере, в VS2017) не испытывал проблем отладки шаблонных классов


          1. DrZlodberg
            21.03.2019 14:07

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


            1. a-tk
              21.03.2019 14:32

              На поставленный breakpoint в тело кода лямбды не реагирует?


              1. DrZlodberg
                21.03.2019 14:44

                Да, на БП сработал. Я ступил. :(


        1. DistortNeo
          21.03.2019 14:20

          А можете пояснить, с какими конкретно проблемами вы столкнулись при отладке C# кода с лямбдами? Правда интересно, потому что у меня не было никаких проблем с отладкой.


          1. DrZlodberg
            21.03.2019 14:29

            Проблема одна. Отладчик не хочет заходить в лямбду. Может просто руки кривые (я с ними раньше дел не имел). Тут подумал — может надо было использовать вход в функцию. Просто у меня вообще лямбды бессмысленно (imho) сделаны: вся функция состоит из одного return, в который запихана огромная лямбда, которая возвращает результат работы этой функции. Зачем так надо было делать — загадка.


            1. a-tk
              21.03.2019 14:32

              Адаптер интерфейса?


              1. DrZlodberg
                21.03.2019 15:32

                Да вроде обычный класс. А в чём проблема для интерфейса сделать без лямбд?


            1. DistortNeo
              21.03.2019 14:52

              Проблема решается выставлением галочки «Just my code» в свойствах отладчика. Тогда отладчик будет заходить внутрь пользовательских лямбд при вызове библиотечных функций.


              1. DrZlodberg
                21.03.2019 15:07

                Стоит (по умолчанию, похоже), запомню. А тут проблема была в кривых руках


    1. NN1
      20.03.2019 23:42
      +1

      В большинстве таких случаях удобней использовать внешнюю генерацию кода, а не сражаться с макросами C.


      1. GCU
        21.03.2019 10:45
        -1

        О да! Встроенного препроцессора мало — надо писать свой. Flex и Bison в помощь :)


        1. khim
          21.03.2019 11:36

          Да, встроенного препоцессора мало. Он изначально задумывался, как «нечто простенькое, что есть всегда». И m4 использовался в качестве замены, когда его возможностей не хватало, а иногда и что-нибудь посерьёзнее. А потом… потом появились IDE. Которые встроенный процессор поддерживали, а всё остальное — нет. И тогда этой «тележке для грузчика» начали пользоваться для того, чтобы возить грузы между континентами. Она для этого приспособлена плохо, грузы мокнут и портятся, но… альтернативы-то «типа нет»!

          Кажется последняя популярная библиотека, которая реализовывала всё примерно так, как это изначально было задумано — это Qt, где вместо того, чтобы насиловать препроцессор написали moc. Но это, в некотором смысле, «последний из могикан»: Qt писалась во времена, когда подход «библиотека должна быть совместима с моей любимой IDE, а иначе я её использовать не буду» ещё не стал доминирующим.

          В современном же мире люди будут скорее насиловать мозги и PVS-Studio тонной макросов, чем подключат к проекту ещё один препроцессор (что, кстати, иронично, так как, в отличие от Turbo Pascal и Turbo C, современные IDE это позволяют сделать).


          1. BigBeaver
            21.03.2019 13:20

            И Qt до сих пор прекрасна.

            Вроде, еще лет 10 назад можно было везде свои команды сборки настраивать. Странно все это. Но люди ленивые, так что ожидаемо.


            1. 0xd34df00d
              22.03.2019 21:30

              Ну так себе прекрасна. Шаг влево, шаг вправо...


              Q_OBJECT не очень дружит с темплейтами, до Qt5 сигналы void smthHappened(Foo) и void smthHappened(MyNs::Foo) были различными для класса внутри MyNs, и даже с новым синтаксисом нельзя подключиться к сигналу, объявленному в интерфейсе.


  1. TargetSan
    20.03.2019 23:39

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


    1. khim
      21.03.2019 03:47

      Все макросы именовать начиная с $. Частью идентификатора он быть не может
      Это кто вас так жестоко обманул? Идём по ссылке — а потом в магазин за губозакатывательной машинкой.

      зато препроцессоры «большой тройки» воспринимают его как нормальный допустимый символ.
      Не «зато». Стандарт действительно оставляет это на усмотрение разработчиков компилятора, но фишка тут вот в чём: если компилятор таки не считает доллар валидным символом, то и в препроцессоре он запрещён тоже. А если считает — так он, конечно, разрешён везде. Чтобы программы с VAX'ов, где он часто разработчиками на C использовался для имитации namespaceов было удобнее переносить. Так что применяем машинку ещё раз.

      P.S. В MSVC в полном соответствии с документацией (скроллить до фразы The dollar sign $ is a valid identifier character in Visual C++) доллар разрешён всегда, в clang'е и gcc — да, это опция, которую можно включать и выключать, но, опять-таки, везде.


  1. smind
    21.03.2019 00:51

    А где же полноценная проверка Миднайт коммандера?


    1. Andrey2008 Автор
      21.03.2019 08:23

      Его без нас проверяют и правят :).


      1. smind
        21.03.2019 11:53

        Ну я в свое время много косяков нашел используя cppcheck.
        Но я не видел чтобы mc тестили ваши продуктом.


  1. kpcb
    21.03.2019 03:37
    +1

    На сколько помню Apple отказалась от GCC из-за смены лицензии на GPLv3 после версии 4.2.1 а не из-за макросов в коде


    1. khim
      21.03.2019 03:48

      У них было выбор — форкнуть или попробовать допилить LLVM. И есть подрзрение, что посмотрев на код GCC они таки решили допиливать…


  1. khim
    21.03.2019 03:54

    Если известно, что входным значением всегда является константа, то можно добавить constexpr и быть уверенным, что все вычисления произойдут на этапе компиляции.
    К сожалению быть уверенным можно только если описать функцию как consteval (C++20). Если у вас более старый диалект языка, то нужно результат работы constexpr-функции засунуть в constexpr-переменную — только тогда можно быть в чём-то уверенным.


  1. AxisPod
    21.03.2019 05:31

    Вспоминается движок UE4, там таких макросов просто море.


  1. kunix
    21.03.2019 09:20

    Могу добавить, что A2W и T2W еще хуже, чем кажутся.
    Если их вызвать в функции, которая вызывается в цикле, а компилятор решит ее заинлайнить…
    Ну вы поняли, что будет :)
    Вообще, alloca — изощренный способ стать инвалидом.


  1. a-tk
    21.03.2019 10:00

    А какие можно предложить альтернативы для написания конечного автомата для реализации кооперативной многозадачности в embedded?
    Там обычно много boilerplate-кода вида:

    switch (state)
    {
      case 0: // начальное состояние
      // много буков
      state = __LINE__; return true; case __LINE__: // такое повторяется при каждой смене состояния или в контрольной точке
      // много буков
      default:
        return false;
    }


    1. 0xd34df00d
      22.03.2019 21:32

      Корутины из C++20.


      1. a-tk
        22.03.2019 22:49

        … и году к 30-му они станут доступными на нужных целевых платформах.


  1. koldoon
    21.03.2019 13:59

    Это еще полбеды, когда используются только стандартные макросы… Приправим сюда еще m4, Qt-шный MOC и вот тогда наступает настоящий ад!


  1. rogoz
    21.03.2019 14:09

    Кстати, настоящей замены #define то до сих пор нет.

    constexpr int A = 5;
    int f()
    {
        auto B = &A;    
        return A;
    }

    A:
            .long   5

    antoshkka я конечно не достаточно эксперт по C++ и стандартам его, но может можно расширить consteval:
    consteval int A = 5;
    //работает как #define A 5, только на уровне компилятора
    int f()
    {
        auto B = &A; //ошибка
        return A+1; //нормально
    }


    1. khim
      21.03.2019 16:29
      +2

      Объясните, пожалуйста, какую высокоуровневую задачу вы решаете. Конечно если вы возьмёте адрес переменной — то её придётся в объектник засунуть, как иначе? Но если адрес не брать — её и не будет.

      Почему вам этого не хватает? Если ошибок боитесь — ну сделайте класс с приватным operator&


      1. rogoz
        21.03.2019 19:34

        класс с приватным operator&…

        Разве поможет?
        int f(const int &a)
        {
            return a;
        }
        constexpr int A = 5;
        int main()
        {
            return f(A);
        }

        A:
                .long   5


        1. khim
          21.03.2019 21:01
          +1

          Поскольку вы так толком и не сказали с чем вы пытаетесь бороться, то я не могу сказать — поможет или нет. Заметьте, что ваша программа прекрасно компилируется, если заменить в ней constexpr int A = 5; на #define A 5. При использовании «охранительного» класса будет точно также создаваться временный объект и передаваться ссылка на него, а если попытаться взять адрес — то вас об этом вежливо известят.


          1. NN1
            21.03.2019 22:46

            И тут на помощь приходит std::addressof :D

            Можно защититься через enum:

            enum A { MyConstExprValue = 5} ;
            
            auto x = &(A::MyConstExprValue);  // нельзя
            auto x = std::addressof(A::MyConstExprValue);  // и так нельзя
            


            1. khim
              21.03.2019 23:48
              +1

              И тут на помощь приходит std::addressof :DM
              Что значит «приходит на помощь»? Основной принцип в C++, описанный ещё в изместной книжке — это то, что все защиты в C++ работают против случайного непреднамеренного доступа, но не против кражи или взлома.

              Вы часто вообще используете std::addressof? Достаточно часто для того, чтобы могли случайно создать проблему (которую вы, кстати, до сих пор отказываетесь описывать)? Покажите — я хочу это видеть!


          1. rogoz
            21.03.2019 23:08

            Ну я не то чтобы прямо борюсь, лично мне без этого жить можно. А цель в 1 сообщении — настоящая замена #define, где обсуждать такую тему, как не здесь.

            Заметьте, что ваша программа прекрасно компилируется, если заменить в ней constexpr int A = 5; на #define A 5.
            Поведение разное, #define в .rodata не попадает.

            Ваши примеры не работают с -O0 или даже -Og, «константа» оказывается в .rodata, а с -O2 и static int A = 5; в данном случае будет выглядеть как константа.
            Возможно тривиальное
            consteval int A()
            {
             return 5;
            }
            будет работать как #define эквивалент, потому что, судя по стандарту, взять адрес consteval функции принципиально нельзя, так что компиляторы возможно не будут генерировать тело функции даже с -O0.
            Edit: да, походу можно с enum пошаманить.


            1. khim
              21.03.2019 23:45
              +1

              Заметьте, что ваша программа прекрасно компилируется, если заменить в ней constexpr int A = 5; на #define A 5.
              Поведение разное, #define в .rodata не попадает.
              В каком разделе стандарта описана .rodata?

              Edit: да, походу можно с enum пошаманить.
              enum можно было для этого использовать ещё в C89.

              судя по стандарту, взять адрес consteval функции принципиально нельзя, так что компиляторы возможно не будут генерировать тело функции даже с -O0.
              Что, когда и как генерируется компиляторами — не определяется спецификацией языка. Можно себе представить компилятор, который будет и #define засовывать в .rodata. И уж тем более вас не должно волновать что он делает -Og: если вас не интересует скорость сгенерированого кода и потребляемая память — то есть масса других языков для этого.

              А цель в 1 сообщении — настоящая замена #define, где обсуждать такую тему, как не здесь.
              Извините, но ваша «настоящая замена» уже давно прератилась в настоящего шотладца.

              Если не определить заранее — каким критериям предлагаемая замена должна удовлетворять — то не будет никакой возможности придумать замену.

              Любая замена #define, разумеется, будет в чём-то от #define отличаться — а иначе какой смысл? Зачем в языке ещё одна сущность, которая полностью дублирует другую?


              1. rogoz
                22.03.2019 00:21

                enum «настоящая замена», полностью подходит, чтот подзабыл я про такое использование.

                На SO цитата на тему, в принципе именно то, что я хотел и имел в виду. «It's because enum never gets any storage while const variable is still a variable and will get (static) storage if the compiler can't proove it won't need one, which it often can't.»


                1. khim
                  22.03.2019 04:42

                  Ответ, который вы там откопали дико стар — он был дан, когда ещё C++11 в компиляторах не было. Если компилятор не сможет во время компиляции выяснить значение constexpr-переменной — это ошибка компиляции. А то, что у такой переменной можно взять адрес — так это преимущество, не недостаток. Особенно в C++17, где constexpr-переменная — не static, а inline


  1. dim2r
    21.03.2019 14:49

    из макросов сразу вспоминается
    #define TRUE FALSE
    #define PI 3.14268265359


    1. konoplinovich
      21.03.2019 16:24
      +1

      #define struct union


      1. a-tk
        22.03.2019 10:07
        +1

        … для экономии памяти.
        И #define while if для быстродействия.


  1. Mingun
    21.03.2019 17:05

    Важно, что макросы провоцируют ошибки.

    Эээ, нет. Обе приведенные в пример ошибки провоцирует название макроса как одной из стандартных функций. Если макрос будет называться is_space_or_tab, то никому и в голову не придет ожидать от него поведения функции isspace.


    Второе: усложняется чтение кода

    Ну, тут как посмотреть. Или 20 строк, в которых 2-3 строки меняются, повторенных 40 раз, или 40 строк макровызова с четко видимыми аргументами. Макрос в этом случае еще и от опечаток страхует.


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

    C++ вообще нужно использовать с повышенной аккуратностью :)


    1. Psionic
      21.03.2019 17:13

      Если макрос будет называться/blockquote>
      Я один использую редакторы отделяющие разной подсветкой функции и макросы?


      1. Mingun
        21.03.2019 17:17
        +1

        В общем случае редактора маловато будет, IDE нужна, которая поймет, что в данном месте isspace — это функция из стандартной библиотеки, в месте 2 — функция из библиотеки в соседнем файле, а в месте 3 — макрос, определенный в дебрях системных библиотек, если стоит дефайн X и не стоит дефайн Y. Причем все 3 случая могут быть в одном файле


    1. DistortNeo
      21.03.2019 17:23
      -1

      А если вместо макросов использовать шаблоны, то получаем ещё и бонусы от статической типизации и возможности полноценной отладки.


      1. Psionic
        21.03.2019 18:23
        +2

        Если макрос нельзя отладить в голове и нужна его трассировка то это плохой макрос, я видел макросы с вложенными туда алгоритмами на 100 строк и более — искренне не понимаю людей пишущих такое. Тот случай когда алгоритм-алкоритм.


    1. tenzink
      22.03.2019 20:54
      +1

      Написать «хороший» макрос не так и просто — сложнее чем эквивалентную функцию. Даже если переименовать isspace => is_space_or_tab все проблемы не уйдут. Например, при таком вызове:

      is_space_or_tab(readCharFromFile());


      1. Mingun
        22.03.2019 21:05

        Это было только возражение к тому посылу, что макросы плохи, потому что могут быть приняты за стандартную функцию. А если бы isspace была бы функцией, это что, как-то кардинально бы поменяло ситуацию? Точно также писавший смотрел бы справку по стандартной библиотеке, а вызывался бы собственный костыль.


        Кроме того, некоторые вещи без макросов просто не реализовать — например, превращение токена в строку. Никакой шаблон или constexpr вам это не сделает. Особенно, если его нужно использовать и как строку, и как идентификатор. А очень часто если противник макросов — то не признает их ни в каком виде. При этом в описанной ситуации очевидно отказ от макроса делает только хуже.


        1. Cerberuser
          22.03.2019 21:07
          +1

          А если бы isspace была бы функцией, это что, как-то кардинально бы поменяло ситуацию?

          А разве в этом случае код бы собрался без проблем?


          1. 0xd34df00d
            22.03.2019 21:36

            Да, если бы isspace была объявлена в некотором неймспейсе, и код был в том же (или дочернем) неймспейсе.


          1. khim
            22.03.2019 21:36

            Если бы он был в namespace? Да, легко.


        1. khim
          22.03.2019 21:37
          +1

          А очень часто если противник макросов — то не признает их ни в каком виде.
          А как такие умники код тестируют? GTest им не годится, значит… что? И как оно выглядит?

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

          Что именно «существенно» — вопрос обсуждаемый, но если у меня в проекте человек, который готов вместо макроса на пять строк породить тысячу строк безумного кода на шаблонах… я, пожалуй, с ним работать не смогу.


  1. AIshutin
    21.03.2019 21:33

    Это тоже только «отмазка».


    Это не правда. В спортивном программировании замена max и min на макросы позволяют немного ускорить код, превращая T/L в OK, если в задаче это критичные операции.


    1. DistortNeo
      22.03.2019 00:25
      +1

      Это в каком году так было?


      1. playermet
        22.03.2019 07:24

        Во времена Turbo C наверное.


        1. AIshutin
          22.03.2019 20:19
          -1

          github.com/AIshutin/cpp-std-benchmark Сейчас (март 2019) в среднем -8% на GNU GCC 8.2. Тестил ДО снизу на больших (1e6 запросов) радномных тестах с операцией взятия максимума на отрезке и изменения в точке. Буду рад, если кто-нибудь потестит у себя локально и сравнит результаты.


          1. khim
            22.03.2019 21:34
            +1

            А какое имеет отношение этот бенчмарк к замене std::max на макросы? Там, я извиняюсь, другой алгоритм, а не замена std::max на макросы.

            Если же взять этот код, заменить std::max на стандартный макрос, то… та-да… скомпилированный код у двух вариантов будет идентичен до последнего бита!

            Ни и откуда у идентичного кода ускорение на 8%? Может вы обладаете сильным чувством веры и умеете ускорять своей верой бинарники на 8% — но мне это, увы, не удаётся.

            P.S. А вот вопрос на тему «а почему и как отказ от использования std::max (или макроса MAX) может ускорить ваш код» — это действительно хороший вопрос для собеседования. Но к «вере в макросы» он отношения не имеет.


            1. AIshutin
              22.03.2019 22:18

              1. Чем Вы их компилировали и с какими флагими, если они были? Я это делал без флагов.

              2. У меня скомпилированный код из первого коммита разный

              Я залил бинарники первых исходников на гитхаб. Естественно, я в теории мог пропатчить бинарники или скомпилить не то, что надо. Я не придумал, каким способом можно дать доказательство того, что такие операции произведены не были. Но на других машинах с другими флагами код может соптимизироваться и скомилироваться по-другому.

              3. Я затупил и вместо define-а самостоятельно заменил max на if ручками (который Вы назвали другим алгоритмом. Странно, что у якобы разных алгоритмов якобы одиноковый скомпилированный код.) Это был некорректный бенчмарк не на ту тематику. Сейчас я написал нормальный нераскрытый макрос и у меня локально работает за примерно такое же время как и раскрытый макрос ранее

              С -Ofast видимо оптимизируется до эквивалентого кода. У меня работает с таким флагом одинаково быстро.

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

              P. S. Допустил багу во время написания коммента и исправил. Пост переписывал несколько раз.


              1. khim
                22.03.2019 23:41
                +1

                1. Чем Вы их компилировали и с какими флагими, если они были?
                По ссылке всё есть.

                Я это делал без флагов.
                Что делает обсуждение чего-либо после этого абсолютно бессмысленным.

                Весь дизайн современного C++, снизу доверху, рассчитан на оптимизирующий компилятор. Весь. Обсуждать что там проихсходит при компиляции с -O0 (а это — умолчаение по историческим причинам) бессмысленно.

                Я затупил и вместо define-а самостоятельно заменил max на if ручками (который Вы назвали другим алгоритмом).
                Да, это другой алгоритм, потому что он не производит записи в некоторых случаях. За счёт проблемы алиасинга компилятор не всегда может подобное преобразование сделать.

                Кажется, что это я криворукий, но пока не знаю, как это сделать нормально.
                Нормально — это как? Так, чтобы оптимизатор сломался? Это можно сделать, более того, я это наблюдал лично. Когда количество элементов в одной функции (после инлайн-подстановок) превышает определённый порог (не знаю точно в чём он меряется но в том случае, когда мы упёрлись в порог на MSVC речь шла о функции в 17 тысяч строк) — то, действительно, могут быть проблемы с оптимизацией.

                На clang и GCC мы такого не наблюдали — да и вряд ли вы каждый день пишите функции в 10 тысяч строк.

                Надеюсь, что сейчас все по модулю странной проблемы с макросами нормально.
                Что вы подразумеваете под странной проблемой с макросами?

                Вы по-прежнему реализуете разные алгоритмы. Но для реализации вашего алгоритма #define не нужны — достаточно вспомогательной функции. Примерно так. Заметьте, кстати, что версия со вспомогательной функцией оказалась короче — хотя и всего на две инструкции. Версия с макросами делает так:
                        mov     eax, DWORD PTR T[0+rax*4]
                        mov     DWORD PTR T[0+rsi*4], eax
                        mov     ecx, DWORD PTR T[0+rcx*4]
                        cmp     eax, ecx
                        cmovl   eax, ecx
                

                А версия со вспомогательной функцией так:
                        mov     edx, DWORD PTR T[0+rdx*4]
                        cmp     DWORD PTR T[0+rcx*4], edx
                        cmovge  edx, DWORD PTR T[0+rcx*4]
                

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

                Это, на самом деле, несолько странно — это типичная ситуация, но я не думал, что она проявится на столь простой функции. Скорее я ожидал одинкового кода…

                P.S. Да, версия с функцией — на шесть строк длиннее, да… но вот это ж как раз чистое олимпиадничание: экономия на строчках, на однобуквенных названиях переменных и прочем. Может быть уместно в условиях жёсткой нехватки времени, но в спокойной обстановке я бы, скорее, макросы не использовал бы.


                1. AIshutin
                  23.03.2019 11:21

                  Кажется, что все кроме первого пункта ссылается на прошлую версию моего комментария, который я судорожно пытался изменить в те 30 минут, что дает хабр. Для того, чтобы убедиться, что я не набагал с макросами я сбрасывал буфер после каждого запроса, что замедлило работу программы в 3 раза. Когда я замерял время работы я забыл поменять обратно endl на '\n' и был крайне удивлен ухудшением производительности. Отсюда:

                  по модулю странной проблемы с макросами
                  Кажется, что это я криворукий, но пока не знаю, как это сделать нормально.


                  Да, версия с функцией — на шесть строк длиннее

                  Казалось бы, наоборот, версия с функцией короче на 2 строки, потому что в ней нет define-ов.

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

                  Я не говорил, что макросы это хорошо или что это плохо.
                  Обсуждать что там проихсходит при компиляции с -O0 (а это — умолчаение по историческим причинам) бессмысленно.


                  Видимо, да. Спасибо.

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

                  Я не прав. Видимо, больше обсуждать по теме здесь нечего.

                  P. S.

                  в условиях жёсткой нехватки времени

                  Тогда используют очень небольшое кол-во макросов, в которых нельзя ошибиться, поскольку не поддерживаешь код и все макросы пишешь либо сам, либо они общепринятые в среде:
                  #define ff first
                  #define ss second
                  #define for(i, n) for(int i = 0; i < n; i++)

                  и не более 5-10 других для сокращения объема кода, чтобы можно было пафосно писать без автодополнений IDE, а код становился более читаемым для спортивных программистов.
                  Конкретно я пользуюсь только двумя верхними и когда нужно тем, что написан ниже:

                  Временами используется:
                  #define int long long
                  если внезапно оказалось, что где-то что-то переполняется, и нужно срочно это исправить.


                  1. mayorovp
                    23.03.2019 11:42

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


                    1. DistortNeo
                      23.03.2019 12:06

                      Когда я ездил по олимпиадам, на C/C++ практически никто не писал. Самым часто используемым языком был Pascal/Delphi, потому что в нём были киллер-фичи: проверки на переполнение и выход за границу массива из коробки, удобая IDE. Те же, кто писал на C/C++, тратили существенно больше времени на написание и отладку кода.

                      А необходимости в макросах я не видел: скорость набора кода не является ограничителем.


                      1. AIshutin
                        23.03.2019 12:36

                        .first и .second могут загромождать формулы и строчки.
                        #define int long long < — нужен не для скорости набора кода, а для того, чтобы быстро убрать переполнение по всему коду

                        скорость набора кода не является ограничителем

                        Это, конечно, правда.

                        на C/C++ практически никто не писал
                        Сейчас зависит от уровня соревнований и сложности конкретной задачи. На соревнованиях высокого уровня это обычно основной язык. А так, python, Pascal/Delphi тоже используются.


                    1. AIshutin
                      23.03.2019 12:34

                      Не применяли или не видели? Не все их используют, естественно. Но они частенько встречаются в коде и это является нормальным, поэтому я сказал, что они стандартные.

                      На codeforces.com они есть не во всех посылках, но во многих бывают. В московской сборной, если я правильно помню, я видел их у каждого, у кого смотрел код, правда, это было не было порядка 10 людей. Можно посмотреть найти посылки всех людей из московской сборной вместе с остальными на региональном этапе здесь, но их неприятно разгребать. Но сам по себе проход и участие в региональном этапе не делает человека спортивным программистом, поэтому посылки остальных людей в большинстве своем не репрезентативны.

                      Вот посылки с последнего раунда на CF:
                      #define int long long
                      пример 1
                      пример 2
                      пример 3
                      пример 4
                      пример 5

                      Я верю, что #define-ы на pair<int, int> на бывают разные. По моему субъективному опыту ff и ss самые популярные, но как оказалось после просмотра посылок последнего раунда, это правда только для локального сообщества, в котором я нахожусь.
                      посылка, но это из московской сборной
                      пример 2
                      пример 3
                      пример 4

                      #define for(i, n)…
                      здесь For, а не for
                      здесь тоже For, а не for
                      здесь он вообще как FOR
                      Кажется, что этот макрос чаще пишется с большой буквы, что является неожиданностью для меня.

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


    1. a-tk
      22.03.2019 10:08
      +1

      Вот поэтому спортивных программистов и не любят.


      1. khim
        22.03.2019 21:35

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