this->notes.
4.49K subscribers
36 photos
1 file
414 links
О разработке, архитектуре и C++.

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

Day 33.

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

void foo(int a, int b, float c); // no default args

#define GET_ARGS(_1,_2,_3,x,...) x
#define foo(...) GET_ARGS(__VA_ARGS__, \
foo(__VA_ARGS__), \
foo(__VA_ARGS__,3), \
foo(__VA_ARGS__,2,3),)
foo(1,2,3) // foo(1,2,3)
foo(1,2) // foo(1,2,3)
foo(1) // foo(1,2,3)


@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
🤩16🤪4
#cpp

Day 34.

Сегодня ступаем на уровень 5.

Есть такой неофициальный термин, который используется для обозначения токенов в процессе раскрытия макросов препроцессором: blue paint.

Давайте представим такой макрос:

#define A A + 1

A

1. Встречаем A.
2. Добавляем A в active expansion set (или иначе красим в синий).
3. Заменяем на A + 1.
4. Видим снова A.
5. Но он уже painted blue -> не раскрываем его снова.

Потому получаем A + 1, а не бесконечную рекурсию.

Почему blue?

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

Хотя в стандарте это написано так

If the name of the macro being replaced is found during this scan of the replacement list ..., it is not replaced. Furthermore, if any nested replacements encounter the name of the macro being replaced, it is not replaced. ...


https://eel.is/c++draft/cpp.replace#cpp.rescan-3

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍14🥴31
#cpp

Day 35.

