this->notes. – Telegram
this->notes.
4.54K subscribers
25 photos
1 file
330 links
О разработке, архитектуре и C++.

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

1. Недавно думал, почему пустая структура в C++ всегда весит один байт (стандарт требует, чтобы у двух различных объектов были различные адреса).
Давайте подумаем, что может сломаться, если так сделать.
Как в таком случае должны выглядеть массивы? По факту это будет n элементов (число n компилятор часто хранит слева от указателя), расположенных по одному адресу. Т.к. объект нулевого размера не хранит данных, а всю информацию о типе компилятор имеет, можно все вызовы функций делать от одного адреса (это никак на объект не повлияет). В случае, когда мы делаем arr[i], компилятор может отдавать один и тот же элемент. Если объект используется как обёртка над какими-то операциями (в конструкторе/деструкторе), их вызов корректен (правда деструктор дважды по одному адресу это ub, но, предположим, закостылили). При итерировании по массиву мы помним число n, потому понимаем, когда пора остановиться, а когда начинается ub -> можем пытаться оптимизировать. По этим же причинам удаление массива -- операция конечная. Конечно возникают проблемы с реализацией end() для кастомных контейнеров, т.к. элемент, расположенный за последним, становится недостижимым, но условно можно обязать пользователей определять end() как arr + sizeof(char). На этом поиск проблем я остановил. Хотя есть ощущения, что сломается что-то гораздо более сложное (целый мир strict aliasing).
Понятно, что как будто можно было бы в такое научиться, но язык и так очень нетривиален, чтобы ещё такие навороты делать. Хотя зачатки подобного уже есть (EBO/EBCO, [[no_unique_address]], VLA/FAM о которых я уже упоминал).
Вообще зачем об этом думать. Например я в последнее время часто использую такую структуру:

template <typename T>
struct To {};


которая используется для tag dispatching. И раз такой аргумент нужен только для выбора перегрузки функции, можно было бы его оптимизировать в ноль байт. Как-то от этого и раскрутилась мысль.
Кстати Rust умеет как-то с типами нулевого размера работать: раз, два. Может какой-нибудь поклонник этого языка расскажет чуть больше в комментариях : )

2. Я думаю, все знакомы с правилом пяти. Оно гласит, что если вы определяете что-то одного из конструктор копирования/перемещения, аналогичных operator= или деструктора, вы должны написать и все остальные. Это не требование компиляторов, а правило хорошего тона, иначе вступают в силу сложные правила того, как работает компилятор при генерации остальных методов.
Интересно, что такой подход как будто противоречит single-responsibility principle, который говорит, что класс должен отвечать за что-то одно. Поинт в том, что если у вас определён хотя бы что-то одно из указанного, вы должны определить все пять == научить объект манипулировать данными ещё как-то кроме методов, поддерживающих некоторый инвариант класса.
Потому гораздо полезнее правило нуля: не писать никого вообще. И вы наверняка им пользовались. Если у вас в полях класса есть какие-то нетривиальные объекты, компилятор сам справится корректно их скопировать/переместить/удалить. Потому что, на самом деле, писать что-то из пяти этих особых методов вам нужно очень редко (чему конечно противоречит опыт обучения, когда мы постоянно переписываем какие-то (не)стандартные контейнеры и пишем подобное постоянно).

3. Чувак поясняет, почему double/float в любом языке программирования на самом деле не вещественные числа (спойлер: формально с точки зрения математики числа в компьютере не могут представить все вещественные числа). Статью заминусили. Как мне кажется, вполне справедливо, потому что если все в мире понимают о чём речь, зачем воду мутить?

4. Известный факт, что можно объявлять структуры/классы в функциях/методах. Но почему-то нельзя так же объявить шаблон структуры/класса. Причин никаких это запрещать не обнаружил. Даже нашёл бумагу с исправлением: link.
👍2🤔1
#list

1. В последней статье на хабре я писал про sparse set и приводил ссылку на реализацию в folly. Правда он был insert-only. Но ведь одна из его фишек именно в том, что его можно быстро чистить. Потому решил закоммитить это счастье (впервые что-то на кодерском коммичу в чужие репозитории; раньше коммитил в какие-то опенсорс сайтики: англ e-maxx и несколько других про алгоритмы). У меты интересный флоу работы с пр-ами: подписать какое-то соглашение (вроде норм), увидеть упавшие билды (мастер тоже не собирается), ревью, после чего твой пр они переносят во внутреннюю экосистему, где двое суток что-то собирается, и потом закрывают пр и видимо мержат опенсорсную версию folly и внутреннюю. Необычно.
Ещё у меня есть коммит в userver, но честно говоря, не коммит а параша)

2. Недавно увидел фичу на ютубе (хотя есть она давно): если навести на таймлайн видео, над красно-серой полоской будет полупрозрачная серая полоса, похожая на рельеф холмов в профиль. Это частота просмотров различных частей видео. Не знаю, как это реализовано, но, думаю, можно с использованием t-digest. Эта структура данных позволяет относительно дёшево хранить распределения величин. В ней у вас есть какое-то количество центроидов (центроид == диапазон значений + их количество в этом диапазоне). Если при просмотре пользователь задел диапазон, увеличиваете счётчик в центроиде для этого диапазона, после чего можете сгладить эти значения для красивого отображения.
Ещё такую структуру можно использовать для подсчёта перцентилей какой-то метрики. Например для промежутка от 0 до 100 процентов взять 200 центроидов и для любого значения (p50, p95, p98) брать префиксную сумму значений центроидов по этой метрике. Такая сд и реализуется не прям сложно, и является довольно информативной (особенно учитывая, что абсолютная точность вам не нужна).

