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
#cpp
Краткий ликбез (наверное уже последний) по основным нововведениям в С++20.
cppstories.com/2022/20-smaller-cpp20-features/
1👍1
#cpp
Рефлексия в C++Next на практике.
habr.com/ru/post/598981/
👍2
#cpp
Метапрограммирование в C++ и русская литература: через страдания к просветлению.
habr.com/ru/article/448466/
1👍1
#cpp
[видео]
CppCon 2019. Timur Doumler. Type punning in modern C++.
Автор рассказывает об алиасинге типов и strict alias rule, выравнивании, неопределённом поведении и как его избежать.
youtube.com/watch?v=_qzMpk-22cc
1👍1
#cpp
Динамический неоднородный плотно упакованный контейнер.
habr.com/ru/post/302372/
1👍1
#cpp
Элементы функционального программирования в C++.

[из Википедии] Частичное применение - возможность в ряде языков программирования зафиксировать часть аргументов многоместной функции и создать другую функцию.
habr.com/ru/post/313370/

Композиции отображений.
habr.com/ru/post/328624/
1👍1
#algo
Довольно приятный плейлист про не очень стандартные алгоритмы, которые ещё и хорошо поясняются.
www.youtube.com/playlist?list=PLc82OEDeni8SGp5CX8Ey1PdUcoi8Jh1Q_
1👍1
1👍1
#cpp
Помоги компилятору помочь тебе.
Автор рассказывает о флагах компиляции, что они делают, и почему стоит их использовать в своих проектах.
habr.com/ru/post/490850/
1👍1
#cpp #proposals
Пачка новых предложений за январь.

1. У бумаги про std::hive уже 19 ревизий. И тут ещё появилась просьба вернуть это предложение в 23й стандарт. Оч интересно, как это предложение летает туда-сюда. Мне почему-то забавно.

2. Пары и туплы очень похожие типы, ведь первое просто частный случай второго. Туплы могут быть сконструированы из пар, но не наоборот. Такое положение наводит на мысли, что std::pair немного избыточен. Удалять его конечно никто не будет, но туплам можно добавить некоторые возможности пар, чтобы последние использовались реже. Proposal.

3. Из C делают современный язык, хех. Но местами выглядит это конечно мда: Basic lambdas for C, Improve type generic programming.

4. Ослабление ограничений для constexpr. На самом деле тут ничего капитального. Просто какие-то мелочи.

5. std::breakpoint для остановки при выполнении в дебаге. Честно говоря, не очень понимаю, зачем тащить это в язык. Дебагеры хорошо справляются.

6. std::is_debuger_present для понимания, отлаживается ли сейчас программа. Вот это уже что-то полезное. Довольно часто приходилось пихать вывод только для отладки.

7. Сделать move_iterator<T*> random_access итератором. Это поможет выполнять некоторые операции с ренджами эффективнее.

8. Более строгое требование для атрибута [[assume(...)]].
1
#c #cpp

Попалась довольно сомнительная статья на хабре про редкие возможности в С. Учитывая половину списка, не очень понятно, что такое редкие возможности в понимании автора. Не предлагаю вам её читать, чтобы не тратить время. Остановлюсь только на самых интересных моментах и прокомментирую/добавлю немного информации.

1. Конкатенация строк времени компиляции.
Думаю, вы тоже видели какой-то подобный код:

std::string very_long_string = "This is first part of long string." "And this is second";

Никаких плюсов и функций конкатенации: компилятор сделает это одной строкой сам. Используя такое поведение можно реализовать интересный макрос:

#define LOG(exp) std::cout << "Result of " #exp "=" << exp;
int x = 12;
LOG(x*5);


Результатом будет "Result of x*5=60".

2. Препроцессорная склейка строк.
Как вы думаете, будет ли инкремент в следующем коде?

int inc(int x) {
// doing increment\
x++;
return x;
}