Как мы уже поняли, препроцессор умеет только несколько базовых операций:
• раскрывать макросы
• склеивать токены (##)
• работать с запятыми в списках аргументов (то бишь разделять по запятым аргументы).

Но иногда хочется делать что-то совсем человеческое. Например, написать if:

IF(1)(a, b)
IF(0)(a, b)

Так нельзя. Потому приходится крутиться.

Сегодня придётся напрячь думалку.

Зайдём издалека.
Давайте определим вот такой странный макрос:

#define PROBE ,found

Обратите внимание, что он раскрывается в 2 аргумента для другого макроса: "" и "found".

И сделаем макрос CHECK:

#define TUPLE_AT_1(b,a,...) a // just return 2nd argument
#define CHECK(...) TUPLE_AT_1(__VA_ARGS__,)


Теперь мы можем сделать конструкцию, которая позволит нам узнать, определён ли макрос:

#define FOO PROBE

#define IS_FOO_IMPL(x) IS_FOO_##x
#define IS_FOO(x) CHECK(IS_FOO_IMPL(x), 0)

#define IS_FOO_FOO PROBE

Теперь можем проверять:

IS_FOO(FOO)
// CHECK(IS_FOO_IMPL(FOO), 0)
// TUPLE_AT_1(IS_FOO_IMPL(FOO), 0, )
// TUPLE_AT_1(IS_FOO_FOO, 0, )
// TUPLE_AT_1(PROBE, 0, )
// TUPLE_AT_1(, found, 0, )
// found

или иначе:

IS_FOO(BAR)
// ...
// 0


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

IF(1)(A, B ) // A
IF(0)(A, B ) // B

Сделаем базу:

#define PROBE ~, 1

#define SECOND(a, b, ...) b

#define CHECK(...) SECOND(__VA_ARGS__, 0)

Тогда

CHECK(PROBE) // 1
CHECK(x) // 0


Теперь построим макрос NOT:

#define NOT_0 PROBE
#define NOT(x) CHECK(NOT_##x)

И когда используем:

NOT(0) // CHECK(NOT_0) -> CHECK(PROBE) -> 1
NOT(1) // CHECK(NOT_1) -> NOT_1 doesn't exist -> 0

Вот мы уже научились инвертировать значение.

Осталось сделать BOOL:

BOOL(0) = 0
BOOL(1) = 1

Реализация тривиальная:

#define BOOL(x) NOT(NOT(x))


Последнее, что нам нужно, это сделать IF:

#define IF_0(t, f) f
#define IF_1(t, f) t

IF_0(A, B) // B
IF_1(A, B) // A

Его мы можем собрать через знакомый нам CAT:

#define CAT(a, b) CAT_I(a, b)
#define CAT_I(a, b) a##b

#define IF(c) CAT(IF_, BOOL(c))


Конечно, в коде не обязательно писать IF(0)/IF(1). Вместо значения может быть какой-то другой макрос, который раскроется в конкретное число. Сам макрос может прийти откуда-то снаружи.

Жоска мне кажется. Чего только не придумают.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
🤯264👎2🤪2
#cpp

Day 36.

Иногда используют похожую структуру для реализации INC/DEC:

#define AT_0(a,b,c,d,e,f,g,h) a
#define AT_1(a,b,c,d,e,f,g,h) b
#define AT_2(a,b,c,d,e,f,g,h) c
#define AT_3(a,b,c,d,e,f,g,h) d
#define AT_4(a,b,c,d,e,f,g,h) e
#define AT_5(a,b,c,d,e,f,g,h) f
#define AT_6(a,b,c,d,e,f,g,h) g
#define AT_7(a,b,c,d,e,f,g,h) h

#define CAT(a,b) CAT_I(a,b)
#define CAT_I(a,b) a##b

#define INC_(n) CAT(AT_, n)(1,2,3,4,5,6,7,0)
#define DEC_(n) CAT(AT_, n)(7,0,1,2,3,4,5,6)

#define INC(n) INC_(n)
#define DEC(n) DEC_(n)

INC(6) // 7
INC(7) // 0
DEC(0) // 7
INC(DEC(INC(INC(1)))) // 3


AT_x равносильно взять элемент по индексу x, а INC/DEC просто передают сдвинутое на 1 в нужную сторону количество аргументов в нужном порядке.

Тут фактически мы сделали конечную группу \Z_8.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍11🌚6🤯21👎1🤔1
#cpp

Day 37.

Иногда можно хулиганить.

Как????

Сейчас расскажу.

Давайте возьмём такой странный пример:

#define SCAN(...) __VA_ARGS__
#define EMPTY()
#define LOOP_INDIRECTION() LOOP
#define LOOP(x) x LOOP_INDIRECTION EMPTY()() (x)

SCAN вы знаете. EMPTY() раскрывается в пустоту. А два последних понятно во что раскрываются. Там прям справа написано. Но как это работает, мы сейчас рассмотрим.

Давайте сначала поймём, во что раскроется LOOP(1):

LOOP(1)
// 1 LOOP_INDIRECTION EMPTY()() (1)
// 1 LOOP_INDIRECTION EMPTY()() (1) // раскрываем EMPTY()
// 1 LOOP_INDIRECTION () (1)

Вот тут важный момент: между LOOP_INDIRECTION и () есть пробел. А значит препроцессор воспринимает это как два отдельных токена. из-за чего раскрывать LOOP_INDIRECTION() он не станет. Так что мы на этом останавливаемся.

Теперь давайте просканируем:

SCAN(LOOP(1))
// SCAN(1 LOOP_INDIRECTION () (1))
// 1 LOOP_INDIRECTION()(1) // токены рассматриваются как один поток
// 1 1 LOOP_INDIRECTION () (1)

То есть получили уже 2 единицы в начале.

Обобщая идею можно реализовать EVAL:

#define EVAL(...) EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL2(...) __VA_ARGS__

И для

EVAL(LOOP(1));

мы получим единицы на всю глубину EVAL:

1 1 1 1 1 1 1 1 1 1 1 1 1 1 LOOP_INDIRECTION ()(1)


Можно в итоге написать макрос, которые из

E3(LOOP(f,1,2,3,4,5,6,7,8,9,END))

раскроется в

// f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8) f(9)


@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍14🫡83😱1🌚1
#cpp

Day 38.

Есть целая отдельная библиотека Boost.Preprocessor, в которой используется довольно много подходов, на которые мы с вами смотрели.

Код там забористый.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
🌚7🫡3👍2
#cpp

Day 39.

Начинаем уровень 6.

С помощью макросов можно сделать и какие-то структуры данных (перенося подход всё то же самое можно и в некоторых функциональных языках, и на шаблонах).

Давайте реализуем list:

#define LIST_HEAD(a,b) a
#define LIST_TAIL(a,b) b

LIST_HEAD(1,(2,(3,))) // 1
LIST_TAIL(1,(2,(3,))) // (2,(3,))

И на самом деле этого достаточно.

Можно научиться проверять на конец:

#define TUPLE_AT_1(x,y,...) y
#define CHECK(...) TUPLE_AT_1(__VA_ARGS__,)
#define LIST_END(...) ,0
#define LIST_IS_END(x) CHECK(LIST_END x,1)

LIST_IS_END((9,)) // 0
LIST_IS_END(LIST_TAIL(9,)) // 1

Теперь вспомним пост про реализацию BOOL и IF и поймём, что это можно использовать как условие в тех конструкциях.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍9👎4🗿2🤯1🤓1
#cpp

Day 40.

Представьте, что вы хотите на препроцессоре писать не макросами, а на каком-нибудь Lisp-like языке программирования (ну просто попытайтесь представить, что вы этого хотите).

Есть такая библиотека ORDER_PP:

ORDER_PP(8to_lit(8add(8, 5))) // 13

или

ORDER_PP(
8to_lit(
8mul(
8add(2,3),
8add(4,5)
)
)
) // 45

Язык на самом деле мощнее. Можно объявлять переменные, писать функции, рекурсию, списками пользоваться, ленивые вычисления мутить (вроде как).
Можем вот факториал вычислить (это всё будет вычислено препроцессором!):

ORDER_PP
(
(8define FACT
(8fn (8N)
(8if (8zero 8N)
1
(8mul 8N
(FACT (8sub 8N 1)))))
))

ORDER_PP(8to_lit (FACT 6))

(возможно оно не скомпилируется, но суть такая)

Как это работает? И что это за 8 перед командами?

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

Во время лексического анализа препроцессор не знает ни про какие числа. Он знает про токены языка. А они бывают разные:

identifier
pp-number
string-literal
punctuator
... and others

Нас интересует pp-number. Ниже примеры того, что является pp-number:

123
1e10
0xFF
3.14
1E+10
0x123ABC

1abc
123hello
8X
8let
8mul
0foobar

И пусть вторую пачку компилятор не сможет разобрать как литералы, это всё равно pp-number (данные, а не identifier). А макросы ищутся только среди идентификаторов.

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

ORDER_PP пользуется этой возможностью хранить программу как данные и где-то внутри библиотеки в нужный момент происходит склейка pp-numbers в полноценные макросы:

ORDER_PP_8mul // for example

После этого перехода в мир стандартных макросов уже начинается жоское вычисление всего вокруг.

Чего только не напридумывают.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
🤯20👍4🔥1
#cpp

Day 41.

Кроме хорошо знакомого вам assert был и ещё никак не связанный с первым #assert.

Подразумевалось, что #assert будет помогать генерировать какие-то утверждения для препроцессора. Например

#assert cpu(x86)
#assert os(linux)
#assert this(notes)

после чего вы могли писать

#if #cpu(x86) // true
...
#if #os(linux) // true
...
#if #this(notes) // true
...
#if #os(win) // false
...

То есть с помощью #assert препроцессор записывал в отдельную табличку верные утверждения, а потом умел сообщать, есть ли такой факт. Причём проверка проводилась для predicate(argument).

Ещё утверждения можно было отменять с помощью

#unassert cpu(x86)

И больше это неправда.

Фактически это примитивная логическая база знаний.

Выше я писал «был», потому что в C11 #assert и #unassert не стало. Потому что фактически почти никто не реализовал. Все писали примерно:

#define CPU_X86 1

#if CPU_X86
#ifdef CPU_X86


@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
9🔥1
#cpp

Day 42.

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

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

Для решения таких задач можно подменить стандартный хедер на свой.

Для начала его надо реализовать:

// my_headers/string.h

#pragma once

#include <string.h> // подключаем системный хедер

#define strcpy(...) static_assert(false, "Use strncpy instead")

И начать компилировать вашу программу как-нибудь так:

clang++ -I my_headers ...

(тут мы указываем путь к хедерам, так как наш my_headers расположен первым, то и проверяться он будет первым)

Или через cmake как-то так:

target_include_directories(
my_target
BEFORE PRIVATE my_headers
)

Теперь пользователь будет писать

#include <string.h>

и вместо системного хедера подключится ваш. Круто?

А видите ли вы какую-нибудь проблему в реализации my_headers/string.h?

Например, что внутри мы тоже подключаем <string.h>, который (согласно нашей сборке проекта) будет искаться в my_headers/string.h.

Какая-то рекурсия!

Для решения этой проблемы некоторые компиляторы реализовали нестандартное расширение #include_next. Достаточно в нашем хедере использовать именно его:

#include_next <string.h>

#include_next будет инклудить указанный файл, но поиск файла будет осуществляться после того каталога, в котором находится текущий файл. То есть в нашем примере мы сразу пропустим директорию my_headers и начнём искать где-то ещё. Если у вас нет других string.h до системных хедеров, мы получим нужный файл из стандартной библиотеки.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍20🔥53🤯2🌚1
#cpp

Day 43.

Так как писать код на препроцессоре бывает неимоверно неприятно, есть тулы в помощь. Например, ppstep, который позволяет интерактивно (пошагово) разбираться в раскрытии ваших макросов и всего вокруг.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
😁6🤩4👍3
#cpp

Day 44.

Сегодня ссылка, где Paul Mensodines скрупулёзно поясняет механизм раскрытия макросов: How macro expansion works.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍9
#cpp

Day 45.

Мы уже обсуждали, как сделать LIST, но есть и более свойственная препроцессору структура данных: sequence.

Sequence это просто набор элементов в скобках:

(1)(2)(3)(4)(5)

Тут 5 элементов. Но конечно мы не ограничиваемся числами:

(a)(b)(c)
(int)(double)(bool)
(1)(2)()((),w,())(awoo())(())

Основные операции иногда реализуются просто:

#define SEQ_PUSH_FRONT(x, seq) (x) seq
SEQ_PUSH_FRONT(z, (a)(b)(c)) // (z)(a)(b)(c)

#define FRONT(seq) FRONT_I seq
#define FRONT_I(x) x
FRONT((a)(b)(c))
// FRONT_I(a)(b)(c)
// a

В конце на самом деле надо ещё как-то убрать (b)(c), но сейчас это не так важно.

Другие операции могут быть чуть сложнее, но концептуально там ничего такого. В любом случае самим реализовывать вам не нужно. Берите Boost Preprocessor и крутите

BOOST_PP_SEQ_SIZE((a)(b)(c)) // 3
BOOST_PP_SEQ_ELEM(1, (a)(b)(c)) // b
BOOST_PP_SEQ_PUSH_BACK(seq, x)

Там ещё и итерироваться можно, но про это завтра.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍5🤯2🤓1
#cpp

Day 46.

Шагаем на ступеньку номер 7.

Мы бы могли с вами реализовать итерацию по sequence, но иногда лучше вовремя остановиться и заиспользовать что-то готовое:

#define PRINT(r, data, elem) elem

BOOST_PP_SEQ_FOR_EACH(
PRINT,
_,
(a)(b)(c)(d)
)

Получим

a b c d


Как это реализовано?

Ну вот примерно как вчера откусываете по одному токену из начала и применяете к нему «функтор». Чего непонятно-то???

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍8🗿3👎1
#cpp

Day 47.

Я уже упоминал Paul Mensodines, который написал огромное письмо про механизм раскрытия макросов.

И упоминал про Boost.Preprocessor.

Вот Paul когда-то написал chaos-pp — настоящая глыба в мире препроцессорных библиотек. Там вообще всё что хотите. Рекурсия, высокоуровневые алгоритмы (MAP, FOLD, FILTER, ZIP и др.), лямбды и частичное применение макросов, разные контейнеры (sequence, tuple, list).

Правда её не так часто используют. Просто потому что она очень сложная (== высокий порог входа), без документации, а большинство задач решаются чем-то попроще.

Но как артефакт кайфово.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍122
#cpp

Day 48.

Если вам нужна сравнительно мощная, но более современная альтернатива chaos-pp, стоит взглянуть на Metalang99.

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

Так что Metalang99 — ваш второй выбор (после Boost.Preprocessor).

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
4👍4
#cpp

Day 49.

Одна из моделей, которую можно реализовать на макросах (хотя мы выяснили, что можно любую видимо) — continuation machine. Фактически это штука, в которой вы можете построить пайплайн работы с данными (но такими словами про неё не говорят, слишком современно). На картинке ниже страшная реализация для простого кейса работы с регистрами (разбирать мы ёе не будем, я хочу просто подышать.....).

Кстати репозиторий это что-то вроде оптимизированного интерпретатора Brainfuck. Есть и другие альтернативы: bfi, CPP_COMPLETE.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
10🤯3👍1
#cpp

Day 50.

Макросы можно класть на и доставать из (со) стека:

#define PRAMGA(...) _Pragma(#__VA_ARGS__)
#define POP(m) PRAMGA(pop_macro(#m))

#define X 1 POP(X)
#pragma push_macro("X")
#define X 2 POP(X)
#pragma push_macro("X")
#define X 3 POP(X)
#pragma push_macro("X")
#define X 4 POP(X)
X // 4
X // 3
X // 2
X // 1


Не уверен, что понимаю, зачем это нужно.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍2🤪1