3. Тут окончили обсуждать C23 (да, C; не C++). На первый взгляд выглядит, как будто C начинает местами догонять — и даже обгонять — плюсы, но мы-то знаем, что пропасть бесконечна…
Вот часть того, что комитет решил добавить в C23 (более полный список можете найти тут):
- #embed — возможность получать данные из внешних файлов на компиляции. По опыту go (go:embed) это и код экономит, и пользователя в рантайме не задевает. Удобно. В плюсах такое тоже тащат (последнее обновление было 20ого апреля);
- __has_include, который подъехал в C++20;
- гарантированное two’s complement для представления чисел;
- несколько новых директив препроцессора: #warning, #elifdef, #elifndef;
- уже знакомые из плюсов атрибуты: [[deprecated]], [[fallthrough]], [[maybe_unused]], [[nodiscard]] и [[noreturn]];
- realloc() с нулевым размером запрашиваемой памяти становится undefined behaviour (Andrei Alexandrescu на одном из докладов на CppCon сказал, что всего два человека в мире знают, когда правильно эту функцию использовать, похехал). Интересно, что такое изменение позволяет делать🤔;
- nullptr;
- немного прокачали енамы;
- constexpr;
- всякие литералы для чисел, разделитель разрядов как в плюсах, удаление триграфов (давно пора, хотя мы когда-то чуть-чуть так лабы в универе обфусцировали), auto (но я не нашёл пруфов, только на reddit писали) и ещё много всего.


Факт, что принимающая ноль аргументов в C функция должна помечаться void довольно известный:

int f(void) {}

Но я никогда не думал, что будет, если написать в плюсовом стиле:

int f() {}

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

void f(int numargs, ...) {}
👍71
#cpp

30 июля была яндексовая C++ Zero Cost Conf. Ощущения смешанные.

Антон Полухин сделал ещё один анонс, как и за пару дней до, о появлении userver в опенсорсе. Вообще странновато, почему они называют это секретной новостью на конфе, если днём ранее выложили пост, и несколькими неделями ранее выложили фреймворк в паблик на гитхаб (который был широко замечен, что как будто их удивляет).
Вообще мы в Лавке живём в юсерверной экосистеме, и по моим ощущениям это приятно. Я конечно не делал чего-то технически сверхсложного, но почти всё, с чем встречался, удобно.

Был приконый небольшой доклад про сжатие вещественных чисел в ClickHouse.

Новости из мира C++ от Полухина. В целом, всё то же, что было в недавней статье про C++23, но меня очень насторожило, что несколько раз звучала фраза вроде “круто, что у С получилось протащить, значит и в С++ появится”. Как-то мда.
Ещё он несколько раз шутил, что вот вы ждали эту фичу 30 лет, и вот она. Но это не смешно, это грустно…

Был доклад с 4мя кейсами про оптимизацию различных ситуаций. Первая — ускорение вычисления расстояния по координатам, хотя Андрей Аксёнов рассказывал об этом ещё в 2019м в рамках целого доклада.
В целом доклад не очень интересный, потому что описанные задачи довольно простые, и крутых идей в решениях я не увидел. Наверное не то, что хотелось бы на конфе такого уровня.

Более менее зашёл доклад про ускорение бинарного поиска.

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

========================
Иногда хочется поделиться рандомными мыслями, очень слабо связаными с прогой или не связаными с ней вообще. И не хотелось бы засорять этот канал, т.к. тут хочется двигаться в сторону образования, а не рассуждений. Потому я сделал @dzikart, чтобы делиться подобным там. Писать туда буду крайне нерегулярно и хз о чём вообще. Если вам интересно, приходите (минимум один человек сказал, что будет читать подобное : ) ).

Ещё у меня есть полумёртвый канал с мемами @memesfromhole.
👍8👏11
#common

Сборщики мусора в других языках.

В Go используется улучшенная версия BF Mark, которая неплохо работает в многопоточной среде. Тут важным достижением сборщика мусора является то, что stop-the-world занимает константное время, как у ZGC в Java.
Единственная возможность настройки сборщика мусора -- это переменная окружения GOGC. GOGC есть размер всей кучи приложения относительно общего размера всех достижимых объектов, т.е. GOGC равное 100 означает, что размер всей кучи приложения на 100% больше размера всех достижимых объектов. Если требуется уменьшить общее количество времени работы сборщика мусора, стоит увеличить GOGC. Если же допускается отдать больше времени сборке мусора, но выиграть себе память — нужно уменьшить GOGC. Вот какая-то история успеха настройки сборщика мусора в Uber.
Тут вот есть какая-то статья под названием Go Does Not Need a Java Style GC. Можно освежить какие-то штуки в памяти и посмотреть на различия языков и, соответственно, их сборщиков мусора.

В одном из самых популярных компиляторов для языка программирования JavaScript под названием V8 используется композиция нескольких сборщиков мусора. Основной абстракцией при сборке мусора является сборка мусора с поколениями. Тут их 3, причём каждое лежит в своём сборщике мусора.
Самые молодые объекты появляются в оптимизированном mark-compact сборщике мусора. Обычно он добавляет освободившиеся блоки во freelist, но иногда принимает решение запустить операцию уплотнения. Следующее поколение -- объекты-подростки, которые содержатся в копирующем сборщике мусора. Последнее поколение живёт в простом mark-sweep сборщике. Из-за того, что он чистится довольно редко, высокая эффективность тут не требуется.

В C# используется оптимизированный mark-compact с тремя поколениями. В D используется сборщик мусора со stop-the-world этапами маркировки и фоновым sweep этапом. В языке программирования Nim существует несколько сборщиков мусора: подсчёт ссылок; mark-sweep; boehm; сборщик мусора Golang; оптимизированный подсчёт ссылок; оптимизированный подсчёт ссылок с разрешением циклов; отсутствие сборщика мусора.
Также сборщики мусора используются в других известных языках программирования: Rust (тут какие-то рассуждения о том, каким его можно сделать), BASIC, Dylan, Lisp и lisp-подобные языки, Lua, ML, Prolog и др. Даже у git есть (он там используется для уплотнения инфы и очистки лишней и запускается автоматически при некоторых популярных операциях, так что вы постоянно его используете : ) )!