Вопрос в том, что произойдёт раньше: конкатенация строк, разбитых через \, или замена комментария на пробельные символы? Легко можно убедиться, что x++ станет частью комментария, то есть строки конкатенируются раньше. Я как-то смотрел замечательную лекцию по тулчейну, где пояснялся этот момент и думал "ну разве можно так набагать", а недавно мне рассказали, что обнаружили подобное в проде. Забавно.

3. Функции в стиле K&R.
Не знал, что такая возможность раньше была в C. Суть заключается в том, что вы объявляете функцию следующим образом:

int foo(s, f, b)
char* s;
float f;
struct Baz * b;
{
return 5;
}


в то время как сегодня она выглядела бы более привычно:

int foo(char* s, float f, struct Baz * b) {
return 5;
}


4. tmpfile.
В статье конечно речь идёт и сишной функции, однако есть аналог std::tmpfile (который тем не менее ничем не отличается). Автор не приводит каких-то полезных применений временных файлов. Мне когда-то было полезно при написании внешней сортировки (правда, там было использование std::tmpnam, но сути не меняет), а ещё, если внимательно посмотреть на то, что делает ваш компилятор при билде программы (например --verbose для g++), то можно увидеть, что компилятор создаёт некоторое кол-во временных файлов, которые можно даже попросить его не удалять.

5. Отрицательные индексы.
Делается такое довольно просто:

int* arr = new int[100] + 50;

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

6. std::new_handler.
В C++ есть такое понятие как new_handler (не новый обработчик, а обработчик оператора new). В случае, если new не может выделить необходимое количество памяти, он вызывает свой обработчик в надежде, что тот разрулит ситуацию: сделает дефрагментацию, что-то освободит, переназначит обработчик или что-нибудь ещё. В случае повторного неуспеха обработчик будет вызван ещё раз, то есть такая программа вполне приводит к бесконечной рекурсии:

void f() {}

int main() {
std::set_new_handler(f);
int* p = new int[1000000000000];
}
👍4🔥21
#highload

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

Очень важной задачей при разработке высоконагруженных систем является поддержание отказоустойчивости (часто хочется удержать что-то вроде 99.99% аптайма). У нас проверка работоспособности сервисов проверяется довольно понятно: проводятся учения по отключению одного из датацентров. Такие учения бывают внешние (для всей компании) и внутренние (для конкретных сервисов и продуктов). По своему (небогатому) опыту могу сказать, что такие мероприятия очень полезны: вроде на прошлой неделе всё прошло успешно, а сегодня уже проблемы с коннектами в базе, тайминги выросли или на сервисе начинает загораться congestion control.

Но рассказать хотелось бы об инструментах Netflix: chaos monkey.
Chaos monkey является частью chaos engineering (хотя можно сказать, что стала началом):

Chaos engineering is the discipline of experimenting on a distributed system in order to build confidence in the system's capability to withstand turbulent conditions in production.

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

Обычно падения настраиваются, чтобы отключение инстанса происходило в тот момент, когда команда наиболее готова к сражению с падением. "Падать" могут лишь те машинки, которые помечены для этого доступными. Все доступные для "уранивания" машинки делятся на группы. Каждый будний день обезьяна приходит в каждую группу инстансов и бросает монетку (если точнее, то монетка взвешеная), чтобы решить, ронять ли какую-то машинку из этой самой группы. Если она решает, что надо, то шчедулит падение на случайное время в промежутке от 9:00 до 15:00. Каждое приложение/сервис определяет 2 величины, на основе которых бросается монетка: среднее время в рабочих днях между падениями и минимальное время в рабочих днях между падениями.

Из минусов можно отметить:
- для chaos monkey необходимы spinnaker (система развёртывания в нетфликсе) и mysql;
- генерирует только псевдослучайные падения конкретного инстанса приложения/сервиса, в то время как разнообразие проблем в реальной жизни гораздо шире;
- работу chaos monkey нельзя просто так прервать. Если что-то пойдёт не так, остаётся только сражаться.

