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

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

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