=============================
Рано или поздно появится последний пост по сборщикам, в котором посмотрим на сборщики на/для C++. И тут конечно интересны больше технические моменты, чем концептуальные. Если вы не понимаете, как конкретно могут работать те или иные моменты в реализации gc для плюсов, напишите в комментариях. Постараюсь не забыть и написать об этом (если не разберёмся на месте).
👍7🔥1
#list

1. Интересный факт, что вот такой код является корректным:

void f() {
constexpr const MyType& var = some_global_var;
}


Подробнее, как и почему это работает, можно посмотреть в докладе Как объявить константу в C++?

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

int a[] = {1, 2, 3, 4, 5};

struct A {
int a[] = {1, 2, 3, 4, 5};
};


Первое объявление переменной скомпилируется, а структура нет. Компилятор не справится вывести размер для int a[], т.к. у вас могут быть разные конструкторы, из которых размер выведется другой. Вот такой код выведет размер 5 для массива a:

struct B {
B() : a{1,2,3,4,5} {}
int a[];
};


Если же в структуре A сделать a static inline, то всё чинится (понятно почему). Где такое может сыграть роль? На уровне повыше:

class Type {
std::vector v{1, 2, 3, 4, 5};
};


Такое тоже не компилируется, но уже из-за того, что не работает CTAD (примерно по тем же причинам).

3. У знакомого (@KonstantinTomashevich) обнаружился блог про геймдев и C++. Если вам интересна специфика этой области, можно следить за новыми постами на канале.

Я в целом не поклонник геймдева (может как-нибудь расскажу в @dzikart), но какие-то технические моменты были интересны и познавательны.

4. Очень забавный видос (всего 7 минут) про неожиданные забавные конструкции в C++.

5. Хватит ссылаться на TIOBE.

6. В конце зимы узнал про escape analysis в go и всё хотел написать про это пост, но непонятно зачем, если есть хорошая статья.

7. Вкинул полмысли про рабочее время.
👍4🔥2👌1
#algo

Деревья в алгоритмах 1/2.

В алгоритмах часто используются различные деревья (как графовый термин) в виде различных структур данных. Стандартный пример это деревья поиска или кучи. Однако разновидностей структур данных, которые решают те или иные задачи, и в качестве базы (или хотя бы в названии) содержат в себе дерево очень много. Давайте на них и посмотрим.
Опустим подробные описания чего-то дефолтного вроде avl-деревьев, красно-чёрных, или бора (trie). Может только увидим их какие-то прокачаные версии. И не будем особо рассматривать персистентные сд. Там конечно всё очень круто, но для обзорного поста по многообразию ту мач.

Мне идейно очень нравится декартово дерево (пусть оно и довольно известно). Тут и все свойства обычного bst, и это в некотором роде вероятностная структура данных. А если писать неявное, то появляется возможность делать много разных операций как у дерева отрезков, но с большей вариативностью: перемещать части массива данных, менять местами чёт/нечёт и при желании даже копировать отрезки (с персистентной версией).
Splay-деревья тоже bst но обладают замечательным свойством: элементы, которые ищут чаще, со временем оказываются выше, что позволяет тратить меньше времени на их поиск в будущем. Хз, как часто они используются, но в вакууме свойство очень приятное.
Scapegoat tree в отличие от других bst старается не хранить в нодах ничего кроме key-value и указателей на детей и вместо небольшой перебалансировки на каждую операцию делает большие перебалансировки поддеревьев, но редко.

Рядом с обычными bst есть различные версии B-деревьев, которые активно используются в базах данных и файловых системах. Стандартное B-дерево это просто обобщение bst, где разрешается иметь более двух детей/элементов в ноде, что позволяет лучше работать с кешами + ускорить операции работы с большими блоками данных. Следующая версия B-дерева это B+-дерево, которое имеет немного другую структуру и быстрее работает при последовательном обходе. Тут же можно найти B*-tree (которое пытается плотнее упаковывать данные) и B*+-tree (которое, неожиданно, комбинирует фичи двух прошлых). Известными частными случаями B+-tree являются 2-3 и 2-3-4 деревья. Есть ещё какие-то чуть-чуть апдейченные версии вроде counted b-tree.

Далее идут различные боры (или префиксные деревья). Из интересного тут есть цифровые боры, которые позволяют работать с множеством чисел асимптотически эффективнее, чем со стандартными bst (в основном используя факт о максимально значении в множестве). Например это Van Emde Boas tree, X-fast trie и Y-fast trie (эти три умеют делать стандартные операции bst с лучшей асимптотикой, хотя может с памятью похуже), Fusion tree (а это в несколько новых операций). А есть вообще упоротые штуки вроде Hash Array Mapped Trie. Видел ещё ternary search tree, которое бор с не более чем тремя детьми. Это достигается введением нод разного типа и особых правил с ними. При использовании вы иногда просто юзаете ноды как ребро, опуская символ из неё. Ещё есть сжатые боры вроде Radix trie/Patricia tree.

Ещё есть всякие сд, позволяющие эффективно работать со строками Например стандартное суффиксное дерево. Идея у него очень простая: выписываем в бор все суффиксы строк вашего множества, после чего можно очень быстро искать различные подстроки. Даже можно искать строки с опечатками, правда с некоторыми ограничениями (если опечатка в начале, всё ломается). Потому есть специализированные структуры данных вроде BK-tree. Ещё довольно частым кейсом является код Хаффмана.
👍3🤯21
#algo

