Pull to refresh

Comments 77

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

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

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

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

Да, а что с отладкой шаблонного кода? Мало с ними сталкивался, так что интересно, что меня там ждёт…
Я думаю, у вас тут скорее проблема инструмента, чем языка. Вы и сами на это указали:
С т.з. отладчика лямда — это один оператор (если в дизассемблер не лезть), что очень «облегчает» их отладку.

P.S. В С++ (по крайней мере, в VS2017) не испытывал проблем отладки шаблонных классов
Вот как раз он лямбы отлаживать и не умеет. Интересно, а хоть один умеет? К вопросу об инструментах.
На поставленный breakpoint в тело кода лямбды не реагирует?
Да, на БП сработал. Я ступил. :(
А можете пояснить, с какими конкретно проблемами вы столкнулись при отладке C# кода с лямбдами? Правда интересно, потому что у меня не было никаких проблем с отладкой.
Проблема одна. Отладчик не хочет заходить в лямбду. Может просто руки кривые (я с ними раньше дел не имел). Тут подумал — может надо было использовать вход в функцию. Просто у меня вообще лямбды бессмысленно (imho) сделаны: вся функция состоит из одного return, в который запихана огромная лямбда, которая возвращает результат работы этой функции. Зачем так надо было делать — загадка.
Адаптер интерфейса?
Да вроде обычный класс. А в чём проблема для интерфейса сделать без лямбд?
Проблема решается выставлением галочки «Just my code» в свойствах отладчика. Тогда отладчик будет заходить внутрь пользовательских лямбд при вызове библиотечных функций.
Стоит (по умолчанию, похоже), запомню. А тут проблема была в кривых руках
В большинстве таких случаях удобней использовать внешнюю генерацию кода, а не сражаться с макросами C.
О да! Встроенного препроцессора мало — надо писать свой. Flex и Bison в помощь :)
Да, встроенного препоцессора мало. Он изначально задумывался, как «нечто простенькое, что есть всегда». И m4 использовался в качестве замены, когда его возможностей не хватало, а иногда и что-нибудь посерьёзнее. А потом… потом появились IDE. Которые встроенный процессор поддерживали, а всё остальное — нет. И тогда этой «тележке для грузчика» начали пользоваться для того, чтобы возить грузы между континентами. Она для этого приспособлена плохо, грузы мокнут и портятся, но… альтернативы-то «типа нет»!

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

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

Вроде, еще лет 10 назад можно было везде свои команды сборки настраивать. Странно все это. Но люди ленивые, так что ожидаемо.
UFO just landed and posted this here
Ну. Я не сталкивался. Видимо, не достаточно много пишу.

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

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

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

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

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

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

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

У них было выбор — форкнуть или попробовать допилить LLVM. И есть подрзрение, что посмотрев на код GCC они таки решили допиливать…
Если известно, что входным значением всегда является константа, то можно добавить constexpr и быть уверенным, что все вычисления произойдут на этапе компиляции.
К сожалению быть уверенным можно только если описать функцию как consteval (C++20). Если у вас более старый диалект языка, то нужно результат работы constexpr-функции засунуть в constexpr-переменную — только тогда можно быть в чём-то уверенным.
Вспоминается движок UE4, там таких макросов просто море.
Могу добавить, что A2W и T2W еще хуже, чем кажутся.
Если их вызвать в функции, которая вызывается в цикле, а компилятор решит ее заинлайнить…
Ну вы поняли, что будет :)
Вообще, alloca — изощренный способ стать инвалидом.
А какие можно предложить альтернативы для написания конечного автомата для реализации кооперативной многозадачности в embedded?
Там обычно много boilerplate-кода вида:

switch (state)
{
  case 0: // начальное состояние
  // много буков
  state = __LINE__; return true; case __LINE__: // такое повторяется при каждой смене состояния или в контрольной точке
  // много буков
  default:
    return false;
}
UFO just landed and posted this here
… и году к 30-му они станут доступными на нужных целевых платформах.
Это еще полбеды, когда используются только стандартные макросы… Приправим сюда еще m4, Qt-шный MOC и вот тогда наступает настоящий ад!
Кстати, настоящей замены #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; //нормально
}
Объясните, пожалуйста, какую высокоуровневую задачу вы решаете. Конечно если вы возьмёте адрес переменной — то её придётся в объектник засунуть, как иначе? Но если адрес не брать — её и не будет.

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

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

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

Можно защититься через enum:
enum A { MyConstExprValue = 5} ;

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

