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
#common

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

Два основных подхода это использование offset и курсора.

Первый, это просто указание как много данных стоит пропустить от начала выдачи. После того, как вы получили первую 1000, можно запросить тот же запрос с offset=1000 и получить следующие 100 значений (чиселки условные конечно).
Оффсет может быть чуть более сложным, чем просто число (например номер страницы и номер последнего элемента на последней странице), но суть одна -- сдвиг от начала.
Проблема тут в том, что это может быть неэффективно. Например, если данные берутся из базы: вам необходимо сделать ваш [тяжёлый] запрос (мб с сортировкой и прочим) с чем-то вроде OFFSET 1000 LIMIT 100, читай отбросить первую тысячу результатов (== выкинуть работу в мусорку) и взять следующую нужную пачку. Чем больше данных и чем меньше размер одного батча, тем больше работы мы делаем зря. Ещё данные приходится грузить в память (что не так плохо, когда у вас какой-нибудь новостной сайт без персонализации, но для чего-то более сложного не катит). И не оч работает, когда есть вставки/удаления. Не круто.

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

[запрос: моло]

cursor: {
молоко: {
skip: 12,
min_relevant: 0.57,
},
молочное: {
skip: 27,
min_relevant: 0.13,
}
}


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

Детектить конец пагинации можно разными простыми способами: дополнительное поле, присылать null значение для курсора или повторять его.

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

Links:
1. Five ways to paginate in Postgres, from basic to exotic.
2. How to Implement Cursor Pagination Like a Pro.
3. Paginating Real-Time Data with Cursor Based Pagination.

======================
Блин вас уже полтысячи. Неожиданно и приятно. Всем спасибо.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20🔥5
#cpp

Лямбды 1/2.

1. Все мы помним, что список захвата работает только для локальных перенных. В то время как статические и глобальные переменные захватываются, грубо говоря, автоматически. Такое верно и для constexpr переменных, потому что в итоге у вас в коде будет подставлено конкретное значение. Логично и просто.
И тут стоит помнить, что константные интегральные типы неявно являются constexpr:

const int i = 1; // implicitly constexpr
const std::size_t j = 1ull; // implicitly constexpr
const float f = 1.f; // not implicitly constexpr


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

2. Immediately Invoked Function Expressions (IIFE).
Это довольно полезный подход использования лямбд, когда они вызываются сразу же:

[]{ std::cout << “ASD”; }();

Конечно в таком виде такое не используется. Гораздо более полезным такое будет, когда хочется инициализировать объект каким-то нетривиальным способом:

A a;
if (condition) {
a = firstWay();
} else {
a = secondWay();
}


Мы разделили инициализацию объекта на две части. Между ними он может быть невалидным, и использование до ифа может привести к уб. Как-то небезопасно и не оч поддерживаемо.
А ещё класс может не иметь конструктора по умолчанию, что в целом делает подход выше невозможным.
А ещё инициализируемая переменная может быть помечена const, что так же не даёт инициализировать её в ифе (в отличие от const в C++, в Java ключевое слово final, которое говорит, что переменной можно присвоить что-то только единожды -> код выше в случае final переменной заработает).

Если код достаточно простой, можно заюзать тернарный оператор:

const A a = condition ? firstWay() : secondWay();

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

const A a = [condition] {
if (condition) {
return firstWay();
} else {
return secondWay();
}
}();


Такой подход хорошо подходит для всех случаев, когда у вас действия происходят рядом с perfect forwarding:

std::vector<A> v;
v.emplace_back([condition] {
if (condition) {
return firstWay();
} else {
return secondWay();
}
}());


и другими функциями того же рода.

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

3. Вот тут (пункт 3) писал про приведение лямбд с пустым списком захвата к указателю на функцию.
Интересно, что такой хак работает не всегда:

auto* fptr = +[](auto i) {
return i * i;
};


Что в целом понятно и ожидаемо.

4. Вот тут таска (и чуть ниже ответ) про то, как сообразить call_once и call_n с помощью лямбд.
Кстати прикольно, что на нескольких различных докладах на CppCon чуваки говорили, что на ручных бенчмарках лямбда, инициализирующая статический объект, — более эффективна как реализация std::call_once чем собственно реализации в версиях стандартной библиотеки.

5. С появлением generic лямбд с C++14 вы можете писать примерно все обычные для шаблонов штуки вроде auto&& для универсальных ссылок или даже auto… для variadic templates. Ну и лямбды друг в друга можно передавать. Удобно.

6. Variable template lambda.
С C++14 можно создавать шаблонные переменные. И, что интересно, если вы используете лямбду для инициализации такой переменной, вы имеете доступ к шаблонному аргументу переменной:

template <typename T>
constexpr auto c_cast = [](auto x) {
return (T)x;
};


Тут, грубо говоря, вы получаете не просто шаблонный operator() в вашей лямбде, но и сам класс лямбды становится шаблонным.
5👍10🔥31
#cpp

Лямбды 2/2.

7. Init capture optimisation.
Если у вас есть какой-то код вроде

std::find_if(v.begin(), v.end(),
[&pref](const auto& s) {
return s == pref + “bar”;
});


то вычисление pref + “bar” будет делаться на каждой итерации. Потому лучше вынести его в список захвата:

std::find_if(v.begin(), v.end(),
[str = pref + “bar”](const auto& s) {
return s == str;
});


что позволит немного сэкономить на вычислениях.

8. С C++17 лямбды могут быть constexpr.

auto f = []() constexpr {
return sizeof(void*);
};

std::array<int, f()> arr = {};


Аналогично с consteval с C++20.

9. Вот тут можно почитать, как написать удобный хелпер Overloaded для работы с std::variant и std::visit с помощью наследования от лямбд.