Деревья в алгоритмах 2/2.

Очень часто деревья используются во всяких геометрических штучках. Самым простым примером кмк является binary space partitioning: вся плоскость это корень; по мере разделения областей к вершине, представляющей какую-то часть плоскости, добавляется два ребёнка (т.е. дерево всегда полное). Примерно таким же, но в терминах множеств объектов в мультиразмерном пространстве, занимаются ball trees.

Ещё сд:
- Rope -- это сд, позволяющая эффективно (ну +-) работать со строками; хотя олимпиадники любят использовать не по назначению, когда надо уметь обращаться по индексам во множестве;
- Zipper -- идиома из функционального программирования. Тут рядом есть Finger tree;
- дерево интервалов (не путать с деревом отрезков);
- или совсем молодое дерево палиндромов.

Пачка фактов:
1. Дерево отрезков можно писать за 2n памяти просто перенумеровав вершины.
2. Есть упрощённая версия реализации красно-чёрных деревьев — left-leaning reb-black trees (левое или может левацкое кчд). Аналогично есть левацкая куча. Ещё как вариация кчд есть AA-tree.
3. Слышал, что в STL любят кчд, потому что у тебя не более двух поворотов на каждую операцию, в то время как условное авл-дерево может сильно измениться.
4. Большинство индексов в базах данных построены на той или иной структуре данных. Тут можно посмотреть на небольшой лес.
5. По факту кучи тоже деревья (в графовой терминологии), но писать ещё миллион букав не хотелось, потому можете сами глянуть тут.

Если вы знаете ещё что-нибудь из этой оперы, смело накидывайте в комментарии.
👍2😢1
#list

1. Нашёл вот такой proposal, где указано множество конструкций из C/C++, которые работают по-разному (и соответственно предлагается это пофиксить). Из интересного тут много кейсов с оператором запятая, скобками и тернарником (везде замешаны lvalue и другие категории значений). Ещё прикольно, что в C тип можно объявлять где хочется:

void func(struct S { int x; } s);

В С++ в отличие от C можно делать так:

void bar(void);
void foo(void) {
return bar(); // OK
}

Выглядит странно, но по факту это можно юзать как замену вызову функции и return в две строки.

Ещё символьные литералы в C это int, а не char (т.е. sizeof(‘a’) == sizeof(int)).
Вот такое тоже в плюсах не скомпилится:

char buf[] = u8"text";

потому что u8 это const char8_t, когда в C это char.

И пустую структуру/enum в C создать нельзя.

Короче тут гора всего интересного. Предлагаю изучить самим (дока правда огромная, но вы справитесь : ) ).

2. Давайте посмотрим на две сигнатуры функций:

template <int I> void f(A<I>, A<I+10>);
template <int I> void f(A<I>, A<I+1+2+3+4>);


При выборе перегрузки компилятор споткнётся, т.к. после инстанцирования они эквивалентны. Однако с точки зрения грамматики это разные сигнатуры, потому заранее такое задетектить не получится. Такой код ill-formed, no diagnostic required (ifndr), т.е. компиляторы вольны обрабатывать такой код любым удобным образом. Можно сказать, что это некоторого рода уб.

3. Узнал об aliasing constructor у std::shared_ptr. Какие-то дикие вещи совсем. Почитать можно вот тут.

4. Интересно, что в go использование указателей вместо значений несёт в себе гораздо бОльшие неприятности, чем в C++. Всё из-за сборщика мусора и escape analysis. Об этом можно почитать вот в такой интересной статье. Там же можно узнать, как эффективнее работать со слайсами и скрывать указатели от escape анализа.

5. Ещё вот нашлась статья под названием “Как мы себя обманываем, только бы продолжать пользоваться Golang”. Не скажу, что она убедительна, но что-то есть.

6. Небольшой тестик, чтобы выяснить, кто вы из C++.
👍7🔥2
#cpp

Несмотря на то, что для программирования не прям часто нужны какие-то нетривиальные/необычные знания, иногда получается к месту использовать какие-то интересные/неожиданные штуки. О том, что мне доводилось юзать/видеть в последнее время, и расскажу ниже.

Известно, что шаблоны функций/классов должны реализовываться в хедерах, потому что при разделении на объявление/определение компилятор не сможет сматчить, где и что ему инстанцировать. Однако это не значит, что нельзя написать шаблон функции в одном месте (например хедере), а инстанцировать его в .cpp (при соблюдении некоторых условий). Делается это при помощи явного инстанцирования в .cpp и extern template в месте, где не нужно инстанцировать этот шаблон:

// .hpp

template <typename T>
T max(T lhs, T rhs) { return lhs; }


extern template int max<int>(int, int); // запретили инстанцирование на месте

// какой-то .cpp

#include <.hpp>

template int max<int>(int, int);


В данном случае мы спасаемся от инстанцирования функции в каждом месте, где происходит #include <.hpp>, и компилятор сможет найти уже инстанцированную вверсию в .cpp. Получается, можно экономить [на спичках] на инстанцировании, если вы знаете, какие типы будет принимать ваша функция. Ещё чуть-чуть можно увидеть у Жени.

Очень рад, что всё время за изучением SFINAE, было потрачено не зря, т.к. приходилось писать какие-то [тривиальные] проверки на наличие поля у структуры в шаблоннах функций, хотя если бы я увидел конструкции с std::declval и самим SFINAE впервые, мог бы знатно охренеть.

Ещё часто приходится юзать стандартные алгоритмы. Видел даже понятный и полезный кейс для std::stable_sort (переупорядочить элементы поисковой выдачи от поискового движка по типу, не потеряв при этом относительный порядок, который задал движок при ранжировании).

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

template <typename T>
struct Type;
[[maybe_unused]] Type<decltype(var)> tt;