В какой-то момент одного инструмента стало не хватать, потому появилась simian army (которая, тем не менее, уже не поддерживается).

Simian army включает в себя ещё три основных инструмента:
1. Janitor monkey или сегодня swabbie -- аналог сборщика мусора для облачной экосистемы.
2. Conformity monkey проверял соответствие инстансов некоторым предефайненым правилам. В случае несоблюдения правил, инстанс отключался. Сейчас это часть spinnaker.
3. Security monkey проверял инстансы на различные дыры в безопасности.

и ещё несколько, которые были депрекейтнуты сильно раньше или не опубликованы:
4. Chaos gorilla, который симулировал отключение одной из 25 зон инфраструктуры AWS (т.е. отключение огромной части всех инстансов).
5. Chaos kong -- аналог прошлого инструмента, но симулирующий отключение меньших частей инфраструктуры.
6. Latency monkey -- тулза, увеличивающая тайминги ответов некоторых ручек в сервисах.
7. Doctor monkey -- инструмент, мониторящий состояние инстансов. Если инстанс "заболел" (пятисотит, выросли тайминги или кушает много cpu), doctor убирает его из приложения.
8. 10-18 monkey (i.e. l10n-i18n) проверяет работу сервисов, которые работают в различных географических зонах. Чекаются проблемы связанные с локализацией и всем что около.
1👍1
Массивная и интересная (мне показалось) статья про fork().
https://habr.com/ru/post/586604/
1👍1
#common

Аллокатор как метод управления памятью впервые появился в С (речь не о std::allocator, а о самом термине, который был реализован в malloc/free), а своё, можно сказать, известное воплощение получил в C++ как std::allocator (в 1994м) и std::pmr::allocator (В C++17). Однако не одними C/C++ едины. Аллокаторы также существуют и в других языках программирования (как самостоятельный термин, так и наследие/заимствование C/C++).

В силу развития языка в какой-то момент аналоги malloc/free появились в COBOL и Fortran 90. Аналогичный метод управления памятью есть и в D (malloc, new), т.к. он появлялся как более правильный C++ и тянет за собой часть стандартных библиотек C/C++, хотя сам D имеет свой сборщик мусора.

Но эти примеры немного надуманы, потому что термин аллокатор тут появляется из аналогичности методов управления памятью.
Более честными примерами являются:

1. Rust.
Из интересного можно отметить две вещи.
В расте есть глобальный аллокатор, который в отличие от C/C++
очень легко подменить:

#[global_allocator]
static GLOBAL: MyAllocator = MyAllocator;


До версии 1.28 rustc неявно линковал jemalloc в каждую свою программу. Чтобы ослабить зависимость от libc, это поведение изменили в сторону стандартного системного аллокатора.

update: В C++ тоже можно "подменить" глобальный аллокатор.
Перегрузим глобальные операторы new/delete, засунув в них соответствующие операции из готового аллокатора. Единственное что это всё-таки немного неполноценно в силу того, что используемый аллокатор может работать только с памятью (ведь вызов конструктора/деструктора не переопределяется), а интерфейс std::allocator до C++20 (пусть он и не прав идеологически, что такими вещами занимается) всё-таки хочет и это контролировать. С 20ого стандарта методы construct/destroy удаляются и заменяются на std::construct_at/std::destroy_at. В таком виде можно говорить о корректной замене глобального аллокатора.

2. Zig.
Тут нет дефолтного аллокатора. Чаще всего вы выбираете свой аллокатор согласно рекомендациям и отдаёте его как параметр. Если вы пишете свою библиотеку, авторы предлагают придерживаться подобной концепции и требовать у пользователя аллокатор, который ему нравится. А вообще он предоставляет (не сказал бы что богатый, но) зоопарк из нескольких аллокаторов, которые вам могут понадобиться: от std.heap.c_allocator (C malloc/free) и нескольких пул-аллокаторов до аллокаторов для тестирования и std.heap.GeneralPurposeAllocator, если вам ничего не подошло (так написал, будто между крайними случаями ещё солидное кол-во аллокаторов, хотя на самом деле я перечислил почти все).
3
#common