Вы часто вообще используете std::addressof? Достаточно часто для того, чтобы могли случайно создать проблему (которую вы, кстати, до сих пор отказываетесь описывать)? Покажите — я хочу это видеть!
Ну я не то чтобы прямо борюсь, лично мне без этого жить можно. А цель в 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 пошаманить.
Заметьте, что ваша программа прекрасно компилируется, если заменить в ней constexpr int A = 5; на #define A 5.
Поведение разное, #define в .rodata не попадает.
В каком разделе стандарта описана .rodata?

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

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

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

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

Любая замена #define, разумеется, будет в чём-то от #define отличаться — а иначе какой смысл? Зачем в языке ещё одна сущность, которая полностью дублирует другую?
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.»
Ответ, который вы там откопали дико стар — он был дан, когда ещё C++11 в компиляторах не было. Если компилятор не сможет во время компиляции выяснить значение constexpr-переменной — это ошибка компиляции. А то, что у такой переменной можно взять адрес — так это преимущество, не недостаток. Особенно в C++17, где constexpr-переменная — не static, а inline
из макросов сразу вспоминается
#define TRUE FALSE
#define PI 3.14268265359
… для экономии памяти.
И #define while if для быстродействия.
Важно, что макросы провоцируют ошибки.

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


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

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


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

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

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

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

А если вместо макросов использовать шаблоны, то получаем ещё и бонусы от статической типизации и возможности полноценной отладки.
Если макрос нельзя отладить в голове и нужна его трассировка то это плохой макрос, я видел макросы с вложенными туда алгоритмами на 100 строк и более — искренне не понимаю людей пишущих такое. Тот случай когда алгоритм-алкоритм.
Написать «хороший» макрос не так и просто — сложнее чем эквивалентную функцию. Даже если переименовать isspace => is_space_or_tab все проблемы не уйдут. Например, при таком вызове:
is_space_or_tab(readCharFromFile());

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


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

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

А разве в этом случае код бы собрался без проблем?
UFO just landed and posted this here
Если бы он был в namespace? Да, легко.
А очень часто если противник макросов — то не признает их ни в каком виде.
А как такие умники код тестируют? GTest им не годится, значит… что? И как оно выглядит?

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

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


Это не правда. В спортивном программировании замена max и min на макросы позволяют немного ускорить код, превращая T/L в OK, если в задаче это критичные операции.
github.com/AIshutin/cpp-std-benchmark Сейчас (март 2019) в среднем -8% на GNU GCC 8.2. Тестил ДО снизу на больших (1e6 запросов) радномных тестах с операцией взятия максимума на отрезке и изменения в точке. Буду рад, если кто-нибудь потестит у себя локально и сравнит результаты.
А какое имеет отношение этот бенчмарк к замене std::max на макросы? Там, я извиняюсь, другой алгоритм, а не замена std::max на макросы.

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

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

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

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

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

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

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

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

P. S. Допустил багу во время написания коммента и исправил. Пост переписывал несколько раз.
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. Да, версия с функцией — на шесть строк длиннее, да… но вот это ж как раз чистое олимпиадничание: экономия на строчках, на однобуквенных названиях переменных и прочем. Может быть уместно в условиях жёсткой нехватки времени, но в спокойной обстановке я бы, скорее, макросы не использовал бы.
Кажется, что все кроме первого пункта ссылается на прошлую версию моего комментария, который я судорожно пытался изменить в те 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
если внезапно оказалось, что где-то что-то переполняется, и нужно срочно это исправить.

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

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

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

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

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

на C/C++ практически никто не писал
Сейчас зависит от уровня соревнований и сложности конкретной задачи. На соревнованиях высокого уровня это обычно основной язык. А так, python, Pascal/Delphi тоже используются.
Ну это уже очень далеко от темы. Давайте скажем так: я видел много олимпиадников, кто-то использовал #define ff first, кто-то нет, но ни один из них ни разу не пробовал ничего подобного к нам в Git в программу на миллионы строк что-то подобное заливать.
Не применяли или не видели? Не все их используют, естественно. Но они частенько встречаются в коде и это является нормальным, поэтому я сказал, что они стандартные.

На 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
Кажется, что этот макрос чаще пишется с большой буквы, что является неожиданностью для меня.

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

Что такое «спортивное программирование»? Чем там меряются? У кого exe-шник компактнее?

Ну, это вечная проблема, но статической рефлексии в C++ нет и в ближайшие N лет не будет точно, а я в некоторых проектах постоянно вынужден собирать имена переменных и писать в них значения и реализовано это макросами ибо больше нечем, а кучу магических цифр писать вообще фу как и видеть простыню битовых сдвигов поскольку это вообще не даёт абстракции.
Sign up to leave a comment.