Объявляем переменную incomplete шаблонного типа, после чего компилятор сообщит, что не получилось создать такую переменную, и напишет полный тип var без потери важной информации.

Т.к. в коде довольно много всяких std::variant и std::visit рядом с ними, приходится использовать очень крутую обёртку, использование которой выглядит примерно так:

std::visit(Overloaded{
[](const Type1& a) {},
[](const Type2& a) {},
[](const Type3& a) {},
[](const auto& a) {},
}, var);


А реализуется она вот так:

template <class... Ts>
struct Overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
Overloaded(Ts...)->Overloaded<Ts...>;


Кмк оч красиво и просто.

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

В таком ключе примерно понятно, зачем ботать (или хотя бы запоминать ключевые слова) бесконечно неполезные штуки в университете. Вдруг когда-то пригодится : )
👍111
#cpp

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

Немного из метапрограммирования 1/2.

1. type_identity (С++20).

Это очень простой хелпер для реализации множества других type trait’ов:

template <typename T>
struct type_identity { using type = T; };


Например можно использовать вот так:

template <typename T>
struct remove_reference : type_indentity<T> {};
template <>
struct remove_reference<T&> : type_indentity<T> {};
template <>
struct remove_reference<T&&> : type_indentity<T> {};


Конечно же, можно сокращать гораздо более сложные конструкции.

2. void_t (C++17).

void_t это алиас на void:

template <typename…>
using void_t = void;


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

Давайте посмотрим, какие с void_t есть проблемы.

struct One { using x = int; };
struct Two { using y = int; };

template <typename T, std::void_t<typename T::x>* = nullptr>
void f() {}
template <typename T, std::void_t<typename T::y>* = nullptr>
void f() {}

int main() {
f<One>();
f<Two>();
}


При попытке скомпилировать данный код, мы получим redefinition of function f. Но мы же знаем, что всё должно работать! Давайте попробуем заменить void_t на что-то другое (покажем только изменившийся код):

template <typename ...Args> using MyType = void;
template <typename T, MyType<typename T::x>* = nullptr>
void f() {}
template <typename T, MyType<typename T::y>* = nullptr>
void f() {}


Такой код по факту ничем не отличается и не работает (однако ведёт нас к успеху). Давайте попробуем исправить проблему и увидеть, что же было не так. Т.к. void является довольно специфическим типом, давайте заменим его на что-то более обычное:

struct MyTypeT {};
template <typename ...Args> using MyType = MyTypeT;


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

struct MyTypeT {};
template <typename ...Args> struct MyType : MyTypeT {};


всё заработает.

Или если мы реализуем void_t вот так:

template <typename T, typename…>
struct void_t_impl : type_identity<T> {}

template <typename… Args>
using void_t = typename void_t_impl<void, Args…>::type;


и заюзаем такой в первом примере, всё заработает!

Давайте поймём, почему не работает. template type alias’ы с C++11 являются упрощённой конструкцией в языке, специально чтобы все штуки вроде type_trait_t/type_trait_v работали как можно быстрее. И по факту они просто заменяются на то, что алиасят. В C++14 решили сказать, что все dependent штуки из шаблонов templated type alias’ов должны где-то запоминаться (хотя вообще-то никакого запоминания инстанцированных версий у них нет). Получилось противоречие в стандарте, когда вообще-то должно запоминаться и разруливаться в смысле SFINAE, но получается, что просто заменяется на void и ничего не работает. Тогда как со структурой такого не происходит.

Пример отсюда. Более подробное пояснение тут.
👍91
#cpp

Немного из метапрограммирования 2/2.

3. add_lvalue_reference.

Давайте попробуем написать стандартную метафункцию, добавляющую lvalue ссылку к типу (далее будем опускать написание алиасов _v/_t для краткости):

template <typename T>
struct add_lvalue_reference : type_indentity<T&> {};

add_lvalue_reference_t:
int&& -> int&
int& -> int&
int -> int&


А что для void? Для void ответ void& даст ошибку компиляции, что не очень приятно. Можем сделать частичную специализацию. Только стоит помнить про все возможные комбинации cv-квалификаторов, что означает не одну специализацию, а четыре. Плюс такое решение не является устойчивым во времени: во-первых, на текущий момент void единственный тип, для которого не может быть добавлена lvalue reference, но не будет ли в будущем ещё одного, неизвестно; во-вторых, cv-квалификаторы тоже могут расшириться до условных cvlgbtq-квалификаторов, из-за чего придётся дописать множество специализаций. Очень некрасиво и топорно. Хочется какое-то более аккуратное решение, которое будет работать для всех типов, которые вызывают подобную проблему. Можно сделать вот так:

template <typename T, typename Enable>
struct alr_impl : type_indentity<T> {};

template <typename T>
struct alr_impl<T, remove_reference_t<T&>> : type_indentity<T&> {};

template <typename T>
struct add_lvalue_reference : alr_impl<T, remove_reference_t<T>> {};


Работает это просто. Для вызова add_lvalue_reference<SomeType> происходит попытка сматчить вызов и частичную специализацию (т.к. частное всегда предпочтительнее общего). В этой специализации делается remove_reference_t<T&>, что для cv void не скомпилируется -> случается SFINAE -> специализация выбрасывается из списка кандидатов и выбирается базовый шаблон.
Этот код конечно можно упростить:

template <typename T, class Enable>
struct alr_impl : type_indentity<T> {};

template <typename T>
struct alr_impl<T, void_t<T&>> : type_indentity<T&> {};

template <typename T>
struct add_lvalue_reference : alr_impl<T, void> {};


В частичной специализации делается проверка на то, является ли выражение T& well-formed. Если да, то специализация для <T, void> срабатывает.

Аналогичным способом можно реализовать add_rvalue_reference, add_pointer и много других type traits.

