Запись стрима на Youtube: https://youtu.be/mXRbSESitaI?si=CJSh-2eMbeoNkKr1 2025-02-07
Код — это текст
А из чего получается текст?
-
Черновик
-
Болванка
-
Шаблон
-
Генератор
-
Давайте же отнесёмся к нему как к тексту
-
Например, сначала сделаем с этим текстом что-то специальное
-
Переоформим, отредактируем, подстроим под условия
-
А уже потом будем работать с этим текстом по существу
Что если обобщить не поведение программы, а саму программу?
- Нам нужны разные версии программы: для отладки, для тестирования,…
- Некоторые условия — про контекст работы, а не про время выполнения
flowchart TB
subgraph Компилятор
direction TB
cpp{{Препроцессинг}}
cc1{{Компиляция}}
as_{{Ассемблирование}}
ld{{Компоновка}}
cpp --> cc1 --> as_ --> ld
end
ch([Текст])
other_ch([Ещё тексты])
other_o([Ещё машинные коды])
true([Чистый код])
mcode([Машинный код])
asscode([Ассемблерный код])
exe([Исполняемый файл])
ch:::main ==>|-E| cpp
ch ==>|-S| cc1
ch ==>|-c| as_
ch ==>|-v| ld
cpp:::pre -.-> true:::extra
cc1:::step -.-> asscode:::extra
as_:::step -.-> mcode:::extra
ld:::step ==> exe:::main
other_ch:::possible -.-> cpp
other_o:::possible -.-> ld
classDef pre fill:#c7642a,color:#fff,stroke:#c7642a,stroke-width:10pt;
classDef step fill:#888,color:#fff,stroke:#888,stroke-width:10pt;
classDef main fill:#979736,color:#fff,stroke:#979736,stroke-width:10pt;
classDef extra fill:#fff,stroke:#979736,stroke-width:10pt,stroke-dasharray: 3 6;
classDef possible fill:#fff,stroke:#888,stroke-width:1pt,stroke-dasharray: 3 6;
Препроцессинг
Подгоним текст под условия
*.c *.h ⇒ *.i
Компиляция
Превратим текст в коды ассемблера
*.i ⇒ *.s
Ассемблирование
Превратим коды ассемблера в машинные коды
*.s ⇒ *.o
Компоновка
Склеим коды в исполняемый файл
*.o ⇒ *.out
Исполнять свои команды — директивы препроцессора
#include <stdio.h>Создавать «схемы замены текста» — макросы
#define LOL "Вы, конечно, шутите, мистер Фейнман"Менять текст по правилам из макросов
printf("%s", LOL);Удалять комментарии 🙂
// Ничего не знаю, починяю примусflowchart TB
subgraph Строки программы
code([Строки кода])
subgraph Препроцессорные строки
include([Обычная директива])
define([Объявление макроса])
end
define -.->|разворачиваем| code
include -.->|собираем| code
end
subgraph Вызов компилятора
make{{Makefile}}
gcc{{gcc}}
end
subgraph На чём пишем
bash(команды терминала)
subgraph Большой C
lib(разное из библиотек)
c(чистый С)
end
subgraph Препроцессор
dir(директивы)
macro(макросы)
end
bash -.-> make
end
include -.-> dir
include -.-> macro
define -.-> macro
make -.-> macro
gcc -.-> macro
code:::simple -.-> c:::simple
code -.-> lib:::simple
make -.-> define
gcc -.-> define
classDef simple fill:#979736,color:#fff;
Aaaaaaaaaaaa!!!!!!!!!!11одинодинодин — или нет?
- Так бывает
gcc -E— код после препроцессораgcc -E -dM— только макросы (все и отовсюду!)- Макросы мимикрируют под код (и это многое усложняет)
HTML + JS
PHP
(x) — скобки желательны
#x — только для аргументов
x ## y — аргументы или другие макросы
\
вызывать другие макросы
_Generic((x),…) — делать статичные подстановки в зависимости от типов данных (затрудняет рефакторинг!)
Не можем директивы! ~~#ifdef~~ ~~#define~~ ~~#undef~~ …
И макросы невидимы для отладчиков
Не можем зависеть от смысла (логика программы, значения переменных и т. п.)
__FUNCTION__(или__func__),__TIME__,__DATE__,__FILE__,__LINE__,__MACOSX__,__STDC_VERSION__,__STDC_NO_VLA__- Некоторые из них — это макросы времени выполнения (
#undefне выйдет)
-
Параметры в месте раскрытия ставим в скобки
#define DOUBLE(x) (x)*2
-
Одного раза достаточно — это про параметры
#define SQUARE(x) do { int r = (x); r *= r; } while(0);
-
Весь раскрывающий текст прячем в блоки
-
Следим за
; -
Следуем традициям именования (
E, капс,_) -
Перед
#defineставим#undef -
Следим за запятыми
#define ALERT(x) printf("%s\n", #x);
Пишем мелкий код и заодно обустраиваемся в системе.
- Переопределить
NULL🙂 (+ история сPI) - Создать свой мешок макросов — например,
macro.h - Научить редактор подсвечивать ваши макросы
- Создать разные цели компиляции для тестирования и реальной работы
- Программа «Время компиляции» (+ вариант со вводом из терминала)
- Макрос foreach для строк или статичных массивов
- Макрос подсчёта количества параметров в других макросах
- Некоторые знакомые нам «функции» библиотек — это на самом деле макросы
- Условная компиляция в зависимости от настроек платформы
- Конструирование имён функций и переменных
- Конструирование кода с типовой функциональностью
- Синтаксис для вызова разных функций в зависимости от типа аргумента
- Макросы или
enum? Решайте, нужна ли вам типизация и отладка
Авторы неизвестны, решения искали долго в 2000-х
-
Склейка значений двух макросов
#define CONCAT(x, y) x ## y
-
Превращение макроса в строку
#define STR(x) #x
-
Подсчёт количества аргументов (
##даст и 0)#define _COUNT_ARGS(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT #define COUNT_ARGS(...) _COUNT_ARGS(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1, 0)
-
Повторялка кода (ну, такое)
#define REPEAT_3(macro, data) macro(data) macro(data) macro(data) #define PRINT(x) printf("%s\n", x); REPEAT_3(PRINT, "Hello!")
-
Выбрать N-й аргумент макроса
#define SELECT_NTH(N, ...) SELECT_NTH_HELPER(N, __VA_ARGS__) #define SELECT_NTH_HELPER(N, _1, _2, _3, _4, _5, ...) _##N printf("%d\n", SELECT_NTH(3, 10, 20, 30, 40, 50)); // 30
-
Выбрать последний аргумент макроса
#define GET_LAST(...) GET_LAST_HELPER(__VA_ARGS__, dummy) #define GET_LAST_HELPER(_1, _2, _3, _4, _5, last, ...) last printf("%d\n", GET_LAST(10, 20, 30, 40, 50)); // 50
-
Применить «нечто» (ну тоже макрос, да) к каждому аргументу макроса
#define APPLY(macro, ...) __VA_ARGS__ #define PRINT(x) printf("%s\n", x); APPLY(PRINT, "one", "two", "three");
-
Проверить и вернуть что сказано (работает и с пустым значением!)
#define CHECK(x, y) if(!(x)) { return (y); }
-
Показать переменную и её значение
#define TRACE(x, y) printf(#x ": %" #y, (x))
-
Макросы помогают повысить уровень абстракции кода как текста, зафиксировать паттерны в нём
-
Макросы в связке с параметрами компиляции (
-D) — нужно перекомпилировать все зависимые библиотеки, не только главную программу! -
Давайте вытащим все объявления о макросах из кода библиотеки (
#define+grep) -
Давайте найдём все места, где используется этот макрос
-
Компиляторы знают про макросы. Можно указать их в момент вызова
-
Возможно, вам нужны не макросы, а разные цели в
Makefileдля разных платформ компиляции -
В целом
#ifdef— скорее инструмент прототипирования. На этапе рефакторинга его нужно изживать -
Упражнение: напишем макросы для работы с массивами? Только тип данных надо заранее определить
-
А можно всё посмотреть?
echo "" | gcc -E -dM -xc -
(для других языков — другой аргумент ключа
-x; для файлов — путь вместо-) -
А в модуле?
diff <(echo "" | gcc -E -dM -xc -) <(echo "#include <math.h>" | gcc -E -dM -xc -) | grep "define [^_]"
-
Лучше не брать те, что начинаются с подчёркивания. Это по соглашению маркер «внутренности» макроса.
-
Можно закомментировать большой кусок кода и не лишиться подсветки:
#if 0 ... #endif
Но внутри не должно быть начинающихся
#if…без окончания, иначе перепутаются#endif -
Написать программу только на макросах. С зависимостью от компиляции. И цели поставить в
Makefile -
Макросы с параметрами. Параметры здесь — это просто «место для чего-то». По ходу разворачивания макроса параметр заменяется на его значение: «возьми то, что написано тут в вызове, и подставь сюда». К переменным — никакого отношения.
-
Легко перепутать обозначение параметра и символ, который хочется использовать, например, в качестве переменной. Как быть? Например,
_в начале обозначения параметра, или вообще не пользоваться однобуквенными переменными (ну, почти — всё-такиforнуждается вi). -
Ключевое отличие от функций: при вызове функций аргумент сначала вычисляется, а потом уже передаётся в параметр. При вызове макросов аргумент передаётся «как есть» — вот буквально что написано, просто последовательность символов.
Основная проблема: мы пишем для «универсальной машины», но работает-то программа на конкретной. Со своими особенностями железа и нюансами компилятора. Возникает миллион ветвлений реализации.
Выход: как можно дольше жить в парадигме «универсальной машины», а конкретику прятать в функции, занятые только реализацией. И выбор вариантов тоже оборачивать в функции.
Не вызываем препроцессор в середине файла. По возможности. Иногда трудно, да. Отсюда эвристика: если видим директиву препроцессора в середине кода — возможно, это повод создать новый файл.
Пусть #ifdef оперируют крупными кусками (например, вызовами функции), а не реализуют управляющую логику кода.
Можно прятать #ifdef за обобщающий макрос? И в коде вызывать только его?
Один заголовок для разных реализаций — а тут-то при чём #ifdef?
Желательно, чтобы все #ifdef были на одном уровне абстрации — и чтобы этот уровень можно было описать словами. Например, «выбор оборудования» или «выбор операционки» или «выбор режима отладки/тестирования/…».
Эвристика «Поток важнее реализации». Поток должен считываться из кода легко. В реализации можно уже и каши навертеть.
Общая идея — относиться к вариантам системы как к типам данных. И так же с помощью макросов «доводить» имя вызываемой функции до нужной полноты.
Можно даже создать API к библиотеке полностью из макросов. Чтобы не допускать программиста-пользователя (часто это вы и есть) до деталей реализации.