10. C С++20 лямбды умеют в конструирование по умолчанию, что помогает не передавать их в места вроде делитеров для умных указателей или как компараторы в конструкторы std::set/std::map и отдавать только тип в шаблон.

11. Забавный факт.

template <auto = []{}>
struct X {};

X x1;
X x2;

static_assert(!is_same_v<decltype(x1), decltype(x2)>);


Этот код успешно скомпилируется, т.к. при каждом обращении к типу, используется новое выражение []{}, которое имеет абсолютно новый тип относительно прошлых выражений []{}. Соответственно каждый раз шаблон X будет инстанцирован разными типами. И потому типы переменных не совпадают.

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

UPD.
Почему-то потерялся линк на оригинал, откуда взял часть инфы.
Доклад Тимура Думлера на CppCon 2022.
👍4🔥21👏1
#list

Микропост.

1. Женя написал крутой пост про способы ускорения компиляции. Докину ещё пост на хабре про hidden friends.

2. Тут собрали какое-то количество предложений по улучшению structure binding. Есть довольно приятные и интересные штуки, которые даже комитет рассматривал, но пока, как я увидел, ничего никуда не протащили (хотя обсуждения местами ведутся довольно давно). Можно посмотреть для расширения сознания.

3. Можете посмотреть очередной доклад Аксёнова про то, как не говнокодить (ничего конкретного вы не услышите; просто набросы).

4. Если приглядеться во множество примитивных типов в Java, можно увидеть отсутствие unsigned типов. Вроде как это потому что один из авторов языка считает, что мало какие разработчики на C способны рассказать, как работает арифметика с unsigned числами, поэтому такого в Java нет в принципе. Странный поинт честно говоря, но факт есть факт.

5. Какой-то прикольный симулятор тимлида. После работы прогером можно поиграть в менеджера. Хех.
👍10
#cpp

Чуть-чуть о сборке мусора на C/C++.

Почитайте тут небольшую статью на хабре про какую-то базовую имплементацию.
Если кратко, то:
- есть глобальное хранилище всех выделенных объектов;
- есть хранилище корневых объектов;
- с каждым объектом хранится какая-то метаинформация;
- есть жёсткие (которые влияют на сборку объекта) и слабые (которые нет) ссылки;
- есть какой-то способ проверки достижимости объектов. В Unreal Engine например надо пользоваться макросами UPROPERTY [1], с помощью которых сборщик мусора ходит по указаным в макросах указателям и тем самым получает ссылки на объекты, которые связаны с текущим. В случае статьи выше применяется хак, что все интересующие сборщик мусора объекты лежат в начале рассматриваемого объекта, что позволяет пройтись по ним, сделать reinterpret_cast и сделать что требуется (это не всегда работает, но тут чисто proof of concept).

Boehm-gc это прям классический сборщик мусора. Тут прям чуваки реализовали аналог malloc, после которого ничего не нужно удалять. Концептуально всё примерно так, как описано выше (лист блоков памяти, мета в хедере выделяемых объектов, mark-sweep), но сильно сложнее : ) Не будем останавливаться (код читать не советую: стандартное месиво директив препроцессора, C и других радостей жизни).

Sutter в своём проекте gcpp вводит такие объекты как deferred_heap, deferred_ptr и deferred_allocator.

deferred_heap -- регион памяти с объектами, на которые можно уметь указывать с помощью deferred_ptr. Вы можете создавать объекты с помощью метода make<T>() и уничтожать их на конкретной куче изолированно с помощью collect(). Концептуально вы можете завести такую кучу на множество объектов одного типа, на каждый объект свой или на любое подмножество объектов/типов. Как вам удобно.

deferred_ptr почти фулл такой же, как std::shared_ptr за тем исключением, что он умеет в отложенное удаление в зависимости от того, что же происходит с его кучей (ну и там немного по-другому реализован aliasing constructor). Куча кстати фиксируется единожды и поменять её указатель не может.

deferred_allocator это обёртка для своей кучи для использования в контейнерах.

По факту проект Sutter’а это подсчёт ссылок с удалением недоступных циклов. Однако когда “классические” сборщики мусора концептуально управляют сырой памятью, deferred_heap это скорее об управлении существующими объектами (в основном корректным вызовом деструкторов неиспользуемых объектов).
Код у него довольно понятный и читаемый. Можете почекать.
И оч много разной инфы в readme. Тоже можно посмотреть.

Тут можете почитать про проблемы (хех), которые могут возникнуть из-за наличия штук для сборщика мусора в стандарте C++11-20.

[1]. Сборка мусора в UE.

UPD.
Забыл про широко юзаемый сборщик на плюсах: olipan gc — сборщик мусора в chromium, который связан с V8.
🤯9👍5
#common

Про компиляцию в общих чертах.

Представим выражение (строка кода):

pos = init + rate * 60

Что с ним произойдёт при компиляции?

👉 Лексический анализ — читаем поток символов и группируем их в лексемы (значащие последовательности). Для каждой лексемы получаем токен вроде <имя, значение>.

<id, 1> <=> <id, 2> <+> <id, 3> <*> <60>

id в данном случае указывает, что информацию о токене нужно искать в глобальной таблице символов (там инфа о всех встреченных объектах). Номер 1/2/3 — индекс объекта в этой таблице.

<60> можно представить в более общем виде как <number, 4>, но для простоты не будем.

Такой набор токенов и отправляется на следующий этап.

👉 Синтаксический анализ позволяет построить из токенов промежуточное древовидное представление, которое описывает грамматическую структуру потока токенов (обычно тут речь о синтаксическом дереве или ast). Получаем что-то такое:

=
/ \
<id, 1> +
/ \
<id, 2> *
/ \
<id, 3> 60


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

=
/ \
<id, 1> +
/ \
<id, 2> *
/ \
<id, 3> int_to_float
\
60


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