Пример отсюда. Там можно посмотреть и несколько других более нетривиальных примеров.

4. Различные вычисления на компиляции могут замедлять время сборки вашего проекта. И вам может захотеться что-то сделать с этим. Но как померять время компиляции? Для clang в этом вам поможет флаг -ftime-trace. Я так долго откладывал рассказ об этом флаге, что кто-то уже это сделал, причём с разбором того, как это работает.

5. И из повседневных мелочей.
- Вот так интересно можно юзать кастомный компаратор для std::set:

bool cmp(X a, X b) { ... }
using Cmp = std::integral_constant<decltype(&cmp), &cmp>;
std::set<X, Cmp> st;


Лёгким движением руки мы сделали из функции структуру.

- Мне нравится, что посчитать N-е Фибоначчи втупую (через N-1 и N-2 числа) на шаблонах работает за O(n), когда через constexpr функцию за O(n!). Просто потому что инстанцирование шаблонов == мемоизация вычисленных значений.
- Вы можете написать почти все type trait’ы с помощью C++ (кроме очень специфических вроде is_union, is_class и др.), но в стандартных библиотеках множество из них реализуются через compiler intrinsics, т.к. это тупо быстрее.


Но вообще лишний раз подумайте, нужно ли вам заниматься чем-то подобным. На самом деле большинство проблем, которые вы можете захотеть решить метапрограммированием/SFINAE, решаются без этих инструментов гораздо проще и понятнее. Или ищите готовое (например в бусте). А лучше переходите на C++20.
👍8🔥1
#common

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

Поэтому и появился этот канал.

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

И поэтому я стараюсь влетать в другие образовательные инициативы, которые мне попадаются. Например этой осенью я буду менторить в яндексовой Школе Бекенд Разработки на треке по C++. Плюсовая ШБР запускается впервые. Там будут реально топовые лекторы, менторы и кураторы, которые широко известны своей деятельностью не только внутри компании, но и в сообществе. А некоторые из них мои непосредственные коллеги, у которых я за срок чуть менее года многому научился (и, чувствую, ещё многому научусь).

Сейчас идёт набор в Школу интерфейсов, а ещё в ШБР на три трека (С++, Python и Java). Флоу примерно такой: на первом этапе лекции и семинары, а на втором работа над реальными проектам в московском офисе Яндекса. Поступить просто: подаёте заявку, делаете тестовое до 14ого сентября 23:59 мск, после чего проходите интервью.

Узнать подробнее и податься можно тут: https://clck.ru/wcP34

===============================
Студентом в ШБР я не был, но пересматривал лекции прошлых лет (которые кстати вы легко найдёте в открытом доступе), и могу сказать, что вся инфа жутко полезна в реальной практике. Так что это очень прикладная программа для интересующихся.
👍20🔥8😁1
#cpp

Сегодня тривиальный пост.

Как распарсится это выражение?

a+++++b;

Варианты:
- ((a++) ++) + b;
- a + (++(++b));
- (a++) + (++b);

Последнее можно отмести сразу, т.к. очень плохо обобщается на более сложные случаи (как вообще такое разбирать?). Давайте выберем из двух других.
В C++ разбор идёт примерно слева направо сверху вниз, причём в текущий токен парсер пытается набрать как можно больше символов (это называется maximum munch, т.е. такая жадная стратегия), потому тут конечно можно распарсить как a + ++++b, но можно добрать ещё один плюс, чтобы получился не бинарный оператор плюс, а постфиксный инкремент, потому верен первый вариант. Ну и тут же легко понять, почему это не скомпилируется: a++ это prvalue (значение оригинального идентификатора a изменяется, но возвращается копия), когда как инкремент/декремент требуют lvalue в качестве аргумента.

Обратите внимание, что если тут расставить пробелы, то всё будет работать как надо (потому что пробел заканчивает набор текущего токена).

По примерно тем же причинам до C++11 нельзя было писать что-то вроде

std::vector<std::vector<int>> v;

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

Я уже чуть-чуть писал про most vexing parse. Давайте посмотрим ещё чуть-чуть.

Когда вы пишете вот такое в глобальном неймспейсе:

Type func(OtherType);

вы сразу понимаете, что это объявление функции. Однако бывают более неочевидные (просто потому что вы имеете в виду другое) кейсы, когда компилятор так же воспринимает объявление:

class X {
public:
X() {}
X(Type obj) : obj(obj) {}
private:
Type obj();
};


Вполне можно думать, что раз приватное и мы инициализируем, то это поле, но это так же объявление функции с именем obj, из-за чего такой код не скомпилируется.

Или более дикие примеры:

struct X {};
struct Y {
Y(const X&) {}
void f() {}
};


int main() {
Y y(X());
y.f();
}


Код не скомпилируется на выражении y.f(), из-за того, как мы инициализируем объект y: на самом деле это тоже объявление функции y, возвращающей Y и принимающей X. По-хорошему конечно использовать фигурные скобки для инициализации: Y y{X()}, — а можно поставить ещё одну пару скобок вокруг X: Y y((X())).

Короч будьте внимательны и читайте ошибки компиляции.
👍3👏2🤯1
#list

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

Предлагается ввести новое ключевое слово epoch, которое опционально можно писать в начале вашего модуля (именно модуля):

epoch 2023;

В пропозале предлагается следующий пример: в C++ существуют неявные приведения. Потому такой код компилируется:

module f;
void f(float) {}
...
f(3.4); // 3.4 is double


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

epoch 2023;
module f;

void f(float) {}
...
// f(3.4); // CE
f(3.4f); // success


Т.к. это модулеспецифичная опция, предполагается, что вы можете безопасно без проблем модуль за модулем мигрировать на новую эпоху (мигрировать бы на модули сначала😢).