Сборщики мусора 1/2.

По чуть-чуть начал разбираться со сборкой мусора, так что накидаю вам немного инфы о ней.

Есть несколько базовых алгосов, которые в разных вариациях юзаются в большинстве промышленных garbage collector'ах. В основном они делятся на трассирующие (которые отслеживают достижимость объекта) и прямые. И параллельно делятся на перемещающие и не перемещающие (в моей вольной интерпретации).

Mark-and-sweep.
Самый дефолтный алгоритм. Для каждого объекта хранится бит достижимости. Изначально он ноль. Все объекты указывают друг на друга. Есть специальные корневые объекты. На этапе mark дфсом обходим все объекты, достижимые из корневых, и устанавливаем им бит достижимости в 1. На этапе sweep обходим все объекты и проверяем, если бит достижимости 1, то просто его сбрасываем. Если же он 0, то объект помечается удалённым (обычно пихают его во freelist). Проблемой такого подхода является stop the world -- всё выполнение программы останавливается, пока сборка на закончится.
Есть ещё вариация mark-and-compact, где вместо помечания участка памяти свободным объекты в памяти как-то перемещаются, чтобы немного её дефрагментировать.

Существует улучшенная версия этого алгоритма под названием BF Mark, которая избавлена от этих недостатков.
Вместо двух "цветов" достижим/нет объект используется 3: чёрный, серый и белый. Чёрные объекты доступны из корней и не имеют исходящих ссылок на белые объекты, белые -- кандидаты на удаление, серые -- объекты доступные из корней, но пока не проверенные на наличие ссылок на белые объекты. Алгоритм состоит из трёх шагов: выбрать объект из серого множества и переместить его в чёрное; поместить все белые объекты, на которые есть ссылки из нового чёрного в серое множество; повторять прошлые два шага, пока серое множество не станет пустым. Когда серый набор пуст, сканирование завершено: черные объекты доступны из корней, в то время как белые объекты недоступны и могут быть собраны мусором. Поскольку все объекты, до которых невозможно добраться сразу из корней, добавляются к белому набору, а объекты могут перемещаться только от белого к серому и от серого к черному, алгоритм сохраняет важный инвариант - никакие черные объекты не ссылаются на белые объекты. Это гарантирует, что белые объекты могут быть освобождены после того, как серый набор станет пустым. Такой метод удобен, потому что его можно выполнять на лету.

Ещё популярна копирующая сборка (semispace/Lisp 2/алгоритм Чейни). Алгоритм основывается на том, что выделяется две области памяти одинакового размера. Все объекты создаются в одной из них, вторая при этом содержится пустой. Как и в прошлом алгоритме, есть несколько корневых объектов, которые ссылаются на другие. В некоторый момент все объекты проверяются на достижимость. В случае, если объект валиден, он дублируется во вторую пустую область памяти, иначе удаляется. В итоге все достижимые объекты скопированы в новое место. Произошла очистка ненужных объектов и уплотнение для избежания фрагментации.
👍51
#common

Сборщики мусора 2/2.

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