👉 Генерация промежуточного кода.
В процессе трансляции исходного кода в целевой, компилятор может несколько раз генерировать различные промежуточные представления. В нашем примере можем получить что-то такое:

t1 = int_to_float(60)
t2 = id3 * t1
t3 = id2 + t2
id1 = t3


👉 Оптимизация кода.
Конечно, сгенерированный выше код кажется нам довольно неестественным. Его хочется упростить. Это и происходит на этапе оптимизации (пока это машинно-независимые процессы). Можем получить следующее:

t1 = id3 * 60.0
id1 = id2 + t1


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

Ещё такой процесс может проходить несколько раз, т.к. какие-то сделанные оптимизации открывают возможность для новых.

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

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

Если верхнеуровнево, то компиляторы для плюсов состоят из несколько крупных этапов:
1. Frontend: препроцессинг, лексический, синтаксические и семантический анализ, построение high level intermediate representation (hir).
2. Middleend + backend: оптимизации hir, оптимизации mir, оптимизации lir, кодогенерация.

Frontend для каждого языка свой. Middleend и backend работаю с ir, что позволяет переиспользовать их для разных языков.

Послушать про то, как работают компиляторы для C++, можно в плейлисте. Правда речь о toolchain, но от этого только интереснее : )

Ещё можно посмотреть пару лекций про LLVM IR. И почитать пару постов на похожую тему в @cxx95: раз, два.

P.S. пример из dragonbook.
👍12🔥1
#cpp

1. Хочется укрепить понимание того, что же такое модификаторы доступа (public, protected, private), потому что в последнее время вижу тут-там некоторое непонимание у молодых особей, изучающиъ C++.
Это именно модификаторы доступа. Они всего лишь не дают напрямую обратиться к члену/методу класса. Но это не значит, что вы совсем не можете работать со скрываемыми объектами.

class A {
struct T {};
public:
T getT() { return T{}; }
};


// A::T t; // ce
A a;
// A::T t = a.getT(); // ce

auto t = a.getT(); // ok

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

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

struct A {
virtual void f() = 0;
};

struct B : A {
private:
void f() override { std::cout << 1; }
};


A* a = new B();
a->f();


Вроде как f() в B помечена private, но т.к. это требование проверяется на компиляции, а виртуальные штуки работают в рантайме, будет успешно выведена единица. Кстати, интересно, могут ли статанализаторы такое трекать как говнокод🤔 Не пробовал. Но может вы потрогаете и расскажете.

2. В C++11 появились типы фиксированного размера. И почему-то стало принято юзать их вместо стандартных int, long long, и прочей братвы. Мне не очень понятно почему. Ведь вам на самом деле редко нужны типы фиксированного размера. Часто вам хватает знания, что конкретный тип удовлетворяет каким-то ограничениям сверху/снизу. И часто можно просто поставить какой-нибудь static_assert на то, что у вас CHAR_BIT равен 8 или сколько вам там нужно. Короч кмк это специфический инструмент, непонятно почему ставший popular.

3. Можно почитать Женин довольно понятный пост про ABI и почему его не хотят ломать (там много ссылочек с доп инфой).
👍62🔥2🤔2
#cpp

RVO/NRVO 1/2.

Давно хотел рассказать про всякие оптимизации, которые делаются в плюсах. Тут будет пост про RVO/NRVO, а позже про что-нибудь ещё.
Может в начале будут какие-то неявные допущения или грубые формулировки, но к концу поста постараюсь донести все детали, чтобы картина сложилась.

Для начала про терминологию в стандарте. Формально есть общий термин copy elision, который в целом о том, чтобы не делать лишних копий объекта и создавать его сразу там, где нужно. Например ниже примеры copy elision:

T t = T(); // not T() + operator= but only T();
T t = T(T(T(T(T(T(T(1))))))); // not T(1) + many T(const T&) + operator= but only T(1)


Вот что говорит стандарт ([class.copy.elision]):

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.

Т.е. компилятор в праве применять эту оптимизацию в обход copy-/move-кторов, даже если в них или деструкторе есть какие-то сайдэффекты.

Так же тут есть 4 пункта. В первом говорится, что тип инициализирующего выражения и тип переменной с итоговым результатом должны совпадать. Это главное условие для RVO/NRVO.

RVO/NRVO это частные случаи copy elision, которые происходят в результате возврата чего-либо из функции. Далее мы будем пользовать примерно таким классом, чтобы понять, что происходит с объектами:

#define TELL(str) { std::cout << str << std::endl; }
struct S {
S() TELL("def")
S(const S&) TELL("cp")
S(S&&) TELL("mv")
};


Так же есть такой термин как temporary materialization – принцип, согласно которому prvalue-выражение не создаётся физически в памяти до тех пор, пока оно не будет присвоено в не prvalue-объект.

Return Value Optimization по сути и есть пример temporary materialization. Посмотрим на базовый пример (godbolt):

S f() { return S(); }
S s = f();


Тут у нас prvalue-выражение S(), благодаря чему мы можем не создавать переменную в момент его появления в return и присвоить создать лишь в s. И из всех интересных нам конструкторов будет вызван лишь конструктор по умолчанию. cv-квалификаторы в сигнатуре функции в данном случае нам ничего не портят (gb).

А если мы можем (не)явно сконструировать возвращаемый тип из выражения в return? Добавим в S такой конструктор:

S(std::string&&) TELL("str&&")

и собственно пример (gb):

S f() { return std::string(10, '1'); }
S s = f();


Всё хорошо, даже не смотря на то, что тип в return и в сигнатуре не совпадают. По факту компилятор представляет выражение в return как S(std::string(10, ‘1’)), что тоже является prvalue. Даже в таком случае всё в порядке (gb):

S f() {
std::string s(10, '1');
return s;
}


Всё корректно. Т.к. компилятор справляется тут даже сделать неявный move переменной s. Причём это всё ещё RVO.