Новая эпоха будет появляться с появлением нового стандарта и выражаться четырёхзначным числом, которое является годом выхода этой эпохи. Соответственно вы просто сможете компилировать свой код в привычном формате -std=standard.

Однако эпохи не являются инструментом для ломания ABI. Они лишь предлагаются как некоторый инструмент для другого преобразования кода в AST, после чего компиляция идёт обычным способом. Тут конечно грустно.

В пропозале есть ещё несколько примеров, которые можно починить эпохами. Вот несколько интересных (обратите внимание, что пропозал предлагает только эпохи и примеры ниже демонстрируют, какие необычные изменения можно будет ввести в язык без боли):
- оставить переменную неинициализированной:
// int i; // CE
int i = void; // OK, unitialized

- запретить устаревшие конструкции:
// int a0[]{1, 2}; // CE
std::array<int> a1{1, 2}; // OK
// typedef int I0; // CE
using I1 = int; // OK

- ввести новые/переименовать существующие ключевые слова (co_await -> await, co_yield -> yield);
- форсить использование nullptr вместо NULL или 0;
- форсить использование break и fallthrough в switch;
- юзание условного std::movable_initializer_list вместо обычного там, где это возможно;
- форсить для конструкторов использование одного из двух слов: explicit, implicit;
- форсить одно из слов const/mutable для переменных;
- следить за выполнением некоторых core guidlines на уровне языка;
- добавить ещё несколько атрибутов для методов/функций вроде [[accessible_until_epoch(X)]] и [[accessible_since_epoch(X)]]

и другие.

Короч прикольно, но шо-то как-то не прям то, что хотелось. Опять же, ABI либо придётся когда-то сломать (уже может быть довольно поздно), либо мы все так и умрём в пережитках прошлого (или нет, я хз).

Proposal.

2. Небольшая статья про то, как можно использовать макрос __has_include и feature test макросы.

3. Какая-то забавная интересная статья про то, как чуваки своего начальника продавали.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍71
#list

0. Как-то я в недрах миллиона инфы потерял ещё один пропозал, который стоило бы упомянуть в последней статье про обработку ошибок.

operator ??.
Суть примерно та же, что у operator try, но автор предлагает не использовать эту форму, т.к., несмотря на то, что подобное встречается в других языках, в C++ try очень сильно ассоциируется с исключениями. Это может запутать, т.к. цель подобного оператора только лишь пропагация ошибки выше, и семантически ничего общего с исключениями такой оператор не имеет.
Выглядеть использование должно примерно как в Rust:

int f = foo()??;

1. Тут начали подъезжать первые доклады с CppCon’22. Первым по традиции был доклад Bjarne Stroustrup. Но его я не очень осиливаю послушать, мб потому что у него какие-то очень абстрактные рассуждения о судьбе вселенной, когда мне больше заходят какие-то конкретные штуки. Например доклад Herb Sutter о Cpp2. Он написал экспериментальный компилятор cppfront для нового синтаксиса C++, который тем не менее полностью поддерживает стандартную версию языка. Можно увидеть конструкции вроде

main: () -> int {
std::cout << ”Hello, World!”;
}


new<std::string>(); // шаблонный new возвращает std::unique_ptr