Одним из самых эффективных алгоритмов сборки мусора является сборщик мусора с поколениями. Он основан на простом утверждении: большинство объектов умирают молодыми.
Новые объекты считаются объектами первого поколения. Если эти объекты переживают n сборок мусора, их поколение становится вторым. Объекты более высокого поколения проверяются реже, т.к. подразумевается, что раз они уже долго прожили, то и далее проживут дольше молодых объектов. В случае, если они опять переживают несколько сборок мусора своего поколения, то они перемещаются в третье поколение. Количество поколений обычно ограничено каким-то небольшим числом (т.е. не растут бесконечно). Правило при обходе графа объектов простое — если нам повстречался объект из более старшего поколения, чем проверяемое в данный момент, дальше в этом направлении не идем. Однако здесь есть тонкий момент. Поскольку объекты в общем случае могут изменяться, они также могут содержать ссылки на объекты более молодого поколения. Поэтому во время выполнения программы необходимо отслеживать ситуации, когда более старый объект начинает ссылаться на более молодой и добавлять в этом случае молодой объект в «корневое множество» объектов соответствующего поколения. Иначе сборщик мусора ошибочно удалит его, как недостижимый.
Иногда полезно знать, как устроен сборщик мусора, которым пользуется разработчик, чтобы не бояться создавать этот самый мусор. Например объекты можно переиспользовать, однако для сборщика мусора с поколениями это будет означать, что объект долгоживущий, из-за чего его поколение вырастет и он будет проверяться реже. Это искуственное продлевание жизни объекта в итоге может сделать операции сборщика мусора менее эффективными.

Чуть позже накидаю про то, как это выглядит в разных языках программирования.
👍71
#cpp

Немножко macros tricks.

Макросы в целом скорее bad practice. Они могут приводить к нечитаемому коду и неожиданному поведению. Но они всё же позволяют делать интересные вещи.

1. При использовании макросов часто совсем непонятно, во что же он раскрывается. Как вам такой код?

int x = 1;
SOME_MACROS(x)


На первый взгляд он кажется неестественным: а почему это компилится? Почему нет точки с запятой? Потому что автор макроса засунул её внутрь. Вы её не поставите и всё скомпилируется. Но кого-то позже введёт это в ступор, кто-то решит поменять ваш макрос и код перестанет компилиться из-за нехватки этой самой ; во всех местах использования. Конечно, можно договориться всегда писать её после макросов, но тогда это лишь рекомендация, которую можно и не выполнять. Лучше будет заставить пользователя макроса писать аккуратно. Например обернув всё тело макроса в do-while:

#define SOME_MACROS(x) \
do { \
++x; \
} while (false)


Ещё вы решаете проблему пересечения имён, т.к. создали новую область видимости. Короч немного накостылили и стало получше.

2. Об этом уже упоминалось, но повторим.
Думаю, вы тоже видели какой-то подобный код:

std::string very_long_string = "This is first part of long string." "And this is second";

Никаких плюсов и функций конкатенации: компилятор сделает это одной строкой сам. Используя такое поведение можно реализовать интересный макрос:

#define LOG(exp) std::cout << "Result of " #exp "=" << (exp);
int x = 12;
LOG(x*5);


Получаем: Result of x*5=60

3. Есть популярные макросы, которые позволяют добавлять немного информации например при отладке (помните, что не все есть везде и их результаты иногда implementation defined).

Первые это __FILE__, __LINE__:

#define assert(expr) \
(static_cast<bool>(expr) \
? void(0) \
: assert_fail(#expr,
__FILE__, \
__LINE__, __ASSERT_FUNCTION))

Или __PRETTY_FUNCTION__:

void f(int) {
std::cout << __PRETTY_FUNCTION__;
}

Получим: void f(int)

Ещё есть __FUNCTION__ и __func__.

И __COUNTER__: по мере вызова в рамках программы он выдаёт натуральные числа от нуля и выше:

std::cout << __COUNTER__ << __COUNTER__ << __COUNTER__;

Результат: 012.

Последний можно использовать для создание макроса для анонимных переменных:

#define CONCATENATE_IMPL(s1, s2) s1##s2
#define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2)

#ifdef __COUNTER__
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, COUNTER)
#else
#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __LINE__)
#endif

Теперь вы можете спокойно юзать этот макрос для создания анонимных переменных:

auto ANONYMOUS_VARIABLE(var) = gsl::finally([] {});

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

