ch
Feedback
this->notes.

this->notes.

前往频道在 Telegram

О разработке, архитектуре и C++. Tags: #common, #cpp, #highload и другие можно найти поиском. Задачки: #poll. Мои публикации: #pub. Автор и предложка: @vanyakhodor. GitHub: dasfex.

显示更多
4 503
订阅者
-524 小时
-217
-5530
吸引订阅者
六月 '26
六月 '26
+28
在1个频道中
五月 '26
+106
在0个频道中
Get PRO
四月 '26
+70
在1个频道中
Get PRO
三月 '26
+57
在2个频道中
Get PRO
二月 '26
+30
在0个频道中
Get PRO
一月 '26
+69
在0个频道中
Get PRO
十二月 '25
+75
在0个频道中
Get PRO
十一月 '25
+75
在1个频道中
Get PRO
十月 '25
+141
在3个频道中
Get PRO
九月 '25
+1 111
在1个频道中
Get PRO
八月 '25
+774
在1个频道中
Get PRO
七月 '25
+342
在3个频道中
Get PRO
六月 '25
+94
在2个频道中
Get PRO
五月 '25
+100
在2个频道中
Get PRO
四月 '25
+71
在1个频道中
Get PRO
三月 '25
+63
在2个频道中
Get PRO
二月 '25
+51
在1个频道中
Get PRO
一月 '25
+34
在0个频道中
Get PRO
十二月 '24
+56
在0个频道中
Get PRO
十一月 '24
+100
在1个频道中
Get PRO
十月 '24
+93
在1个频道中
Get PRO
九月 '24
+220
在2个频道中
Get PRO
八月 '24
+76
在0个频道中
Get PRO
七月 '24
+155
在1个频道中
Get PRO
六月 '24
+44
在1个频道中
Get PRO
五月 '24
+86
在0个频道中
Get PRO
四月 '24
+144
在5个频道中
Get PRO
三月 '24
+100
在1个频道中
Get PRO
二月 '24
+64
在0个频道中
Get PRO
一月 '24
+132
在0个频道中
Get PRO
十二月 '23
+26
在0个频道中
Get PRO
十一月 '23
+16
在0个频道中
Get PRO
十月 '23
+26
在0个频道中
Get PRO
九月 '23
+29
在0个频道中
Get PRO
八月 '23
+29
在0个频道中
Get PRO
七月 '23
+63
在0个频道中
Get PRO
六月 '23
+50
在0个频道中
Get PRO
五月 '23
+53
在0个频道中
Get PRO
四月 '23
+145
在0个频道中
Get PRO
三月 '23
+48
在0个频道中
Get PRO
二月 '23
+46
在0个频道中
Get PRO
一月 '23
+38
在0个频道中
Get PRO
十二月 '22
+33
在0个频道中
Get PRO
十一月 '22
+60
在0个频道中
Get PRO
十月 '22
+37
在0个频道中
Get PRO
九月 '22
+90
在0个频道中
Get PRO
八月 '22
+49
在0个频道中
Get PRO
七月 '22
+352
在0个频道中
日期
订阅者增长
提及
频道
24 六月+1
23 六月+1
22 六月0
21 六月+1
20 六月+1
19 六月+1
18 六月+1
17 六月+3
16 六月+1
15 六月0
14 六月+1
13 六月+1
12 六月0
11 六月+5
10 六月+1
09 六月0
08 六月+1
07 六月0
06 六月+1
05 六月0
04 六月+3
03 六月+2
02 六月+2
01 六月+1
频道帖子
#cpp Day 23. Мы уже пользовались, но не оговаривали. У многих компиляторов есть нестандартные флаги компиляции. -E указывает компилятору, что нужно только выполнить препроцессинг. Не далее. Можно пользоваться, если хотите посмотреть на код после или подебагать макросы. Если помните, когда мы смотрели на #line, в результате компиляции файла с -E мы видели маркеры строк. Это которые меняли значения __LINE__ и __FILE__. Вот их можно убрать, если к -E добавить -P. @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.