f: (inout: s: std::string) = s = “[" + s + “]” // упрощённый синтаксис для однострочников

И ещё много других набросов.

Его компилятор форсит использование новых инструментов вместо устаревших, следование core guidlines и прочее. Из интересного ещё запрещает арифметику указателей, полный запрет использования слова NULL (потому что у него не очень хорошая семантика, которая не несёт ничего хорошего в ваш код). Ещё это происходит через полный запрет некоторых старых инструментов. Например препроцессор. На этом слайде можно увидеть как много в C++ сложностей, которые в cpp2 неприемлемы.

Вообще синтаксис местами спорный, но как будто следует последним модным веяниям в этом месте, из-за чего [кажется] парсинг кода становится гораздо более простым.

Очень много говорил про проблему Python 2/3, когда инкрементальный переход с одной версии на другой невозможен. Про то, как важно сделать C++ более безопасным и простым. Про то, как подобный подход сможет упростить обучение языку.

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

Тут можно найти больше примеров.

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

cpp2 можно потрогать на годболте. Держите.

2. [из архива] Приконый доклад Nicolai Josuttis о том, как он пытался написать эффективную реализацию конструктора для структуры с двумя строками (и у него долго ничего не получалось).
👍6🔥1
#cpp

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

Заведём структурку, в которую будем складывать необходимую информацию:

struct AllocationInfo {
const char* function;
unsigned int line;
unsigned long count;
unsigned long bytes;
AllocationInfo* next;
};


Это базовая нода листа, в которой будем хранить всю необходимую информацию об аллокации. Ну и заведём голову листа:

static AllocationInfo root;

Теперь напишем макрос, который будет служить базой для функции аллокации:

#define MYALLOCATE(n, name) \
[name_data = name] (size_t bts) { \
static AllocationInfo here; \
static bool firstCall = [name_data]() { \
/*first call, initialize the struct*/ \
here.function = name_data; \
here.line = __LINE__; \
here.next = root.next; \
root.next = &here; \
return true; \
}(); \
/* Cool, now deposit info about calls */ \
++here.count; \
here.bytes += (bts); \
return malloc(bts); \
}(n)


Создаём новую ноду листа, которую инициализируем при первом появлении в строчке, после чего обновляем инфу если надо. Ну и макрос-обёртка:

#define ALLOC(n) MYALLOCATE(n, __PRETTY_FUNCTION__)

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

auto p = &root;
p = p->next;
while (p) {
std::cout << "Func: " << p->function << std::endl;
std::cout << "Line: " << p->line << std::endl;
std::cout << "Times: " << p->count << std::endl;
std::cout << "Bytes: " << p->bytes << std::endl;
p = p->next;
}


Демо. И можем увидеть какие-то такие результаты:

Func: int main()
Line: 46
Times: 1
Bytes: 128

Func: void some_func(size_t)
Line: 36
Times: 2
Bytes: 48

Func: int main()
Line: 40
Times: 1
Bytes: 8


Видим, что в main в строке 46 за один раз было выделено 128 байт; в some_func в строке 36 за два раза 48 байт и в main в строке 40 за раз 8 байт.

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

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

Можно немножко зарефакторить код, юзая std::source_location. Оставим как упражнение пытливому читателю.

===================================
Что-то в последнее время начала ощущаться усталость от 2.5 лет нехождения в отпуск. Потому дольше обычного не было постов (ну и ещё из-за запойного просмотра Доктора Хауса). Но, думаю, скоро получится отдохнуть и всё вернётся в нормальный ритм.
👍15
#common

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

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

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

По последнему кстати можно затрекать молодого/неопытного чувака. Как вы сообщаете кому-то о проблемах? Не набивший руку на общение чел вкинет что-то вроде “это не работает”. Чувак с опытом, потративший на решение проблем мало-мальски приличное время, принесёт больше инфы: что он пытается сделать, что не получается (логи/ошибки), может почему не получается (он же подумал перед тем как спросить?), как он пытался её решить и почему это не помогло. Так получается сформировать более общее представление о проблеме и не тратить на это драгоценное время (может вы конечно не цените своё, не подумайте о чужом).
Если вы когда-нибудь задавали вопрос на stackoverflow или другой платформе из этой серии, там чётко написано как правильно это делать. Помните об этом, даже когда напоминалки перед глазами нет.

Ну и помните про nometa.xyz и neprivet.com.
👍14
#cpp

Быстропост.

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

std::function<int(int, double)> f;

С точки зрения реализации вашего класса это выглядит как-то так:

template <typename T>
struct A {};

template <typename Ret, typename… Args>
struct A<Ret(Args…)> {};


Теперь можно в шаблон A пихать выражения вроде сигнатуры функции.

Кроме std::function такое используется в std::is_function (реализация которой тупо перебор всех возможных видов функций; неудивительно, но дико).

2. Вычитал, что выражения

std::is_same_v<char, signed char>;
std::is_same_v<char, unsigned char>;


вопреки ожиданиям возвращают false. Всё потому что char это отдельный тип, который не является (без)знаковым (он какой удобно компилятору на конкретной оси). Ну и signed char не попадает под strict aliasing rule, т.е. unsigned char ближе к char функционально, но формально это разные типы.

3. Возможная реализация std::is_signed/std::is_unsigned выглядит забавно. Тут юзается факт, что беззнаковые числа переполняются:

namespace detail {
template
<typename T, bool = std::is_arithmetic<T>::value>
struct is_signed : std::integral_constant<bool, T(-1) < T(0)> {};

template
<typename T>
struct is_signed<T,false> : std::false_type {};
} // namespace detail

template
<typename T>
struct is_signed : detail::is_signed<T>::type {};


По факту проверяется верность выражения T(-1) < T(0), которое для signed типов верно, а для unsigned типов корректно переполняется (потому что переполнение unsigned типов по стандарту не ub) и становится false.
У unsigned выражение наоборот: T(0) < T(-1). Работает аналогично: либо false, либо переполнение и true.
👍14🤯61
#common

О подходах к реализации шаблонов/дженериков.

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

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

В Java используется стирание типов (type erasure).

public class A<T> {
private T obj;


public A(T t){
obj = t;
}

public T getObj() {
return obj;
}
}


Однако в рантайме дженерик-типа не остаётся. Класс будет существовать в таком виде:

public class A {
private Object obj;


public A(Object t){
obj = t;
}

public Object getObj() {
return obj;
}
}


Т.е. в дженерик-типе/функции T заменяется на Object, а в месте использования происходит следующее:

A<String> a = new A<>("qwe");
var aa = a.get();


заменится на

A a = new A("qwe");
var aa =
(String) a.get();

Т.е. снаружи есть информация о том, какой тип отдавался в дженерик, что позволяет сделать нужный каст. И тут есть одно очевидное преимущество: вы можете не перекомпилировать пакет с определением дженерика. Однако сразу можно заметить и недостатки: т.к. происходит стирание типа до Object (который является родительским типом для всех типов, кроме примитивных), дженерики нельзя использовать с примитивными типами (ха-ха, лохи). Потому и появились типы-обёртки вроде Integer, Boolean и прочих.

Хотя это пытаются пофиксить. Как база используется новый вид классов (неожиданно, примитивный). Вы объявляете свой класс с ключевым словом primitive. Для таких классов появляются правила конвертации в примитивные типы, запрет на наличие значения Null и другие разные правила. Но сейчас не об этом. Пропозал (хз насколько такой термин в сообществе джавы применим).

Когда у вас есть примитивные классы, вы можете интерпретировать примитивные типы вроде int/double и другие как примитивные классы, что позволит реализовать дженерики и для них. Получается, что костылят :(
Хотели ввести в Java 19, но не успели, так что мб к Java 20 получится. Линк.

В Rust дженерики работают примерно как в плюсах: на один крейт (единица компиляции в Rust) тип в дженерик подставляется только один раз (грубо говоря, копирование кода для каждого типа в дженерике). Если дженерик из крейта 1 используется в крейте 2, то будет своя инстанцированная версия в крейте 2.

В go всё примерно так же за тем исключением, как работают интерфейсы относительно трейтов в rust. Тип на “подходимость” к интерфейсу проверяется автоматически, тогда как тот факт, что тип удовлетворяет трейту, надо указывать ручками.

UPD: тема go не раскрыта. Чуть позже вкину подробный пост про это в качестве исправления.
👍7🐳41