Тут рассказывают, как это сделать чуть более юзабельным.

Кстати бонусом вопрос: почему нам нужен промежуточный макрос CONCATENATE_IMPL?
👍41
#cpp #poll

Задача такая: выполнить какой-то код ровно один раз во время жизни нашей программы. Решение вполне понятное:

void init() {
static bool unused = [] {
std::cout << "print
ed once" << std::endl;
return true;
}();
}


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

printed once

А что, если я хочу вызвать какой-то код ровно n раз? Об этом и предлагаю вам подумать : )

Как обычно, если никто не пробьёт, ответ через пару дней.

UPD: хотелось бы увидеть решение руками, а не с чем-то вроде std::call_once (но ими всё равно можно поделиться :) ).
👍31
this->notes.
#cpp #poll Задача такая: выполнить какой-то код ровно один раз во время жизни нашей программы. Решение вполне понятное: void init() { static bool unused = [] { std::cout << "printed once" << std::endl; return true; }(); } При вызове такой функции…
#cpp #poll

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

Вспомним, как работает этот код:

void init() {
static bool unused = [] {
std::cout << "printed once" << std::endl;
return true;
}();
}


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

If the initialization throws an exception, the variable is not considered to be initialized, and initialization will be attempted again the next time control passes through the declaration.

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

struct Throwed {};

constexpr int n = 3;

void init() {
try {
static bool unused = [] {
static int called = 0;
std::cout << "123" << std::endl;
if (++called < n) {
throw Throwed{};
}
return true;
}();
} catch (Throwed) {}
}


Как по мне, оч прикона.

Ещё можно найти такой факт:

If the initialization recursively enters the block in which the variable is being initialized, the behavior is undefined.

Никогда не задумывался об этом. Тож интересно.
👍161
#list

Несколько рандомных фактов.

1. Посмотрим на такой код:

struct Kek { int lol; int kek; };
std::vector<Kek> keks;
for (const auto&
[lol, kek] : keks) {
[=] { std::cout << lol << "\n"; }();
}


Неожиданно, он не скомпилируется. Не скомпилируется он на моменте обращения к переменной lol в лямбде. Странно, да?🤔
На самом деле переменные из structure binding не умеют захватываться в лямбды (это, стоит надеяться, временно). Немножко пояснений можно посмотреть тут.

Кстати примерно из-за этого же structure binding становится пацаном довольно неровным, потому что начинает ломать всякое NRVO. Будьте аккуратны.

2. Попался один интересный доклад на CppCon 2016 про оптимизацию вашего кода. Автор предлагал использовать принцип DRY не только в случае написания кода, но и с шаблонами:

struct B {
virtual ~B() = default;
};

template <typename T>
struct D : B {
std::vector<int> get() const { return m_v; }
std::vector<int> m_v;
};


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

struct B {
virtual ~B() = default;
virtual std::vector<int> get() const { return m_v; }
std::vector<int> m_v;
};

template <typename T>
struct D : B {};


Так сказать взгляд с другой стороны.
Посмотрите доклад. Там прикона : )

3. При использовании std::function<void(args...)> (с возвращаемым типом void) к такому объекту можно кастовать любой функтор с такими же аргументами, но любым возвращаемым типом. То есть вот такое будет компилироваться:

std::function<void()> f{[] -> int {return 1;}};

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


И ещё немного ссылочек на выходные:

0. Интересный доклад о переписывании базы данных личных сообщений в Вк.

1. Обзорный доклад про очереди в highload.

2. Статья про то, как важно уметь отказываться от уже сделанного.

3. Удобное сокращение для стандартных стримов в C++: https://github.com/vitaut/_._ (sorry).

4. Статья про некоторые отличия в реализациях частей C++ от разных компиляторов.

5. Новый формат QOI (qoiformat.org), который неплохо сжимает картинки, на уровне PNG, но при этом в десятки раз быстрее, а его спецификация -- одна страница.
👍31