2
#cpp Day 22. Мы с вами уже говорили про #pragma once. Но прагмы бывают разные. Это фактически implementation defined способ сообщить компилятору, что надо сделать. Но коммуницировать бывает сложно. Особенно если я пытаюсь генерировать прагму макросами: #define DISABLE_WARNINGS #pragma GCC diagnostic ignored "-Wall" Такой синтаксис ломается из-за второго диеза (#). Поэтому есть альтернатива: _Pragma(): #define DISABLE_WARNINGs _Pragma("GCC diagnostic ignored \"-Wall\"") #define DO_PRAGMA(x) _Pragma(#x) DO_PRAGMA(GCC diagnostic ignored "-Wunused-variable") Последнее как раз эквивалентно: #pragma GCC diagnostic ignored "-Wunused-variable" Аргумент — обязательно строковый литерал. Прагмамируйте на здоровье (смешно?). @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
969
3
#cpp Day 21. Вернёмся к __VA_ARGS__. Мы смотрели на такой макрос: #define LOG(fmt, ...) print(fmt, __VA_ARGS__) И когда мы используем его с одним аргументом: LOG("str"); мы получим print("fmt", ), что вообще-то невалидно, т.к. одна запятая без аргумента сзади это CE. Это решали расширением компиляторов: #define LOG(fmt, ...) print(fmt, ##__VA_ARGS__) То есть оно как бы работает, но не везде одинаково, есть проблемы с разными опциями компиляции. Стандартное решение (с C++20) — использование __VA_OPT__: #define LOG(fmt, ...) print(fmt __VA_OPT__(,) __VA_ARGS__) __VA_OPT__ — это такая приколюха, которая оставляет аргумент, если __VA_ARGS__ в том же макросе не пустые. Если пустые (что? да), не оставляет. Совать в аргумент можно не только запятую: #define SDEF(sname, ...) S sname __VA_OPT__(= { __VA_ARGS__ }) SDEF(foo); // replaced by S foo; SDEF(bar, 1, 2); // replaced by S bar = { 1, 2 }; Хотя с __VA_ARGS__ внутри __VA_OPT__ надо быть аккуратными. Есть классическая статья David Mazières из Stanford про то, как можно абюзить __VA_OPT__ для создания рекурсивных макросов: https://www.scs.stanford.edu/~dm/blog/va-opt.html @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 007
4
#cpp Day 20. В C и C++ есть альтернативные варианты записи некоторых операторов. Например, && можно записать как and. Или != можно заменить на not_eq. Такие альтернативы появились в силу ограниченности некоторых клавиатур, в которых иметь символ & (и другие) было не всегда возможно. Сегодня иногда некоторые программисты предпочитают использовать некоторые из альтернативных вариантов записи ради улучшения читаемости: if (a and b) // over if (a && b) Это всё субъективно конечно. Есть ещё альтернативные токены для записи скобок [] {} и # ##. Есть ещё триграфы: ещё одна альтернатива с чуть более широким набором символов (deprecated в C23). Ваша программа могла выглядеть так: %:include <stdio.h> %:include <stdlib.h> ??=include <iso646.h> int main(int argc, char** argv) ??< if (argc > 1 and argv<:1:> not_eq NULL) <% printf("Hello %s??/n", argv<:1:>); %> else <% printf("Hello %s??/n", argc? argv??(42??'42??) : __FILE__); %> return EXIT_SUCCESS; ??> Мы так лабы сдавали в универе. Последние deprecated, так как несут проблемы и могут менять ваше поведение. Из-за того, что они processed early, вместо "What's going on??!" вы можете получить "What's going on|" Проблемес. @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 165
5
#cpp Day 19. Возвращаясь к __FILE__ и __LINE__. Вы можете изменить их значения с помощью #line: #line 123 "name.cpp" int main() { std::cout << __FILE__ << ' ' << __LINE__ << std::endl; std::cout << __FILE__ << ' ' << __LINE__ << std::endl; std::cout << __FILE__ << ' ' << __LINE__ << std::endl; } Результат будет: name.cpp 126 name.cpp 127 name.cpp 129 Или можно только строку переопределить: #line 123 Круто. Но зачем? Представьте, что вы генерируете код. И ваш сгенерированный файл имеет название generated_228228.cpp. Если вы начнёте выдавать юзеру ошибки, основанный на __FILE__/__LINE__ в этом файле, юзер будет в замешательстве. Он-то ни про какой generated_228228.cpp не в курсе. Потому в начале файла вы можете воткнуть: #line 1 "query.sql" Что возволяет вам ссылаться на оригинальный источник. #line кстати влияет не только на макросы, но и на std::source_location. Насколько я понимаю (что может быть неправдой), компилятор сам активно пользуется подобным приёмом. Вы же когда подключаете инсклуд, вы фактически получаете один огромный cpp файл. Но в нём при этом все вызове __FILE__, __LINE__ и других связанных штук работают как будто находятся в разных файлах. Давайте возьмём Hello world: #include <iostream> int main() { std::cout << "Hello, thisnotes!"; } и скомпилируем clang -E main.cpp. Мы получим какое-то полотно (от iostream), а в конце будет: // полотно # 2 "main.cpp" 2 int main() { std::cout << "Hello, thisnotes!"; } Вот это # 2 "main.cpp" 2 — расширенная версия #line у компилятора. То есть он кроме вставки инклудом файла ещё и добавляет в нужное место #line-like команду, подправляющую текущие значения строк и имён файлов. Хотя файл у вас в итоге всего один. Скажу ли я что-то про __FUNCTION__, __PRETTY_FUNCTION__ и __func__? Нет. Ведь это не часть препроцессора, а скорее «implicit» переменные. Так что на самостоятельное изучение. @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 201
6
#cpp Day 18. Аналогично есть __DATE__ и __TIME__. Первый раскрывается в строковый литерал, содержащий дату компиляции. Второй содержит время компиляции. Можно использовать для версионирования ваших бинарников. Чтобы удобнее понимать, что сейчас запущено (та ли версия собралась, что работает у юзера, обновился ли бинарник). Можно использовать как источник какой-то энтропии (да, есть более подходящие инструменты, но мы пытаемся оправдать инструменты препроцессора): static const char* kId = __TIME__; Проблемы тут понятны. У вас ломаются reproducible builds. При неаккуратном использовании заодно и инкрементальные билды (если у вас один из этих макросов где-то в корневом хедере, который пролезает транзитивно в большую часть проекта). __TIME__ говорит время компиляции конкретного translation unit, так что в большом проекте вы можете получить разные значения в разных TU. У __DATE__ (согласно стандарту C99) всегда фиксированный формат: "Mmm dd yyyy". Причём, если день <10, то между месяцем и днём не 1 пробел, а 2. Так длина константы всегда одинаковая. Но это вполне легко может сломать парсинг. Иногда компиляторы ещё дают __TIMESTAMP__ — дата модификации файла. Там прям и дата, и время может быть: "Sat May 23 11:30:00 2026". @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 352
7
#cpp Day 17. С уровнем 2 мы закончили. Делаем шаг в уровень 3. Есть несколько «служебных» макросов, которые могут помочь сделать что-то полезное. Сегодня __FILE__ и __LINE__. Первый раскрывается в строковый литерал, содержащий имя текущего файла (будет это просто имя или полный путь depends). Второй в целочисленный литерал, обозначающий номер строки, в которой макрос был expanded. Подстановка значений происходит в месте использования, а не определения использующего макроса. Например, можно сделать ассерт: #define MY_ASSERT(cond) \ do { \ if (!(cond)) { \ std::cerr << "Assertion failed: " \ << #cond \ << " in " << __FILE__ \ << ":" << __LINE__ << '\n'; \ std::abort(); \ } \ } while (0) Сегодня для этих целей можно использовать std::source_location. @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 583
8
Меня звали, а я ходил! Поболтали с ПСИвИТ про всякое. Не C++ just in case: https://t.me/psyvit/1142
1 764
9
Доброе что у вас там. Это пост-заглушка. Я не пропустил день марафона. Пост написан. Но сегодня должно быть что-то другое, что не хочется постить когда-нибудь. В худшем случае марафонский пост будет запощен чуть позже. Это сообщение чуть позже удалю. А я сегодня и до конца недели тусуюсь на ACCU on sea. Трипрепорт тоже попробую сообразить поскорее.
1 001
10
#cpp Day 16. Пусть вы хотите определить какой-то enum один разок и потом не упускать все места, где используются его значения (причём используются по-разному). В таком случае нам поможет идиома X macro! Для начала определим базовый макрос, куда мы будем добавлять все значения: #define COLORS \ X(Red) \ X(Green) \ X(Blue) Теперь давайте объявим enum: enum Color { #define X(name) name, COLORS #undef X }; Получим: enum Color { Red, Green, Blue, }; А теперь можем использовать: const char* ToString(Color c) { switch (c) { #define X(name) case name: return #name; COLORS #undef X } return "Unknown"; } Где switch станет: switch (c) { case Red: return "Red"; case Green: return "Green"; case Blue: return "Blue"; } При необходимости добавить новое значение нужно будет только в самый первый макрос! Не будет такого, что имя значения и его строкового представления не будут одинаковы. Иногда X macro могут вынести в отдельный .def файл: // colors.def X(Red) X(Green) X(Blue) // usage somewhere enum Color { #define X(name) name, #include "colors.def" #undef X }; Вот кстати ещё один пример, когда мы можем хотеть инклудить один файл в другой несколько раз. Делать так можно для много чего. Можно описать коды ответов: #define ERRORS \ X(404, NotFound) \ X(500, Internal) \ X(403, Forbidden) Или инструкции какие-нибудь: #define INSTRUCTIONS \ X(Add, 0x01) \ X(Sub, 0x02) Что вам там надо. Дебагать конечно это тяжко. Как и любую макрокучу. Почему X? Видимо исторически. @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 817
11
#cpp Day 15. Как вы могли заметить в макросах из предыдущих дней, иногда мы для удобства разделяем их на строки с помощью \ (backslash). Когда компилятор видит \ + \n (new line character), он их просто удаляет. Так вы превращаете несколько physical source lines в одну logical source line. Что важно, этап склеивания строк идёт до этапа удаления комментариев, так что можно легко напороться на проблему в таком случае: int x = 1; // some logic here \ ++x; std::cout << x; Вы получите int x = 1; // some logic here ++x; std::cout << x; // 1 Хорошая IDE подсветит, но если у вас есть друг, который типа крутой кодит в блокноте, пранканите его как-нибудь. Воткните в конце комментария с отступом в 200 пробелов вправо \. Пусть дебагает. @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 786
12
#cpp Day 14. Бывает, что вы наусловнокомпилировали чего-то. Ифдефами обмазались. И не можете покрыть все-все случаи, которые у вас возникают. Или хотите быть прозрачными с пользователем, что в его ситуации нет пока готового решения. Или может хотите юзеру сообщить, что он делает что-то странное (например, пытается использовать стандарт X на платформе Y). Что делать? Выдать понятное сообщение! В препроцессорном мире вам поможет #error: #error "You did something wrong. Drink beer." @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 684
13
#cpp Day 13. Если вам #define нужен «на время», вы можете потом раздефайнить: #define X X X ... X #undef X X // ⛔️CE Фактически это замена области видимости для макросов. Один из примеров (я не говорю, что хороших, мы тут вообще про макросы говорим, что с ними хорошего?) — получить доступ к членам объектов для тестирования: // in test.c #define private public #include "logic_with_A_class.h" // in some test A a = getA(); assert(a.x == 1); #undef private После #define в вашем инклуде все приватные поля в A станут публичными, что даёт нам возможность к ним обратиться (к полю x в примере). Аналогично вы можете подменить все вызовы функции в подключённом хедере на вашу функцию. #define malloc my_malloc #include "code.h" #undef malloc Конечно, можно сломать что-нибудь: #undef assert #undef errno @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 734
14
#cpp Day 12. Иногда вы хотите написать какой-то общий макрос, который будет работать для произвольного числа аргументов: #define MACRO(...) __VA_ARGS__ __VA_ARGS__ — способ сослаться на все аргументы, обозначенные как ... в макросе. Компилятор просто перечислит их через запятую: #define DEBUG(...) printString(__VA_ARGS__) DEBUG("1", "2", "3"); // printString("1", "2", "3") Частая проблема с __VA_ARGS__ — пустой пак аргументов. #define LOG(fmt, ...) print(fmt, __VA_ARGS__) LOG("str"); // print("fmt", ) Мы получаем пустой пак аргументов, из-за чего получаем лишнюю запятую, после которой ничего нет. Это большая боль. Как жить с этим, расскажу попозже. У макросов часто возникают проблемы с шаблонами, так как параметры макроса сплитятся по запятой: #define A(a, b) f(a, b) A(std::pair<int, int>{1, 1}, 1); И вы получите ошибку компиляции, т.к. в примере выше A получил 4 аргумента: • std::pair<int • int>{1, • 1} • 1 А может принять только 2 (и никак не f(std::pair<int, int>{1) ). Я постоянно стукаюсь об это, когда пишу тесты с gtest. С __VA_ARGS__ это иногда может заработать, т.к. вы теперь все аргументы всегда передаёте пачкой: #define A(...) f(__VA_ARGS__) A(std::pair<int, int>{1, 1}, 1); // f(std::pair<int, int>{1, 1}, 1) __VA_ARGS__ можно использовать для подсчёта кол-ва аргументов в макросе (пример для до 5 аргументов): #define NARGS_IMPL(_1,_2,_3,_4,_5,N,...) N #define NARGS(...) NARGS_IMPL(__VA_ARGS__, 5,4,3,2,1) NARGS(a, b, c) // 3 Раскрывается примерно так: NARGS(a, b, c) NARGS_IMPL( _1 = a _2 = b _3 = c _4 = 5 _5 = 4 N = 3 ) N = 3 @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 891
15
#cpp Day 11. ## — token pasting operator или token concatenation operator. Используется в макросах для склеивания двух токенов (на этапе препроцессинга конечно же). #define MAKE_VAR(x) var_##x int MAKE_VAR(test) = 42; // int var_test = 42; Склеиваются именно токены. После препроцессинга должен получиться валидный токен: #define BAD(a, b) a ## b BAD(+, +) // ++ ✅ BAD(x, *) // x* ⛔️ Используется для генерации чего угодно. Часто думают только про имена функций/переменных/классов, но можно и для операторов, ключевых слов, литералы. @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
1 962
16
#cpp Day 10. Когда вы работаете с макросами, вам может пригодиться stringification operator. #define STR(x) #x Он позволяет превратить аргумент макроса в строку: STR(123 foo real) // "123 foo real" Причём аргумент в таком случае не expandится: #define FOO 123 #define STR(x) #x STR(FOO) // "FOO" Если хотите раскрыть, нужен ещё один уровень: #define STR2(x) #x #define STR(x) STR2(x) STR(FOO) // "123" # почти сохраняет исходный текст (может поудалять лишние пробелы): STR( a + b ) // "a + b" Так что некоторые разные входные данные могут давать одинаковый результат. Надо быть осторожными. Вы можете использовать # для дебажных утилиток: #define PRINT(expr) \ std::cout << #expr << " = " << (expr) << '\n'; PRINT(x + y) // x + y = 12 Эта же фича используется в assert (вы видите упавшее условие в ошибке). @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
2 111
17
#cpp Day 9. До сегодняшнего дня мы были где-то на уровне 1. Сегодня делаем шаг на следующую ступеньку (вниз или вверх, это как посмотреть). Подстановка макросов (expansion) не являются рекурсивной. #define A(x) A(x x) A(x) // A(x x) #define B(x) C(x x) #define C(x) B(x x) B(x) // B(x x x x) Выстрелить себе в ногу становиться чуть сложнее. Или проще. Это опять как посмотреть. @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
2 238
18
#cpp Day 8. В C тоже есть массивы. И работягам тоже хочется знать, сколько в этих массивах элементов. Стандартного решения у ребят там нет, но есть общий подход, перетекающий из кодовой базы в кодовую базу. Зовётся ARRAY_LENGTH/ARRAY_LEN/ARRAY_SIZE/COUNTOF/... Выглядит так: #define ARRAY_LENGTH(x) (sizeof(x) / sizeof((x)[0])) Тут мы полагаемся на sizeof, который, согласно стандарту C99 (моя вольная интерпретация): sizeof возвращает размер операнда (в байтах). Размер зависит от типа операнда. Результат — int. Обычно результат не evaluated и является integer constant. Another use of the sizeof operator is to compute the number of elements in an array: sizeof array / sizeof array[0] Так что sizeof(x) вернёт кол-во байт типа массива (если x — массив int[3], то можем получить (в зависимости от системы) 12). sizeof((x)[0]) вернёт размер типа одного элемента (в нашем случае 4). Вот и получаем 3. На собесах могут спрашивать вопросы с подвохом вида: int x = 10; sizeof(x++); std::cout << x; // result? Конечно, вы на такое не попадётесь и скажете 10, ведь sizeof интересует тип. Он не evaluatит свой аргумент. Но всегда ли это так? Конечно нет! Если ваш аргумент — Variable Length Array, то sizeof придётся вычислить аргумент. Мы можем запруфать это через наличие сайдэффекта: int f() { printf("called\n"); return 10; } int main() { sizeof(int[f()]); } Увидим called в output. https://godbolt.org/z/8G1soa8T1 Теперь срочно требуйте оффер х3 от вашего текущего дохода, ведь собеседующий почти наверняка этого не знает. @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
2 400
19
#cpp Day 7. Вчера мы писали макросы, которые заменяли собой один statement. А что, если я хочу что-то более сложное? Обопрусь на пример от Паши (https://t.me/cpp_durka/23): напишем макрос для инкремента двух переменных. #define INCREMENT_BOTH(x, y) (x)++; (y)++ Тут мы умные. Сразу взяли выражения в скобки. Не поставили в конце ; , чтобы обязать пользователя её поставить самому (для консистентности кода). Но если мы чуть-чуть отступим от глупого использования: if (condition) INCREMENT_BOTH(a, b); мы получим if (condition) (a)++; (b)++; b инкрементится вне зависимости от условия. Или ещё пример: #define MACRO(condition, x) if (condition) std::cout << (x) if (flag) MACRO(flag2, 5); else std::cout << 10; Тут else вдруг начинает относиться к if из макроса, а не изначальному, что очевидно баг. Канонический способ такое исправить: #define INCREMENT_BOTH(x, y) \ do { \ (x)++; \ (y)++; \ } while (0) #define MACRO(condition, x) \ do { \ if (condition) { \ std::cout << (x);\ } \ } while (0) @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
2 196
20
#cpp Day 6. Предположим, мы хотим написать макрос для добавления одной переменной к другой: #define SQR(x) x * x В зависимости от способа использования, вы можете не получить или получить проблемы. В таком коде: int a = SQR(1 + 2); мы на самом деле получим int a = 1 + 2 * 1 + 2; Что не совсем то, что вы ожидали. Для надёжности лучше завернуть аргументы в скобки: #define SQR(x) (x) * (x) Но этого тоже может иногда не хватать: #define INC(x) (x) + 1 int a = 10 / INC(1 + 1); Получим: int a = 10 / (1 + 1) + 1; Что тоже не то, что мы ожидали. Так что адекватный макрос должен как минимум завернуть в скобки каждый отдельный аргумент + завернуть всё выражение: #define SQR(x) ((x) * (x)) Как максимум, ваш x может быть вообще-то функцией с сайд-эффектом: int x = SQR(GetValueFromDbAndPostToKafka()); Так что прям совсем идеально было бы сохранить результат внутри макроса и переиспользовать его. Но я не знаю, как это написать, чтобы было полностью эквивалентно функции. Мб пора начать сворачивать с макродорожки на что-то более современное...... @thisnotes. Patreon, Boosty. Спасибо Artyom Garkavy и niki4smirn.
2 151