Стоит понимать, что с C++17 RVO — гарантированная штука и даже не оптимизация.
Давайте поймём, как оно работает. У компилятора есть адрес места в памяти, где должен оказаться результат выполнения функции (называется return slot). Когда выполняются условия для RVO, компилятор может сразу создать объект из вашего return в нужном месте в памяти. Т.е. это скорее про свойства инициализации объектов.

Named Return Value Optimization уже оптимизация (причём не гарантированная, тут компиляторы делают кто что может). Базовый пример (gb):

S f() { S s; return s; }

Или пример посложнее, где в S появился int x (gb):

S f() { S s; s.x = 1; return s; }

void gg(S& s) { s.x = 1; }
S g() {
S s; gg(s); return s;
}


Компилятор всё ещё может создавать локальную переменную s в функциях сразу в return slot итогового объекта.
Для NRVO правда volatile для локальной переменной всё портит (gb).

Почему важно знать об этих штуках? Потому что NRVO можно сломать (gb):

S f() {
S s;
return std::move(s); // return S&&
}


Теперь компилятора обязан создать объект и мувнуть его, потому что это то, о чём вы его попросили явно. Так же можно думать, что у выражения в return и в сигнатуре теперь разные типы (S&& и S).
7👍4
#cpp

RVO/NRVO 2/2.

Как понять, убиваете ли вы NRVO? Можно сделать такой же класс, который выводит операции со своими конструкторами. В случае некоторых компиляторов (clang) можно ещё одним способом, о котором рассказали в комментариях в посте про этапы компиляции. На этапе семантического разбора компилятор производит всякие проверки на корректность типов в выражениях и подобном. Например у clang к основному условия про совпадение типов для NRVO ещё проверяется, правда ли, что во всех return возвращается одна и та же локальная переменная. Посмотрим на пример (gb):

void func1(std::optional<std::string>&);
std::string get_s();
void func2(std::string&);

std::optional<std::string> f(bool flag) {
std::optional<std::string> null;
func1(null);
if (flag) return null;
// (1)
std::string s = get_s();
func2(s);
return s; // (2)
}


Во-первых, корректно ли мы в (2) возвращаем s? Ответ нет. Лучше мувнуть. Потому что в данном случае будет вызван конструктор std::optional<std::string>(const std::string&) для RVO. Если же мувать, получим std::optional<std::string>(std::string&&). Хотя с C++20 мувать уже необязательно.

Во-вторых, случится ли NRVO в (1)? В данном случае нет, т.к. эта локальная переменная не возвращается во всех return. Однако если начать возвращать её и во втором return, то в AST можно будет увидеть строчку:

`-VarDecl <col:5, col:32> col:32 used null 'std::optional<std::string>':'std::optional<std::basic_string<char>>' nrvo callinit destroyed

С пометкой nrvo. Получается, можно сэмулировать структуру вашего кода и посмотреть на AST. Оч прикона кмк.

Если подумать, почему же одна и та же локальная переменная во всех return важна, то всё становится понятно: компилятор уверен, что эту переменную в памяти можно создавать сразу в return slot результата функции. В обратном случае это неверно, так что переменная null копируется.

Круто🤭

Если вы об этом паритесь, общие рекомендации такие:
1. Пытаться возвращать prvalue.
2. Возвращать одну и ту же локальную переменную во всех return.

Аналогичные правила примерно применимы и к исключениям, о которых два других пункта в стандарте. Но, честно говоря, если вы пытаетесь оптимизировать и так не очень производительные исключения, что-то пошло не так : )

Тут можно посмотреть хороший небольшой доклад про то, как это всё работает (с чуть бОльшим количеством технических деталей) и с примерами, когда это не работает.
Ещё можно посмотреть вот это бомбическое видео.
Please open Telegram to view this post
VIEW IN TELEGRAM
8🔥4🤯11
В этом году я начал пытаться писать сам время от времени вместо тупого вкидывания ссылочек на разные материалы, просто потому что так полезнее и мне, и вам.

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

Посты:
- необычные возможности C/C++;
- o chaos engineering;
- об аллокаторах в других языках;
- macros tricks;
- о векторах;
- о хеш-таблицах;
- о вероятностных структурах данных;
- о кешах;
- про рефакторинг;
- backend for frontend;
- про ub;
- впечатления от C++ Zero Cost Conf 2022;
- деревья в алгоритмах;
- про полезности из личного опыта;
- немного из метапрограммирования;
- пару кейсов, связанных с грамматикой языка;
- про эпохи;
- про cpp2;
- статический лист для сборки динамических аллокаций;
- чуть-чуть про софтскиллы;
- о подходах к реализации дженериков (помню, что торчу пост про go);
- про пагинацию;
- про лямбды;
- про компиляцию в общих чертах;
- RVO/NRVO;
- пачки пропозалов: раз, два;
- пачки рандомных фактов: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14;

О сборщиках мусора:
- основные алгоритмы;
- G1GC в java;
- другие сборщики в java;
- python gc;
- сборщики в других языках;
- о сборке мусора и C/C++.

Статьи:
- об устройстве популярных аллокаторов в C++;
- об интересных структурах данных;
- обработка ошибок и C++.

Задачки:
- про использование variadic templates;
- хз про что это но вот.

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

Рандомные факты про жизнь.

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

Завёл @dzikart и пытаюсь поддержать жизнь в @memesfromhole.

Набил ещё три татухи и научился жонглировать (кек).

Не умер, что особенно приятно.

Временно на каникулы. Не болейте.
34👍9🔥7
#common

Ещё немного полезностей из личного опыта.

Ещё когда в начале второго курса в каком-то рандомном курсе по python я столкнулся с регулярками, подумал, что это оч крутой инструмент, который, тем не менее, вряд ли часто применяется (потому что так с большинством знаний, к сожалению). Как же я был глуп. Сейчас я пользуюсь ими чуть ли не каждый день, пусть часто и хватает какого-то минимального базового набора. Ниже небольшой список примеров, где они пригодились:
- я не оч люблю codesearch в ide, потому что сижу в clion, который, как известно не самый быстрый (в том числе потому что у него проблемы с индексацией мало-мальски больших проектов). У меня это скорее прокачаный блокнот, по которому я делаю только Ctrl+F в рамках файла. Но когда надо поискать что-то в более глобальной окрестности, я пользуюсь внутренним сервисом для поиска по монорепе. И он поддерживает регулярки (как для строк, так и имён файлов), что вообще-то довольно удобно.
Кажется, недавно github обновил у себя поиск и можно делать что-то похожее.
- во внутреннем userver, в отличие от опенсорсного, есть кодогенерация (статья на хабре, где про это можно почитать), которая позволяет не писать бойлерплейт для раскладывания полей в структурки и обратно. Ты просто задаёшь схему в формате yaml, по которой валидируется реквест/респонс. И там есть возможность для строковых переменных задавать паттерны регулярками, по которым так же будет проводиться валидация. Если что-то не прошло, клиент получает 4хх автоматически.
- иногда в поиске бывают примеры не самой релевантной выдачи, которые обусловлены особенностями реализации. И иногда их проще закрыть чем-то вроде блеклиста, в котором можно поюзать регулярки для обобщения каких-то кейсов и неразрастания блеклиста.

Недавно обходил деревья dfs’ом, что вызывает какие-то детские эмоции радости, потому что после десятков (и может даже сотен) раз, когда он писался для спшных тасок, это знание перестало быть бесполезным с точки зрения применения в проде.
Рядом у нас ещё есть много разных bfs’ов для разных кейсов. А когда-то видел bfs для специфического обхода какой-то графической сетки.
Короче алгоритмы на собесах даже как-то обоснованы (даже k раз приходилось делать что-то с двумя указателями).

В статье про обработку ошибок я упоминал, что не нужно ловить всё подряд (catch (…) {} или ловить std::exception). Опровержений первому я пока не увидел, но ловить std::exception оказалось полезно. Кейс примерно такой: мы умеем явно ловить только ответы ручек от других сервисов, которые описаны в api, но это не всё, что сервис может ответить, т.к. иногда есть неописанные 5xx или технические 429/другие 4хх. И хочется, чтобы неуспешный запрос не влиял на доступность сервиса, который этот запрос и делает. Но мы согласны деградировать некоторую функциональность. Тут как раз хорошим вариантом будет ловить std::exception.

============================
Сейчас верхнеуровнево пытаемся намутить курсов на новый семестр, примерно с нуля. Занимает оч много времени (из-за чего я тут притих). Но есть ощущение, что получится даже не очень плохо. Может попозже чем-нибудь в этом месте поделюсь.
👍97👌2😐1
#cpp

Посмотрел доклад Andrei Alexandrescu на CppCon 2021. Накидаю приконых фактов оттуда:
- предлагает засунуть в стандарт

template <typename… Ts> struct type_sequence {};

потому что если этого не сделать, каждый будет писать это сам (и вероятно с помощью tuple/variant, что концептуально не всегда то, что нужно). В таком виде со списками типов предлагали работать и на CppRussia 2019 с примерами других функций, которые могут понадобиться при работе со списками типов, что выглядит вообще-то довольно приятно и в ногу со временем. И рядом Alexandrescu предлагает три базовые функции в классической манере для работы с металистом типов: head, tail и cons (создать лист из элемента и другого листа). Всё как в Lisp.

- и депрекейтнуть integer_sequence, потому что это не последовательность целых чисел, а последовательность значений какого-то типа. Вместо него ввести value_sequence, потому что это семантически честнее.

Из интересного ещё можно отметить такое. Предположим есть функция

template <typename T, typename… Ts, typename U>
int f(T, U, const Ts&…);


и её вызовы

f(1, 2); // Ts=<>
f<int, int>(1, 2, 3); // Ts=<int>
f<int, int, double>(1, 2, 3, 4); // Ts=<int, double>


Тип U всегда сможет вывестись! Причём из параметра, а не указанного списка типов в шаблоне. В первом случае это int по понятным причинам. Во втором это тоже int. В третьем же Ts выводится как <int, double>, а U — исходя из параметра. Причина в том, что если variadic pack начал матчится, он не остановится и идёт до конца (жадный матчинг). Получается, в таком кейсе у нас нет возможности указать тип U компилятору явно, ведь он всегда будет выводится из параметра.

Соответственно, если у вас есть что-то такое

template <typename... Ts, typename T = int>
T f(Ts…);


тип T может быть только int, потому что в него никогда ничего не сматчится.

Соответственно, если у вас пак типов функции находится не в конце, то он всегда должен быть указан явно:

template <typename... Ts, typename T>
int f(Ts... values, T value);


f(1); // Ts=<>
f<int, double>(1, 2, 3); // Ts=<int, double>
f<int, int>(1, 2); // error, потому что не хватает одного аргумента


Всё это приводит к забавным кейсам вроде такого

template <typename... Ts, typename... Us>
int f(Ts... ts, Us... us);


Тут Us всегда выводится из параметром (deduced), тогда как Ts должно быть указано явно. Можете попробовать понять для себя, чему они будут равны в следующих случаях:

f(1);
f(1, “2”);
f<int, char>(1, ‘2’);
f<int, char>(1, '2', “three”);


Или может быть такой угар:

template <typename... Ts, typename... Us, typename T>
int f(Ts..., Us..., T);


При любом вызове Us будет пустым паком. Что бы вы ни делали. Все компиляторы это кушают, но стандарт говорит, что если один пак всегда пустой, то это no diagnostic required (грубо говоря, нельзя так).

Ещё есть такие штуки как std::tr2::bases и std::tr2::direct_bases, которые возвращают тайплист всех базовых классов указанного и все непосредственные базовые классы указанного соответственно.

Хотя вообще доклад не об этом : ) Так что посмотрите.
👍9
#common #highload #cpp

Пару дней назад вышли записи прошломайского Highload++ Foundation 2022. Накидаю докладов из того, что уже посмотрел.

1. Как выжать 1 000 000 RPS. Андрей Аксёнов (link).
Классический доклад в своей нестандартной манере про бенчмарк Sphinx.

2. Поиск GPS-аномалий среди сотен тысяч водителей. Александр Царьков (link).

3. Как мы ускорили Яндекс Go на несколько секунд. Денис Исаев (link).
Я немного упоминал этот доклад тут в п.4. Просто и прикона.

4. Экскурсия в бэкенд Интернета вещей. Владимир Плизга (link).
Прикона было узнать, какие задачи в целом решают с помощью IoT. Правда я так до конца и не понял, что это такое.

5. Авито: root cause detector. Юрий Дзюбан (link).
Интересная попытка автоматизировать расследование проблем.

6. Избавляемся от кэш-промахов в коде для x86-64. Евгений Буевич (link).
Я не поклонник вот этих низкоуровневых штук, но может вам будет интересно. И заодно тогда можно глянуть LLVM Optimization Remarks by Ofek Shilon с осеннего CppCon 2022 (link).

7. Как наладить CI-процесс в монорепозитории. Алина Власова (link).

8. Настраиваем инцидент-менеджмент: от хаоса до автоматизации. Сергей Бухаров (link).
Тут скорее было интересно посмотреть, что у других. Имхо, у нас лучше.
Основные вопросы к системе это: насколько нужен скрайбер; почему оценкой импакта на бизнес занимаются разработчики в процессе инцидента; и зачем есть secondary дежурный, почему их только двое (понятно, что дальше это кому-то эскалируется, но почему не третьему, а SRE?)?

Пока местами прикона.
👍6
#highload #cpp #go

Досмотрел всё интересующее из майского highload, так что докидываю к прошлому посту (и пару докладов с плюсовых конф).

1. Как мы готовили поиск в Delivery Club. Иван Максимов (link).
Тут интересно, потому что я много работаю над штуками вокруг нашего поиска (не непосредственно качеством, а именно около). Когда ты чуть-чуть в теме, смотреть интереснее, что там чуваки намутили.

2. Хайлоад, которого не ждали. Дмитрий Самиров (link).
Тут аналогично, потому что чуваки намутили какую-то доставку продуктов из Я Еды. Почти Лавка. Только у нас круче.

3. Как мы устроили переезд 10+млн строк С++ кода на новый стандарт. Никита Старичков (link).

4. Статический анализ кода++. Анастасия Казакова (link).

5. Компилируем 200 000 файлов быстрее, чем distcc. Александр Кирсанов (link).
Это интересно с точки зрения, что в ВК намутили миллион каких-то своих способов компилироваться из PHP в C++ эффективнее. И по чуть-чуть чуваки эту свою инфру развивают. Прикольно посмотреть, что нового они там придумывают.

6. Почему всё сложно с оптимизацией и что с этим делать. Константин Шаренков (link).
В целом доклад с вопросами, но мб вам зайдёт. Мне не то чтобы прям, но выше среднего.

7. Go Map Internals. Егор Гришечко (link).

И плюсовые из закромов:

8. C++14 Reflections Without Macros, Markup nor External Tooling. Antony Polukhin (link).
Блин, оч крутой доклад с CppCon 16ого года, который я как-то пропустил. Антон рассказывает про magic_get/boost::pfr и всякие мозговзрывающие штуки, которые там использовались.

9. Back to Basics: C++ API Design. Jason Turner (link).

10. Метаклассы в C++17: фантастика? Реальность! Сергей Садовников (link).

Мб вам что-то на последних конфах понравилось? Можно поделиться в комментариях.
👍132
#cpp

Сходил на встречу российской РГ21. Накидаю инфы (что-то вы уже видели, но лишним, думаю, не будет).

В C++23:

- починили некорректные срабатываения static_assert(false):

if constexpr (constexpr_condition) {

} else {
static_assert(false, “message”);
}


- починили range based for. Link на proposal;
- добавили static operator[] (в записи приконый пример с таким оператором для енамов);
- монадические операции для std::expected;
- дополнения к алгоритмам ranges (например эффективная итерация по ключам/значениям мап и много другого), но из-за этого ренджи из 23его стандарта сильно несовместимы с ренджами из 20ого. Не оч понятно, что это значит. Подождём и потрогаем;
- std::stacktrace и std::print;
- constexpr много всего (например std::unique_ptr).

С++26:

Пока глобально планируется примерно вот так:
- library support for coroutines;
- executors;
- networking;
- pattern matching;
- reflection;
- и всякие другие штуки.

Конкретно:
- #embed (емнип приехал из C23);
- пытаются пропихнуть std::get для агрегатов;
- stacktrace for exceptions в комитете приняли с вопросами, будут поправлять;
- ABI ломать пока не будут, незачем, говорят.

Хех. Прикиньте, С++23 это вот в этом году. А мы только недавно на 20й стандарт перешли🥴🥴🥴
👍8🔥2
#cpp

Инициализация 1/2.

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

Первый пример:

int i;
return i; // ub

struct S {
int x;
};
S s;
return s.x; // ub


Это default initialisation, что вообще-то немного путает, потому что переменная у вас не инициализируется ничем дефолтным (мусор я бы дефолтным не назвал). Можно поправить так:

S() : x(0) {}

Это member initialiser list. Не оч удобно, потому что надо повторять во всех конструкторах, потому в C++11 появился

struct S {
int x = 0; // default member initialiser
};


Наверное использовать dmi может быть хорошим советом, пусть и не лишённым проблем.
Далее:

int i = 2; // copy initialisation

Такой же способ инициализации используется, когда вы передаёте аргумент в функцию по значению или по значению возвращаете. Если тип инициализируемого объекта и значения справа от = не совпадают, будет произведена некоторая цепочка преобразований типов (насколько я понимаю, сколько угодно стандартных преобразований и может быть одно пользовательское, всё это в произвольном порядке, конечно).
Помните, что copy init это никогда не operator=.

Aggregate init:

int i[3] = {1, 2, 3};
int i[] = {1, 2, 3};

struct S { int x; int y; };
auto s{1, 2};


Агрегатная инициализация будет copy-инициализировать каждый из переданных аргументов. У такого init есть фича:

struct S { int x; int y; };
S s = {1};
return s.y; // какое значение? ub?


Если вы опускаете часть аргументов при использовании агрегатной инициализации, остальные будут zero initialisated. Т.е. у вас никогда при таком способе не будет неинициализированных данных. Потому и работает вот такой способ заполнения массивов нулями:

int a[10] = {};

Ещё тут есть такая фича как brace elision.

Далее static initialisation:

static int i = 1; // constant initialisation
static int j; // zero initialisation


Все эти способы были просто унаследованы плюсами от C. Теперь про плюсовые.

Direct initialisations (whenever the initialiser is an argument list in parens):

S s(1, 2); // constructor
int i(3); // also for built-in type


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

struct S {
explicit S(int) {}
S(double) {}
};
S s1 = 1; // S(double)
S s2(2); // S(int)


Value init (whenever the initialiser is a pair of empty parens):

int f() { return int(); }

Если есть пользовательский default ctor, то вызовется он, иначе zero init. Тут можно словить всякие маслины. Осторожно.

Uniform init — способ универсальной инициализации из C++11 (с помощью {}). Тут же появились list inits:

S s{1, 2}; // direct list init
S s = {1, 2}; // copy list init


И тут же появился std::initializer_list. Думаю, как им пользоваться вы знаете, так что опустим. Про его проблемы можно глянуть тут.
Для агрегатов list init это agregate init. Для built-in типов пример выше. Для классов либо вызов подходящего конструктора, либо direct/copy init.
{} — особый случай. Это дефолтный ктор, иначе ктор от initializer_list, иначе value init ☺️🎧
При list init никаких narrowing conversions не происходит, так что int i{1.0} не закатит.

Ну и опуская всякое неинтересное, не забудем про C++20 designated initialisation:

struct S { int x; int y; };
S s{.x = 1, .y = 2};


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

Тут мы упомянули даже не всё что на гифке. А на гифке тоже пары штук не хватает🙂
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🤯2
#cpp

Инициализация 2/2.

Непонятно что делать с этой инфой. Давайте посмотрим на пару примеров, которые помогут сделать выводы. Самый простой:

int m();


Вот я хочу инициализировать переменную m по месту. Но, учитывая most vexing parse, это у вас не переменная, а объявление функции. Всё сломалось. Или гораздо более стрёмный кейс:

vector<int> v(istream_iterator<int>(cin), istream_iterator<int>());

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

Из забавного (если конечно больно можно считать за смешно) следующие кейсы:

int i(1, 2);
int i = (1, 2);
int i = int(1, 2);
auto i(1, 2);
auto i = (1, 2);
auto i = int(7, 9);


Будет ли первое компилироваться? Конечно же нет. Наверное тогда и второе? Ошибка. Второе это оператор запятая, потому всё корректно. Третье, четвёртое и шестое (по аналогии с 3м), очевидно 🎧, тоже CE. Пятое по аналогии со вторым OK.

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

Из интересного ещё следующий момент. В C++11 следующая запись означала std::initializer_list<int>{1}:

auto i{1};

Однако в C++14 семантику подобного изменили, где это стало int{1}.

auto i = {1};


А что же тут? Это без сомнений init_list. Т.е. вы меняете тип объекта при инициализации с помощью =. Конечно интуитивно это вроде понятно, но кринж🥴, не правда ли? Говорят, что семантику такого поправлять не будут. Может просто запретят что-то такое делать в будущем (или нет, who knows). Мб потому стоит ещё и отказаться от инициализации с помощью =. Просто на всякий случай.
Хотя есть аргумент за такой стиль. У вас будет одинаковый способ “инициализации” переменных и типов:

auto x = …;
using T = …;


Для типов тогда лучше явно указывайте тип: auto i = int{1} (это всё мысли из AAA от Herb Sutter). В защиту такого подхода приводится поинт, что вы не забудете инициализировать вашу переменную. Хотя и тут есть траблы:

std::string x = “1”;
auto x = “1”s; // C++14


Вот эту s в конце (литерал) очень легко забыть/не обратить на неё внимания.
Ещё можно заметить, что такой способ работает (работал*) только для movable/copyable типов, иначе CE:

auto a = std::atomic<int>{9}; // CE before C++17

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

Чисто из опыта совет: помните, что значит первая буква в AAA — Almost. Потому что иногда поиск типа переменной становится неприятным муторным занятием, попросту тратящим время.

Ещё классический момент это инициализация вектора:

std::vector<int> v(3, 1); // 1 1 1
std::vector<int> v{3, 1}; // 3 1


Ну тут всё понятно. Не будем обсуждать.

В докладе Nicolai Josuttis можно посмотреть про это всё счастье подробнее. Там ещё много маслин словить можно.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1111
#cpp

1. Женя писал про попытку закоммитить в clang реализацию assume и про то, почему не получилось. Даже как-то и несмешно уже.

2. А Саша писал у себя про self-move. Почитайте. Там ещё в комментах есть интересное.

3. Тут краткий и понятный поинт про то, почему лучше использовать std::make_unique вместо прямой передачи указателя в конструктор std::unique_ptr. В целом понятно.
А тут можно посмотреть аналогичный ответ про std::make_shared (разница гораздо более ощутимая, но не такая однозначная).

4. Небольшая статья про использование aligned new.

5. Доклад Herb Sutter про видение обобщённого программирования на C++ аж из 2017ого, вполне, тем не менее, актуальный, т.к. многое из рассказанного ещё не завезли.

6. Небольшая статья с заметками про лямбды.

Такой вот минипостик получился.
👍14
#common

Базово про кодировки.

Очень важно различать два основных понятия: текст и его представление в виде байт. Есть две основные операции перевода из одной сущности в другую: из текста в байты с помощью какой-то кодировки (обычно называется encode), и наоборот (decode).

Собственно кодировка это способ/правило/функция отображения текста в байты и наоборот.

Одной из первых адекватных кодировок стала ASCII. Она в соответствие каждому символу ставит семибитовый код (т.е. хранится 128 символов). 32 символа - управляющие, остальные - стандартные английские буквы, цифры, знаки препинания и т.д. 8й бит использовался для контроля ошибок. Позже она была расширена до 256 символов (задействованы все 8 битов). Однако уже успели появиться другие однобайтовые кодировки, которые расширяли изначальные 128 символов. Например KOI-8 — кириллическая кодировка, имеющая интересную особенность: в случае, если используемый редактор не поддерживает эту кодировку, при преобразовании кириллических символов в ASCII получался транслит; соответственно русские буквы шли не по алфавиту. Или CP866 — кодировка, используемая в MSDOS, — и CP1251 — дефолтная кодировка при использовании кириллицы в windows до windows 10.

Со временем 256 символов конечно же перестало хватать. И наличие различных кодировок не является универсальным решением. Потому родился Unicode. Он также является отображением из кода в символ, только размеры чуть больше: 2^21 символов.

Unicode реализуют несколько основных кодировок:
- UTF-32. Каждый символ занимает ровно 4 байта. С ней легче всего работать, но она занимает больше всего памяти из-за фиксированного размера.
- UTF-16. Каждый символ представлен либо 2, либо 4 байтами.
- UTF-8. Символ кодируется 1, 2, 3 или 4 байтами. ASCII-часть кодируется одним байтом, причём коды символов полностью совпадают.

Устройство UTF-8 (наиболее популярная из вышеназванных).

Если говорить об ASCII-символе, то в начале идёт 0, а потом 7 бит символа.
Если символ не ASCII, тогда нужно понять, сколько байт нужно для представления символа. Для этого в самом первом байте вместо нуля пишется некоторое количество единиц. Это количество и указывает на то, сколько байт занимает символ. Т.е. если он занимает 3 байта, то пишется 111, после чего [что логично] идёт 0. Далее идут уже значащие для кода биты. Каждый последующий байт начинается с 10, после чего идут 6 значащих битов.
Зачем нужна такая избыточность 🤔 ?
При разработке кодировки требовалось сохранить обратную совместимость с уже большой кодовой базой, написаной на C, в котором нулевой символ является знаком окончания строки. Как видим, нулевой символ в UTF-8 только один. Ещё тут выполняется такое свойство, что ни один символ не является префиксом другого, что позволяет использовать некоторый набор старых функций, работающих с ASCII с UTF-8 (например поиск подстроки в строке).

Собственно если посчитать, сколько символов мы можем задать подобным образом, получится что-то около 2^21.

Вот хорошая статья про это всё.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍174🔥2
#cpp

Чуть-чуть из доклада Daisy Hollman (не путать с Nadin Holman) на CppCon 2022 (прошлый я упоминал тут).

Fun fact. Можно скрыть общий шаблон с помощью constraint’ов:

template <class T>
struct B {
void print() { cout << “unreachable”; }
};

template <> requires true
struct B<T> {
void print() { cout << “new default”; }
};


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

Правда если вы потом захотите сделать ещё одну специализацию:

template <class T> requires std::integral<T>
struct B<T> {
void print() { cout << “integral”; }
};


то для условного int получится неоднозначность, потому что по факту это выбор между двумя requires true. Но это можно обойти объявлением всегда истинного концепта:

template <class T> concept True = true;

И поменять сигнатуры на

template <class T> requires True<T>
// и
template <class T> requires std::integral<T> && True<T>


Это какие-то жоские преки. Словно про поломанность std::void_t вспомнил.

Второй fun fact (ну это конечно чуваки крутят-вертят как хотят уже).
Помним можно писать user-defined literal templates:

template <char… Chars>
constexpr auto operator “”_i();


И использовать их можно вот даже для чисел:

auto x = 123_i;

Компилятор распарсит 123 в три символа и внутри оператора можете творить что хотите.
Собственно на этом и построена возможность обращаться к элементу тупла через operator[]:

template <size_t I> struct index {};

// преобразовываем литерал в число
template <char… Chars>
constexpr auto operator “”_i () {
return index<[]{
size_t rv = 0;
for (auto c : {Chars…})

rv = rv * 10 + (c - ‘0’);
}()>{};
}


template <class… Ts>
struct index_tuple : std::tuple<Ts…> {
using std::tuple<Ts…>::tuple;

template <size_t I>
auto operator[](index<I>) {
return std::get<I>(*this);
}
};


auto t = index_tuple<int, double>{1, 2.2};
cout << t[1_i];


Ну и всё. Файная обёртка над std::get готова😁
Но помните, что это работает только для литералов.

Прикона конечно, но кринж, что такие простые хотелки решаются вот такими методами. Может на раст уйду. Хз.
Или вообще кодить перестану.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7