Telegram Group Search
#madskillz

Нестандартные представления строк

В "стандартном" C++ есть три основных представления для строк. Не будем учитывать "составные" классы (как std::stringstream), у которых нет уникальных концепций.

=====
1️⃣ const char* - просто указатель на начало строки где-то в памяти. Обычно если итерироваться по указателю, то когда-то достигнем нулевой байт \0 (нуль-терминатор), который указывает на конец строки. Все строковые алгоритмы Си завязаны на признак \0 как на конец строки.

=====
2️⃣ std::string - класс строки, владеющий памятью для нее в куче. Запись std::string s = "abcd"; значит, что где-то в куче занята память под байты abcd\0. Известно, std::string гарантированно нуль-терминирован (начиная с C++11).
Маленькие строки полностью помещаются на стек (это называется small string optimization), но пока проигнорируем это.

=====
3️⃣ std::string_view - класс строки, не владеющий памятью. Представляет собой пару const char* s (начало строки) и size_t len (длину строки).
Не обязательно верно то, что *(s + len) == '\0'. 😁 Ведь std::string_view указывает не на всю строку, а только на какую-то ее часть.

=====
Класс std::string поведением похож на контейнер std::vector<char>. Можно посмотреть на какие-нибудь неклассические контейнеры, чтобы создать новые строковые классы, которых нет в стандартном C++.
4️⃣ SmallString - класс строки, владеющий памятью для нее, с поведением как у SmallVector<char>. Реализован в LLVM.
Запись
    std::string s1;
SmallString<256> s2;
Дает два объекта s1 и s2, у которых одинаковый набор методов, но s2 хранится на стеке, если размер строки не превышает 256 символов (планируется, что так будет в 99.9% случаев). Если размер все-таки превысили, то строку начинают хранить в куче.

=====
В бизнес-логике со строками есть проблема. Иногда в коде надо делать много составных строк. Например, для создания строки - "версии" программы нужно сложить несколько строк-частей:

Major + "." + Minor + "." + VersionPatch

В этом случае происходит создание 3 (!) лишних "временных" строк с аллокациями памяти, то есть делается строка Major + ".", потом строка (Major + ".") + Minor и так далее. Более того, итоговая строка (4-я по счету) тоже по сути лишняя, если мы хотели сразу записать итог в какой-нибудь файл, а не хранить результат сложения.

В кодовой базе LLVM есть решение, которое сложно для понимания, но мы его разберем:
5️⃣ Twine - класс "сумма строк". Документация по Twine, но больше информации в исходнике.

Трудности начинаются на уровне названия класса, как у не-носителя английского 😁 Я так и не понял смысл названия.
Вообще, трудности сначала были со словом string. До того, как я начал программировать, у меня это ассоциировалось со стрингами, которые носил Борат. У этого слова куча значений, пусть в нашем случае это будет шнур.
Теперь посмотрим на слово twine. У него тоже вагон значений, пусть в нашем случае это будет бечёвка, пнятненько?
</конец бесполезного абзаца>

Этот класс опасный: он полагается на стремное правило Reference Lifetime Extension, а также на не менее стремное правило, что объекты, созданные для использования в full-expression, не удаляются до конца выполнения этого full-expression (сформулировал как смог).

Функция должна принимать Twine по константной ссылке:
void foo(const Twine& T);
А подавать туда Twine нужно не отходя от кассы, чтобы сработало правило RLE:
foo(Twine(Major) + "." + Minor + "." + VersionPatch);

Благодаря правилу про full-expression, все составные части строки "живы" на стеке, пока не выполнится вызов foo.

Twine внутри себя выглядит как бинарное дерево. У него два "ребенка":
    Child LHS;
Child RHS;
Каждый ребенок это указатель на какой-нибудь строковой объект: const char, или std::string, или std::string_view, или другой Twine ("поддерево"). Также для удобства поддерживаются числа 😁
    union Child
{
const Twine *twine;
const char *cString;
const std::string *stdString;
/* ... */
int decI;
/* ... */
};

ПРОДОЛЖЕНИЕ В ПЕРВОМ КОММЕНТАРИИ (у телеграма ограничение по размеру постов 😟)
Please open Telegram to view this post
VIEW IN TELEGRAM
#opensource

Обзор на Boost 🆘

Boost это широко известный набор библиотек для C++. Boost оказал большое влияние на развитие C++, но что осталось от его влияния в 2023 году?

Чтобы не копаться в библиотеках самому, можно почитать про разработку в Boost в крутой книге крутого Антона Полухина, которой уже прилично лет, но ее держат в актуальном состоянии.

Надеюсь, не проспойлерю книгу (на сайте все равно есть исходники примеров), но очень, очень много чего в Boost вошло в стандарты C++, и вы половину книги будете читать про то, как работают классы boost::shared_ptr<T> и boost::string_view. Работают они почти так же, как канонические std::XXX, но иногда отличия бывают (в книге рассказано, какие именно).

Эта часть Boost отдала свои жизненные соки Стандарту C++ и перестала быть интересной (кроме как тем, кто пишет на C++ старого стандарта и не может перейти на более новый стандарт).

Далеко не все куски Boost находятся в ажурном состоянии. Сейчас в Boost состоит 169 библиотек, во многом независимых друг от друга. Практически у всех библиотек есть какие-то реальные проблемы из этих:

1️⃣ Не обновлялась с ~2006 года. Да, некоторые библиотеки просто написаны сумрачным гением тысячу лун назад и заброшены.

2️⃣ Стала неактуальной после вхождения в Стандарт C++. Это описал выше.

3️⃣ Повторяет другие библиотеки по функциональности. Бывают приколы как тупо Boost.Variant и Boost.Variant2.

4️⃣ Есть нишевая библиотека вне Boost с лучшим функционалом. Можно сравнить Boost.JSON и nlohmann/json.

В целом Boost так себе в нишевых библиотеках, количество контрибьюторов в отдельную библиотеку намного ниже, чем в популярный проект.
Иногда кто-то хочет усугубить проблему и добавить библиотеку по типу Boost.Lua (еще одну к овер9000 библиотекам про Lua), но к счастью количество библиотек растет не так быстро.
Видимо, делаются попытки с уверенностью, что Boost сам по себе типа как бы бренд, и библиотека становится лучше как бы самим фактом наличия в Boost... Что не так.

5️⃣ Лютая дичь и эрзац-компилятор. Это легендарные библиотеки-монстры, которые выглядят очень странно, потому что нестандартными способами обходят ограничения компиляторов, или просто "чем хуже тем лучше". Я бы отнес их использование к ненормальному программированию.

Например, Boost.Hana для метапрограммирования
struct Person {
BOOST_HANA_DEFINE_STRUCT(Person,
(std::string, name),
(int, age)
);
};

Boost.Spirit как LL-парсер, который притом header-only (поэтому собирается по 10 минут).

Есть библиотеки для имитации std::move до C++11 и прочие попытки перепрыгнуть выше крыши.

6️⃣ Не лучшая техническая реализация. Некоторые фанаты open source верят в миф, что стоит проекту бытоваться открытым, как тут же появляются "тысячи глаз", которые следят за его качеством. На деле никому это нафиг не нужно это вряд ли так, и большой вопрос, где качество кода в среднем лучше.

Из тех библиотек, что я активно исследовал:

Boost.ScopeExit - в другом опенсорсном проекте есть реализация подобной штуки без необходимости писать BOOST_SCOPE_EXIT_END в конце.

Boost.SmallVector - официально стырен из LLVM, а не придуман уникально. Почему бы тогда не использовать библиотеку LLVM?

Boost.DynamicBitset - по состоянию на 2018 год оно использовало захардкоженные таблицы, чтобы искать количество бит в числе или типа того.
Я туда сделал коммиты (github) и ускорил некоторые методы в 2 раза, если система поддерживает интринсики как __builtin_popcount.

Offtop: в 2018 году я исследовал dynamic bitset, потому что программировал тогда шахматы с кастомным размером доски (не 8x8) и std::bitset<64> не подходил. Но в итоге забросил идею, а запрограммировал шахматы только в 2022 году без dynamic bitset (мой лонгрид на хабре).

🤔 Таким образом, в 2023 году Boost может быть не лучшим выбором для активного использования!
Please open Telegram to view this post
VIEW IN TELEGRAM
Channel photo updated
#story

Сделай свой std::flat_set из C++23 📦

В C++23 были приняты новые контейнеры std::flat_set, std::flat_map (и их multi-варианты), аналогично уже существующим std::set и std::map.

В интернете почти нет нормальных объяснений их содержимого. Многие из тех, что есть, скорее запутывают. Там приводятся всякие ненужные размышления на тему локальности памяти, бенчмарки, итераторы, и так далее.
А на деле реализация этих контейнеров простая, ее может сделать каждый. Из готовой реализации можно уже самому понять свойства этого класса.

API flat_set почти не отличается от set. Добавлены новые конструкторы и перегрузки insert. Заменены редкие методы:
node_type extract(...)
insert_return_type insert(node_type&& nh);
то есть те, где юзер должен был работать с вершинами красно-черного дерева.
Можно просто заменить тип на новый и почти наверняка это скомпилируется.

Новые контейнеры являются адаптерами. Это такие контейнеры, которые сами не делают ничего "тяжелого", а только держат у себя другой контейнер и все методы пробрасывают туда. В C++ уже было три таких контейнера - stack, queue, priority_queue:
    template<class T, class Container = std::deque<T>> class stack;
template<class T, class Container = std::deque<T>> class queue;
flat_set (и ему подобные) такой же адаптер:
    template<class Key, class Compare = less<Key>, class KeyContainer = vector<Key>> class flat_set;
То есть множество представляется (по умолчанию) в виде отсортированного вектора (но можно дать и другой класс - static_vector, small_vector).

Пусть у нас есть std::vector<T> data. Попробуем реализовать для него все основные методы flat_set сами, в упрощенном виде (без кастомных компараторов и прочего) 😝

0️⃣Итераторы begin/end/..., методы empty()/size()/max_size()
Подобные методы не рассматриваем, они просто перенаправляют вызовы в сам контейнер
decltype(auto) begin() noexcept { return data.begin(); }

1️⃣ Конструктор flat_set(container_type cont);
Конструкторов там примерно 24 штуки на разные случаи жизни. Можно реализовать случай, когда дается контейнер, который изначально не отсортирован и там могут быть повторяющиеся элементы - с использованием std::unique:
    data = std::move(cont);
std::sort(data.begin(), data.end());
auto last = std::unique(data.begin(), data.end());
data.erase(last, data.end());
Сложность этого метода O(NlogN)

2️⃣ Вставка insert(const value_type& x)
Нужно найти место, куда вставить новый элемент. Если он уже существует, то вставлять не нужно. Это можно делать бинарным поиском через std::lower_bound и вставкой в вектор в этом место.
    auto lower = std::lower_bound(data.begin(), data.end(), x);
if (lower == data.end() || *lower != x) {
data.insert(lower, x);
}
Сложность этого метода O(N) в общем случае и амортизированный O(logN) в случае вставки в конец. "Амортизированный" - потому что можно попасть на реаллокацию вектора 😁

3️⃣ Удаление erase(const key_type& x) и прочие методы (find, contains, ...)
Действуют по одинаковому принципу с insert - найти место этого элемента бинарным поиском и что-то с этим сделать.
    auto lower = std::lower_bound(data.begin(), data.end(), x);
if (lower != data.end() && *lower == x) {
data.erase(lower);
}
Сложность этого метода O(N) в общем случае и O(logN) в случае удаления с конца.

4️⃣ Слияние двух flat_set
В некоторых случаях надо делать слияние двух отсортированных векторов. Это стандартный алгоритм, его часто спрашивают на собеседованиях.

Если нужно вставить несколько элементов, то можно вызвать такой метод, который вставит каждый элемент по отдельности за O(N), с итоговой сложностью до O(N^2) в среднем:
    void insert(InputIterator first, InputIterator last);

Но если есть знание, что вставляемые элементы [first, last) уже отсортированы, то можно вызвать другой метод с "мусорным" объектом в начале, который сделает слияние, это будет работать за O(N), что может быть быстрее:
    void insert(sorted_unique_t, InputIterator first, InputIterator last);

Такую задачу можно решить на Leetcode - Merge Sorted Array 🙂
Please open Telegram to view this post
VIEW IN TELEGRAM
#madskillz

Сделай свой эмулятор процессора Motorola 68000 🖥

В свободное время программисты могут заниматься странными вещами. Например, я сделал эмулятор процессора Motorola 68000. Сейчас там поддержано 94% инструкций и все нужные абстракции, это заняло ~2500 строк кода.

Мне было интересно, рекомендую это делать всем, кто хочет лучше понять работу процессора 🍅

m68k (сокращенно) это процессор, сильно обогнавший свое время. Он использовался на протяжении десятилетий в компьютерах Macintosh, Amiga, Atari, приставке Sega Mega Drive, и прочих устройствах.

1️⃣ Представление в C++

В архитектуре процессора уже есть элементы 32-битовости, но с ограничениями.
Всего есть 16 регистров 32-битных (и 1 регистр 16-битный).
Несмотря на то, что "адресные" регистры (A0-A7) 32-битные, по факту для адреса берутся младшие 24 бита. То есть адресуется пространство в 16 мегабайт памяти.
Процессор поддерживает зачаток виртуализации для многозадачных систем - обращение к регистру A7 по факту будет обращением либо к USP, либо к SSP, в зависимости от флага в статусном регистре.
🔍 registers.h - представление регистров

Процессор может что-то читать/писать по адресам 0x000000 - 0xFFFFFF (24 бита), не обязательно это будет физическая память. Иногда запись в определенные адреса будет влиять на периферийные устройства. Поведение определяется "шиной".
Эмулятор имеет дело с интерфейсом. Запись/чтение могут спровоцировать ошибку по любой причине (например, чтение по нечетному адресу). Я не использую исключения C++ в эмуляторе - объект ошибки возвращается из методов.
🔍 i_device.h - интерфейс записи/чтения памяти

"Текущее состояние" эмулятора, которое можно менять, можно представлять так:
struct TContext {
NRegisters::TRegisters& Registers;
NMemory::IDevice& Memory;
};

Операнды в инструкциях ("цели") могут указывать на адрес в памяти/регистр большим количеством способов. Для этих способов можно выделить примерно такой интерфейс с общим методом чтения/записи данных:
🔍 target.h - представление операнда в инструкциях

Последнее, самый большое представление - у инструкций. У них есть "тип" инструкции и все нужные параметры. Ассемблер очень "ортогональный", поэтому представление в виде набора переменных подходит лучше всего.
🔍 instructions.h - представление инструкций

2️⃣ Как реализовать и протестировать инструкции

Каждая инструкция занимает 2 байта. Иногда могут потребоваться 2/4 байта дополнительных данных после инструкции (обязательно четное число).

Декодирование инструкции можно написать глядя на крутую таблицу от GoldenCrystal (сайт регулярно лежит, в комментариях к посту есть PDF).

Краткое описание инструкции можно читать в этой markdown-документации. Иногда этого недостаточно, тогда можно читать длинное описание в этой книге.

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

Мне очень помогли тесты из этого репозитория. На каждую инструкцию есть 8000+ тестов, которые покрывают все возможные случаи. Суммарно тестов чуть больше миллиона.
Они могут находить даже самые мелкие ошибки - нередко бывает ситуация, что не проходятся ~20 тестов из 8000.
Например, инструкция MOVE (A6)+ (A6)+ (обращение к регистру A6 делается с пост-инкрементом) должна работать не так, как я реализовал, поэтому я сделал костыль, чтобы работало корректно.

Продолжение в комментариях (эмуляция программ)
Please open Telegram to view this post
VIEW IN TELEGRAM
#opensource

Создание своих плагинов для Vim 👩‍💻

Я не люблю холивары, в том числе на тему текстовых редакторов. У людей есть баг - они рационализируют свой выбор и убеждения.
В худших случаях это вытекает в поиск того, за чей счет бы самоутвердиться, и приписывании другой стороне ложных свойств. Так, многие любители не-Vim уверены, что в Vim нет автокомплита, а многие любители Vim бывают уверены в каких-то вовсе страшных вещах насчет не-Vim.
Я сознательно отказываюсь от рационализации своего выбора. Например, довод о "быстроте редактирования" в Vim неактуален в обстановке, когда программист намного дольше читает уже написанный код или обсуждает решение, чем пишет новый код.

Vim👩‍💻 (позже его форк Neovim👩‍💻) для меня основной редактор с 2016 года, а полностью сменил гендер "акклиматизировался" я где-то за год (за сравнимое время можно полностью выучить любой другой редактор).
В Vim каждый сам собирает свою "IDE" из 15-20 плагинов и кучи настроек.

Vim я открываю прямо на удаленной машине - потому что работаю в виртуалке.

Для интереса можно сделать свои специфические плагины, чтобы "IDE" стала удобнее. На Neovim плагины можно писать на языке Lua. Чтобы понять, что нужно делать, можно для начала почитать эту и эту доки, но в процессе написания все равно надо много гуглить.

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

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

1️⃣ Ссылка на CodeSearch
🔍 Гифка (10.7 MB)
😱 Исходник
Раньше надо было копировать текст в терминале (выделение мышью и Ctrl+C), идти на сайт внутреннего поиска кода, вставлять текст Ctrl+V, экранировать спецсимволы... Сейчас можно выделить текст (или не выделять, если нужно найти одно слово под курсором) и нажать Ctrl+S (или ввести команду :CodeSearch, но хоткей быстрее), чтобы вывелась правильная ссылка.

Это конечно никуда не годится, лучшее решение - встроить CodeSearch например в плагин Telescope 😁

По исходнику видно, насколько много костылей в Vim. Например, "слово под курсором" ищется так: vim.fn.expand('<cword>'), а позиции начала и конца выделения это '< и '>.

2️⃣ Вставка на Pastebin
🔍 Гифка (13.3 MB)
😱 Исходник
Есть тулза, чтобы отправлять файлы в нечто вроде местного Pastebin. Чтобы вставить кусок логов или кода, приходилось выделять их, делать временный файл, вставлять выделенное, сохранять текст, вызывать тулзу в другой вкладке... Сейчас можно просто выделить текст (или не выделять, если нужно вставить весь файл) и нажать Ctrl+P (или ввести команду :Paste), чтобы плагин все это сделал и показал ссылку.

Видно еще больше костылей: отсутствие многих базовых функций (их надо писать самому), выделенный текст копируется в регистр " или 0, а путь до текущего файла можно вытащить через vim.fn.expand('%') - это все надо помнить или очень сильно читать документацию.

3️⃣ Автоматический code style при сохранении
🔍 Гифка (44.1 MB)
😱 Исходник
Используется тулза clang-format для форматирования кода, но с локальными хаками и особенностями. Если код не удовлетворяет код-стайлу, то падает проверка на CI, и приходится запускать тулзу, потом смотреть, что она нормально отработала, закрывать и открывать файл, и так далее... Сейчас эта тулза запускается сама, каждый раз после сохранения файла, меняя только нужные участки кода.

Таким образом, можно написать свои плагины, чтобы уменьшить боль от ручного труда. Это, конечно, базовые плагины, и не сравнятся с монстрами наподобии Telescope или Gitsigns. К "большим" плагинам можно делать свои плагины. 😐

Гифки записывал через blue-recorder, визуализация нажатий на кнопки через screenkey.
Please open Telegram to view this post
VIEW IN TELEGRAM
#opensource

Обзор исходников Telegram Desktop 💬

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

Эти исходники находятся тут - https://github.com/telegramdesktop/tdesktop, там же внизу ссылки на инструкции по билду Telegram.

Я попробовал сбилдить Telegram для Linux на двух ноутбуках и на виртуалке, и у меня не получилось, вплоть до того что навернулось окружение рабочего стола на одном ноутбуке 😶 Сначала чинил кривые скрипты сборки, потом пытался обновить старую версию Glib, и так далее.

Зато получилось с полпинка сбилдить его для Windows 😁 Вспомнил как выглядит Visual Studio: скриншот.

Исходники самого Telegram сравнительно небольшие, но вместе с ним качается и билдится из исходников куча хрени (~25 либ включая ffmpeg и Qt, в размере несколько гигабайтов).

Десктоп написан на движке Qt, с довольно хорошим кодом, он сравнительно небольшой. После исследования известны такие факты:
1️⃣ Что-нибудь "сломать" в самом Телеграме не выйдет, так как десктоп сделан правильно - это всего лишь красивая оболочка над Telegram API, которая получает и посылает все данные из сервера.
Например, когда мы видим статус last seen recently вместо точного времени (есть такая настройка), то десктоп действительно не знает точного времени - сервер отдает ему именно такой статус.
2️⃣ Можно написать свой десктоп или даже консольный Телеграм, но на это сомнительное в своей полезности занятие уйдет слишком много времени.
3️⃣ Если собрать "свой" Телеграм и зайти туда хоть раз, то твой аккаунт ставят на счётчик ставят под колпак. За действиями с аккаунта будут следить, в случае разных приколов (спам, накрутка, другой абьюз) будет вечный бан. Об этом честно предупреждают. 🔫

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

Я сделал такую мелкую фичу: в написанном сообщении обнаруживаются все http-ссылки и заменяются на укороченный вариант из clck.ru. Это может быть нужно для безопасности, если в компании есть свой сервис url shortener для внутренних ссылок, но не хочется тратить по 5-10 секунд для вбивания каждой ссылки.

К счастью, Qt это одна из лучших библиотек C++ на свете, поэтому там есть почти всё свое. Контейнеры (как QVector, QList, QString), регексы, сеть, и очень многое другое. Поэтому такие задачи делаются сравнительно без проблем.

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

Примерный коммит, который включает короткие урлы (с параллельными походами!), выглядит так. (И еще такой кусок кода во "внешней" либе). Там работа со строками.

🔍😐 Гифка с результатом

Урлы из гифки:
исходник1, исходник2, исходник3
https://clck.ru/34A7Hv, https://clck.ru/3vyXS, https://clck.ru/8f7h6

При желании можно достаточно сильно кастомизировать Telegram, чтобы в нем показывалось больше информации (например, соответствие профиля юзера и его аккаунта во внутренней сети компании, его должность, и т.д.)
Please open Telegram to view this post
VIEW IN TELEGRAM
Channel photo updated
#creepy

Как выбить из C++ настоящий адрес объекта 🌃

Недавно коллега искал причины какого-то бага. У нас система многопоточная с большим количеством объектов, поэтому было решено вместе с логами выводить адрес объекта в памяти, к которому относится лог. Это выглядело примерно так:
    std::shared_ptr<CallService> callService = ...;
LOG_INFO("blah blah blah " << callService); // выведет адрес `callService` в конце

Спустя некоторое время оказалось, что созданные объекты пропадают с концами 😁 То есть фильтр логов по адресу показывал явно не все логи, которые должны были быть. После исследования нашли, что такие куски кода:
    void Foo::Bar(std::weak_ptr<IListener> listener) {
LOG_INFO("add listener " << listener.lock().get());
}
где listener это указатель на callService (а класс IListener - предок класса CallService), выводит этот же адрес со смещением, в данном случае было на +4 байта вперед 😒

Класс CallService имел множественное наследование, а из-за этого указатели на базовые типы могли указывать со смещением. На простом примере:
    struct Foo { int i; };
struct Bar { short s; };
struct Baz { char c; };
struct All : Foo, Bar, Baz {};
// ...
All all; // &all == 0x7ffdfa3ce770
Foo* foo = &all; // foo == 0x7ffdfa3ce770
Bar* bar = &all; // bar == 0x7ffdfa3ce774
Baz* baz = &all; // baz == 0x7ffdfa3ce776
Это логично, потому что указатель должен давать доступ к под-объектам без всяких приколов, то есть вызов bar->s должен работать одинаково, без разницы куда указывает bar. Таким образом, затея коллеги полностью провалилась, полностью.

Но есть тот факт, что виртуальные классы нормально работают со смещенными указателями. Если указатель когда-то начнет указывать не на тот адрес, то он все равно разрушится по правильному адресу при вызове виртуального деструктора.
    // IEdible - виртуальный класс, (с `virtual ~IEdible()`)
// struct Mango : IFruit, IEdible
std::unique_ptr<IEdible> edible;
{
std::unique_ptr<Mango> mango = std::make_unique<Mango>();
std::cout << mango << std::endl; // вывод "0x56366c90beb0"
edible = std::move(mango);
}
std::cout << edible << std::endl; // вывод "0x56366c90beb8"
// удаляется объект именно по правильному адресу "0x56366c90beb0"!
// с вызовом ~Mango()

То же самое верно для других виртуальных методов. Если взять указатель на предок, и он будет смещенным, то вызов метода все равно отработает корректно, а точнее - неявный параметр this будет пофикшен.
    struct IEdible {
virtual ~IEdible() = default;
virtual void Eat() = 0;
};
struct Mango : IFruit, IEdible {
void Eat() override { Eaten = true; }; // используется неявный `this`
bool Eaten = false;
};
// ...
IEdible& e = ...; // смещенный указатель
e.Eat(); // работает корректно!

Таким образом можно попробовать найти "настоящий" адрес объекта виртуального класса 😐 Для информации я прочитал две крутые статьи:
C++ vtables - Part 1 - Basics
C++ vtables - Part 2 - Multiple Inheritance
Оттуда узнаем такие факты (верные для 64-битного Linux с включенным rtti):
1️⃣ vtable pointer указывает не на начало vtable, а смещен на 16 байт от начала. Первые 8 байт это число top_offset (смещение относительно реального объекта), вторые 8 байт это указатель на объект typeinfo
2️⃣ Компилятор генерирует особые методы под названием thunk, которые фиксят смещение указателя this на реальный и потом вызывают нужный метод. В примере выше e.Eat() на самом деле вызовет thunk, который сместит this на -8 (это top_offset) и потом вызовет Mango::Eat().

Продолжение в комментарии - нахождение реального адреса!
Please open Telegram to view this post
VIEW IN TELEGRAM
#compiler

Простые тесты для C++ кода на Python 🍄🍄🍄🍄🍄

Часто хочется покрыть тестами код на C++ в проекте - не логику, которую описывает код, а сам исходный код. Сейчас для этого есть приличное количество чекеров в clang-tidy. Можно написать свой чекер, который все равно будут ревьюить много месяцев и КПД всего этого занятия близок к нулю. Для специфических проверок надо что-то колхозить самому.

Примерных проверок может быть много:
1️⃣ Запрет на использование typeid(x).name(), потому что он дает mangled имя, с советом использовать костыль, который отдаст demangled имя.
2️⃣ Запрет на использование старого API в новом коде во время масштабного рефакторинга.
3️⃣ Проверка, что в начале каждого файла находится копипаста лицензии проекта.

Можно придумать проверку на примере лямбд 😊 Пусть у нас есть такой код:
    int counter = 15;
const auto addEvent = [&counter](int number) {
if (counter > 0) {
// do something...
--counter;
}
};
// ... call the lambda
addEvent(1337);
В этом примере лямбда использует внешнюю переменную, которая влияет на логику.
Если переменная используется только внутри лямбды, то ее можно вкостылить прямо в capture list. Тогда вместо двух верхних строк примера будет такая строка, дающая аналогичный результат:
    auto addEvent = [counter = 15](int number) mutable {
Такой же подход работает для переменных любых типов. Если интересно, что происходит внутри лямбд, то можно почитать целую книгу про них.

Попробуем сделать тест на такие кейсы 😁 Если мы для этого используем библиотеки clang, то логика такая:
1️⃣ Получаем AST (Abstract Syntax Tree) из исходного кода.
2️⃣ Лямбды в AST это вершины LambdaExpr.
3️⃣ "Объявление переменной" в AST это вершина VarDecl.
4️⃣ "Использование переменной" (в каком-то месте) это вершина DeclRefExpr.
5️⃣ Если все "использования переменной" находятся внутри какого-то одного и того же LambdaExpr, а "объявление переменной" находится вне этого LambdaExpr, то тест должен упасть, потому что данную переменную можно всунуть в capture list лямбды.
6️⃣ Делаем рекурсивный обход AST с корня и держанием указателя на "текущую лямбду", и делаем проверку на пункт 5.

Этот тест можно сделать на 👩‍💻 C++: вкостылить новый чекер в локальной сборке clang-tidy или просто сделать программу на libclang. Но можно сделать то же самое на 👩‍💻 Python и кода будет меньше в несколько раз, и это делается быстрее - менее муторно. Python хорошо подходит для быстрого написания разных тестов.

Поддерживается libclang в Python, и после просмотра примеров можно поставить его себе:
    pip install pytest
pip install clang
pip install libclang
и сделать такой простой тест, где реализуется описанная проверка для лямбд. Можно в директории рядом сохранить тестовый файл source.cpp и проверить, что тест падает:
    python3 -m pytest test.py

Вывод:
E           Failed: These variables can be declared in lambda capture: 
E "counter" (at source.cpp:3:44), to lambda at source.cpp:4:88

libclang на Python выглядит нормально, но неприятно то, что для понятия "нода AST" не к месту придумали новый термин "курсор".
Также не хватает некоторых очевидных фичей, например получения родительской ноды: в курсорах есть пара ссылок на другие курсоры (типа родительские, двух видов), но они работают неправильно.

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

Еще можно делать "исправляторы" исходников - которые берут AST, что-то туда дописывают и сохраняют в другой файл, а компилятор имеет дело с уже поправленным AST (то есть с этим другим файлом).
Please open Telegram to view this post
VIEW IN TELEGRAM
#youtube

Поваренная книга пирата: создание своего торрент-клиента 🏴‍☠️

https://youtu.be/usVMq7LDW1Y

Когда есть желание написать о чем-то прикольном, обычно я делаю это в двух форматах:
1️⃣ Статья на Хабр - кондовые лонгриды, которые можно читать, помолясь и перекрестясь.
2️⃣ Этот канал - небольшие приколы, которые быстро читаются.

Сейчас я решил попробовать новый формат - а именно формат видео на ютубе.

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

Возможно, такой формат будет более удачным. Так можно накидать неограниченный объем информации (как на Хабре) и оставаться неформальным (как в Телеге) 😁

Заранее прошу прощения за мое чувство юмора, и дисклеймер: старайтесь не пиратить, не бухать, не накуриваться; а если очень хочется, то делайте так, чтобы никто не узнал 😡
Please open Telegram to view this post
VIEW IN TELEGRAM
C++95 pinned «#youtube Поваренная книга пирата: создание своего торрент-клиента 🏴‍☠️ https://youtu.be/usVMq7LDW1Y Когда есть желание написать о чем-то прикольном, обычно я делаю это в двух форматах: 1️⃣ Статья на Хабр - кондовые лонгриды, которые можно читать, помолясь…»
#madskillz

Как правильно выполнить любой код до и после main() 🏃

Функция int main() не является настоящей "точкой входа" в программе. До того, как зайти туда (и после него), программа выполняет много другого кода, в том числе юзерского.

Почти всегда этот код - инициализация статических объектов, которая выполняется до main(). Программа:
    std::vector<int> MakeVector() {
std::cout << "do MakeVector" << std::endl;
return {1, 2, 3};
}
std::vector<int> VECTOR = MakeVector();
int main() {
std::cout << "do main" << std::endl;
}
Выведет такое:
    do MakeVector
do main
Пример на godbolt

В этом коде можно делать что угодно (читать файлы, записывать в поток и т.д.), окружение там полностью готово. Я часто использую лямбды:
Пример на godbolt 1, godbolt 2.

😀 Есть крутой лонгрид (со схемами) про то, что происходит до и после main(): Linux x86 Program Start Up.
Но все описание там "примерное", потому что поведение зависит от компилятора и его версии 🖥
В моем случае некоторые действия находятся в другом положении схемы (увидел по gdb), и еще есть дополнительная вложенность - есть функции для отдельных .cpp-файлов, а там внутри уже вызовы для инициализации переменных.

⌨️ Для того, чтобы "вызвать функцию до main()", можно пометить ее специальным атрибутом.
А именно __attribute__((constructor)) или менее вырвиглазно [[gnu::constructor]].

Здесь слова "constructor" и "destructor" не относятся к классам в C++, это исторические названия еще до C++.

Пример на godbolt с ctor-ами и dtor-ами

Однако здесь есть типичный прикол с std::cout 😁
👍 Пример по ссылке выше, скомпилированный на x86-64 clang trunk по состоянию на 13.06.2023 работает нормально.
Более старый релиз x86-64 clang 16.0.0 дает сегфолт в кторах и будет работать только если поменять там на printf - ссылка на godbolt.
😠 Это связано с тем, что std::cout и подобные объекты инициализируются в конструкторе статического объекта std::ios_base::Init (объявление этого объекта находится в хедере <iostream>).
А компилятор данной версии так скомпилировал, что кторы выполняются ДО НЕГО и пытаются обратиться к неинициализированному объекту.
Функция printf таких спецэффектов не имеет, там просто перенаправление в нужный системный вызов (syscall) без регистрации и СМС.
Мораль думайте сами 👍

Для чего может быть полезна такая штука? Лучше описать как "вопрос-ответ"

1️⃣
Вопрос: В чем принципиальное отличие этого кода:
    [[gnu::constructor]] void foo() {
// приколы
}
от этого:
    struct Dummy {
Dummy() { /* приколы */ }
};
Dummy dummy;
или более укуренного этого:
    const auto dummy = []{
// приколы
return nullptr;
}();
Ответ: Никакого, кроме более удобной записи.

2️⃣
Вопрос: Зачем это все надо, почему нельзя этот кусок кода поместить в int main()???
Ответ: Иногда нужно выполнить кусок кода, только если мы линкуем какую-то библиотеку или подключаем хедер. Например, если в библиотеке хэширования есть расчет хитрых данных для хэшей на старте программы в [[gnu::constructor]], то не надо париться с тем, как и что вызвать в main(), можно просто линковать библиотеку и заработает.

Да точно это же и происходит для std::cout, как мы только что увидели выше!

3️⃣
Вопрос: Какие жесткие проблемы решает эта запись?
Ответ: В кторах и дторах можно указывать приоритет 😐 Чем он выше, тем позже выполнится ктор (и соответственно раньше дтор).
Пример на godbolt.
Это решает артиллерийский выстрел в ногу под названием Static Initialization Order Fiasco. Очередность выполнения инициализаций обычно зависит от порядка линковки, а с приоритетом стало можно это нормально разруливать! 😎

В остальном проблемы линковки все еще остаются легендарными - можно почитать пример особо опасной проблемы, о которой писал раньше: https://www.group-telegram.com/cxx95.com/76
Please open Telegram to view this post
VIEW IN TELEGRAM
#theory

Замороженные корутины 🧊

Am Ende bleib ich doch alleine
Но в конце я остаюсь один
Die Zeit steht still und mir ist kalt
Время остановилось, и мне холодно

Этот пост больше теоретический, чем практический - просто эта тема сложная, чтобы даже иметь какой-то minimal viable product 😱

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

1️⃣ Разработка "тасок" по триггеру

"Таска" это такая микро-программа на Python, которая должна запускаться по какому-то триггеру и делать что-то обычно небольшое: отправка email, создание тикетов, http-запрос в сервис, и прочее 👦 Сервис выполняет эти программы на каком-нибудь свободном сервере.

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

Есть проблема - система не может как бы "продолжить" код таски с той точки, где было ожидание таски. Был придуман нереальный костыль, который выглядит примерно так:
    def on_execute(self):
if not self.Context.config_stage:
self.Context.config_stage = True
param1 = ...
param2 = ...
task = sdk.CreateTask(param1, param2, ...) # создаем дочернюю таску №1
raise sdk.WaitTask(task)
if not self.Context.run_stage:
self.Context.run_stage = True
param = ...
task = sdk.CreateTask(param, ...) # создаем дочернюю таску №2
raise sdk.WaitTask(task)

Сервис сохраняет "контекст" между разными запусками таски, а метод on_execute каждый раз запускается сначала ♻️
В "контексте" можно ставить флажки, чтобы имитировать разные стадии таски, еще туда можно сохранять списки/строки/числа ✍️
Наконец, оповещение сервису о том, что текущей таске надо дождаться другую таску, делается через бросание исключения (raise), чтобы это "прорвалось" за пределы def on_execute (удобнее, чем просто return, если вызываются всякие вложенные методы) 🤔

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

2️⃣ Микросервисы в Apphost

По ссылке открыто описано что такое Apphost - фреймворк для микросервисов (на Python или C++).
Он решает многие старые проблемы, но появляется новая проблема - теперь программа не может просто так сделать HTTP-запрос.
Нужно разбивать всю программу на микроскопические куски кода, которые принимают некие "входящие" объекты и формируют "исходящие", но не более того. Все походы во внешние сервисы делает Apphost. Эти куски кода выполняются на разных серверах.
Та часть, которая раньше делалась мгновенно, сейчас делается в 10 раз медленнее из-за настройки конфигов, жесткого обдумывания "графа" выполнения, и так далее.

Использование корутин

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

На самом деле, проще всего изучать корутины в простых языках - здесь статья про корутины в Lua. Общий подход не меняется.

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

Суть идеи, которая решит проблемы, которые я описал - замороженные корутины: программа выполняется на одном сервере, потом останавливается, ожидает возобновления, и выполняется на каком-нибудь другом сервере с той точки, где была остановлена 😎

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
Please open Telegram to view this post
VIEW IN TELEGRAM
#cringe

Про SJW в C++ комьюнити 🤡

Настало время обратить внимание на такую суперважную для Барнаула (Алтайский край) проблему, как борьбу за социальную справедливость в комьюнити по C++.

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

Я написал ему на почту - что произошло и кто может отревьюить патч. Оказалось, что сообщество его "отменило" aka "закэнселило" 🤡

В 2011 году он, видимо, последовал совету Трампа ("grab them by the pussy"), после чего получил две уголовки и попал в почетный список Форбс "U.S.'s sex offender registry".

Зачем об этом знать и какое отношение это имеет к C++?
Оказывается (как он мне написал), в 2021 году группировка с includecpp.org об этом факте узнала и начала попытки "отменить" еб*ря-террориста со всего интернета, и оффлайн тоже. 🔫

По этой причине он, например, не выступал на CppCon 2021 - для этого борцуны в твиттере угрожали Гербу Саттеру, а также выгнать Microsoft из комитета по C++.

И соответственно из LLVM его также выгнали - якобы по причине нарушение "Code of Conduct".

Далее произошел небольшой прикол (это было начало 2022 года) 😁
- Я в ответ написал ему, что понимаю его - меня также "отменяют", ограничивая в правах пожрать в макдаке и оплатить онлифанс, хотя я даже не трогал маленьких девочек.
- На что он бомбанул и ответил в общих чертах, что тут как раз все правильно, SJW ни при чем, наши страны находятся в состоянии войны, тебя разъ*бут как Ирак, зато ты после этого получишь аналог Плана Маршалла 😱

От такой щедрости я почувствовал мем с пингвином. Особо не ответил ему, но надеюсь что он больше не показывает своего Маршалла местным Лолитам 😁

Однако вернемся к нашим маленьким фанатам экстремизма. Сайт includecpp.org выглядит максимально клоунским.
Достаточно посмотреть на "code of conduct" и пооткрывать твиттеры в ссылках под "the moderation and administration team includes", чтобы начать подозревать, что в западных странах есть проблемы с доступностью психотерапии для населения.

На мой взгляд, корнем проблемы является то, что комьюнити по C++ не объединено никаким бизнес-интересом - развитие языка и компиляторов не идет слишком быстро.

Из-за этого, когда толпе нечего делать, они находят всякие причины поссориться с кем-либо, и особенно крикливые объединяются в организованные группировки. В целом это оставляет плохое впечатление и желание дистанцироваться от любого рода "тусовок".
Похожие драмы происходят в Rust и прочих языках, и мне все-таки кажется что нормальный человек не будет таким заниматься.

Я пофантазировал и представил себе "Моральный кодекс C++-программиста в 2030 году", через несколько лет посмотрим насколько я попал 🚬
1️⃣ Запрет паттерна проектирования "фабрика" и слов в коде как work, job и прочее, так как это угнетает права безработных
2️⃣ Одобрямс легализации насвая в стране, истинная вера в то что это "менее вредно чем алкоголь", прослушивание учёных которые доказывают это "научными фактами"
3️⃣ Подписывание бумажки "я признаю Rust Foundation террористической организацией за вторжение в Linux Kernel и оккупацию 20% Stackoverflow"
4️⃣ Переменные наподобии номера гендера хранить в int16, потому что int8 не будет хватать
5️⃣ Не менее 28.2% C++-программистов должны составлять иностранные специалисты из беднейших регионов мира, с такой же зарплатой как у всех
6️⃣ Мультиязычность - одна часть проекта должна быть на C++, другая на Python, третья на Delphi, четвертая на JavaScript и т.д. Реклама, что такой подход делает проект более технологично богатым. Всякие утверждения, что некоторые языки не совместимы друг с другом и что моноязычный C++-проект проще разрабатывать - языковой шовинизм.
7️⃣ Из-за того, что комитет по C++ когда-то попытался поглотить комитет по C, каждый C++-программист теперь должен платить репарации C-программистам. За утверждения, что C и C++ родственные языки, имеющие общую историю и происхождение, которые совместимы в одном проекте, следует немедленный кэнселинг.
Please open Telegram to view this post
VIEW IN TELEGRAM
C++95
billy.png
#books

Обзор книги "Practical Binary Analysis" (2019 г.) 📚

(можно скачать PDF тут)

На фотографии внештатный корреспондент паблика "C++95" Билли, прочитавший данную книгу.
Его отзыв: "Oh yeah, это точно стоит three hundred bucks! ASM WE CAN!"
📞

Я давно ничего не писал из-за занятости, но сейчас смог 🫡

Эта книга посвящена разнообразному анализу бинарников, то есть исполняемых программ без соответствующего исходного кода. В ней очень много информации, больше чем в любой другой книге из #books - ее реально читать неделями.
Там есть знания, которые могут пригодиться разве что reverse engineers и vulnerable researchers, но и другим тоже может быть интересно 🤔

В 1️⃣ главе дается анатомия бинарника и как он вообще появляется на свет - как в книге про компиляцию.

Во 2️⃣ и в 3️⃣ главах описываются форматы исполняемых файлов ELF (на Linux) и PE (на Windows)

В 4️⃣ главе строится своя тулза для загрузки бинарника с помощью libbfd. В книге вообще есть упор на создание собственных анализаторов бинарников.

В крутой 5️⃣ главе решается задача из CTF по поиску "флага" в бинарнике, на примере которой применяются десятки подходов и тулз, чтобы шарить еще лучше.

В 6️⃣ главе, которая на мой взгляд является центральной, показывается фундамент анализа и дизассемблирования 🖥

Там народу являют жестокую правду - в общем случае нет какого-то единого алгоритма дизассемблирования.

Более тупые дизассемблеры (как objdump) имеют линейный алгоритм - тупо расшифровывают ассемблерные команды от начала до конца.
Так как компиляторы могут встраивать вспомогательные данные прямо в код (особенно Visual Studio), то можно например напороться на jump table. По факту это список адресов, но дизассемблер об этом не знает и нарасшифрует в этом месте ассемблерные команды 👍
Ничего не упадет, потому что в x86 "плотный" набор команд и практически любая последовательность байтов представляет собой валидный ассемблер 👍

Более умные дизассемблеры (как IDA Pro) имеют рекурсивный алгоритм - они расшифровывают как бы настоящий код и только те места, куда управление доказанно может перейти, например через jmp. Там свои сложности с неявным control flow, когда в коде есть jump table или vtable, которые неявно указывают куда передавать управление, но это решаемо.

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

Чтобы определить, где находится условная "функция", дизасм ищет "сигнатуры", то есть паттерны ассемблерных инструкций, которые часто используются в начале/конце функции.
Пример сигнатуры в неоптимизированном x86 это "пролог" push ebp; mov ebp,esp и "эпилог" leave; ret
Есть и более сложные кейсы, когда делается "tail call", а оптимизированные функции могут не иметь пролога и/или эпилога, поэтому ошибок может быть от 20% и больше.

Очень сложно восстанавливать структуры данных (C/C++), дизасмы часто не пытаются этого делать ☔️

Для более простого анализа часто используется промежуточное представление (IR, Intermediate Representation) ассемблера - например, вместо исходника где могут быть сотни разновидностей инструкций x86/ARM, можно получить IR где единицы разновидностей. IR бывает несколько, например REIL.

Дизасм восстанавливает код, который делает примерно то же, что ассемблер. Обычно выглядит он все равно довольно ужасно, но дизасмы стараются 🥺 Например, по эвристикам (то есть через три костыля) определяются простые циклы (for/while), математические формулы, и прочее.

ПРОДОЛЖЕНИЕ В КОММЕНТАРИИ
Please open Telegram to view this post
VIEW IN TELEGRAM
C++95
ban.png
#cringe #books

На данном скриншоте - последствие того, как автор поста на два дня положил систему для практикума форк-бомбой.

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

Обзор книги "Введение в язык Си++" (2012/2020 г.) 📚 🤪

Jeder lacht, jeder lacht — ja
Все смеются, все смеются, да
Jeder lacht über dich
Все смеются над тобой

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

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

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

Книжка намеренно небольшая, потому что рассчитана на 6-8 лекций. За 4 года бакалавриата на C++ больше времени не находится, ведь так в учебный план не поместятся философия, урматы и основы экономики.
А ведь плюсы это такая глыба, что полный справочник позволяет Столярову обороняться от своих многочисленных врагов... 🔫👀🔫
    Полное описание всех возможностей языка и его стандартной библиотеки представляет собой книгу такого объёма, что ею вполне можно при желании воспользоваться в качестве оружия против врагов (в смысле чисто физическом).

Предполагается, что читатель уже знает язык C и с него переучивается на C++, там описывается разница между языками
    Для изучения базовых средств языка Си++ вам потребуется уверенное владение языком Си
Звучит аналогично тому, чтобы изучить французский язык, надо сначала выучить латынь (откуда le français произошел), а потом выучить дифф между латынью и французским 😈
Языки упорно называются "Си" и "Си++", одобряем патриотический перевод 👍
В главе Язык Си++ и его совместимость с Си нет ни слова про действительную совместимость языков через extern "C".

    В языке Си++ введен механизм **защиты**, который позволяет запретить доступ к некоторым частям структуры
Интересно, как "инкапсуляцию" перевели как "защиту"... Еще можно переводить "полиморфизм" как "превентивный удар", а "наследование" как "хлопо́к". Там много такого надмозгового перевода.

    Мы могли бы написать и так:
double mod = str_complex(2.7, 3.8).modulo();
В последнем случае мы создали временную **анонимную переменную** типа `str_complex`
Фразу "unnamed object" из Стандарта лучше перевести как "безымянный объект". Но лучше бы были рассмотрены категории выражений (lvalue/rvalue и их подвиды) и их поведение, вместо этого всё объясняется "на пальцах" и размыто, а про move-семантику нет ни слова.

На примере показывается полезность инкапсуляции в C++. К сожалению, не описана типичная Сишная идиома для инкапсуляции opaque data type, что было бы к месту для тех, кто переучивается с С на C++.

В главе про const ничего не сказано про ключевые слова constexpr и (!) mutable, что плохо, потому что не складывается понимание что это слово про логическую константность, а не физическую.

Там же рекомендуется передача переменной через константную ссылку:
    Так, если в описанном ранее классе `Complex` вместо
Complex operator+(Complex op2) { ... }
написать
Complex operator+(const Complex &op2) { ... }
семантика кода не изменится, но физически вместо копирования двух полей `double` будет происходить передача адреса ...
По крайней мере для 16-байтовых типов это неверно - их на x86-64 выгоднее передавать через копию. В проектах можно использовать чекер clang-tidy: пример проверки на godbolt.

ПРОДОЛЖЕНИЕ В КОММЕНТАРИИ
Please open Telegram to view this post
VIEW IN TELEGRAM
#opensource

Обзор на TeX 📖

Ist doch so gut gewürzt und so schön flambiert
Так хорошо приправлено и красиво обжарено
Und so liebevoll auf Porzellan serviert
И с любовью подано на фарфоре
Dazu ein guter Wein und zarter Kerzenschein
Под хорошее вино и нежный свет свечей
Ja, da lass ich mir Zeit, etwas Kultur muss sein
Да, я не тороплюсь, капля культуры должна быть!

В опенсорсе самым 𝓪𝓮𝓼𝓽𝓱𝓮𝓽𝓲𝓬𝓪𝓵 проектом является скорее всего TeX от Дональда Кнута. Это система для создания красивых документов, особенно для научных работ и технических книг 📖 Многие хотя бы раз его использовали, например для описания математических формул.

Про него есть много информации, но почти вся поверхностная. Стало интересно, что там внутри.

Можно смотреть "снизу вверх":
1️⃣ Шрифты - набор изображений символов (обычно векторных), сейчас популярен формат TrueType.
Векторные изображения переводятся в пиксельные на мониторе с приколами наподобии хинтинга и ClearType.

Многие стандартные шрифты имеют разное начертание для разных размеров, чтобы выглядело красивее. То есть буквы в 10м кегле (10pt) это не то же самое, что увеличенные в два раза буквы из 5pt 🤵

Есть шрифты, где записаны математические значки или другие символы в векторном виде, например знак ⚠️, это удобнее чем вставлять картинки.

Это обширная тема, в линуксе даже есть мемчик "шрифты говно" 💩

2️⃣ Формат файлов DVI - выходной файл TeX. Чтобы "прочитать" этот файл, надо хранить состояние из нескольких переменных и выполнять "команды" из файла побайтово. Например, команда set_char_a как бы "печатает" в текущем месте один символ a и сдвигает курсор вправо на ширину символа a.

Этот файл не содержит в себе шрифтов, а только "ссылается" на них, и знает из шрифта высоту/длину/глубину символов. На картинки он тоже только ссылается, и дефолтные DVI-просмотрщики не показывают их.

Смысл этого формата - получать один и тот же документ на любом компьютере 🖥

3️⃣ TeX - сам набор программ и язык разметки.

Самая подробная информация есть в книге от автора The TeXbook в 400+ страниц, можно пропускать слишком подробные куски 🔍
В других книгах как обычно выкинут 80% информации и еще неправильно переведут (например, "инкапсуляцию" как "защиту").

У ванильного TeX примерно 900 команд, но только ~300 команд являются базовыми. Например, команда \TeX для вывода лого TeX является макросом для такой последовательности:
    T\kern -.1667em\lower .5ex\hbox {E}\kern -.125emX
То есть печатается символ T, потом печатается E со сдвигом от изначального места немного вниз и влево, потом печатается X со сдвигом влево 📄

В The TeXbook описано, как текст из .tex-файла переводится в токены, какие есть категории токенов (глава 8)

В отличие от WYSIWYG-редакторов, TeX работает на уровне абзаца, а не строки, и распределяет слова по отдельным строкам так, чтобы минимизировать метрику badness.

В TeX есть понятие "клей" (glue) - это объект-отступ между разными сущностями TeX 🔺 "Клей" после запятой растягивается в 1.25 раз больше, чем "клей" между соседними словами, а "клей" после точки/знака !/знака ? соответственно в 3 раза больше.

"Клей" может быть бесконечно растяжимым/сжимаемым, например команда \centerline это макрос
    \line {\hss #1\hss }
Команда hss ставит бесконечный "клей", и TeX располагает параметр макроса (то есть #1) ровно в центр строки.

Есть огромная куча костылей, чтобы менять любое правило.
Например, чтобы написать в документе Mr. Evil с отступом как между соседними словами (а не с х3 отступом как для нового предложения) надо писать Mr.\ Evil.
Чтобы было нельзя перенести Evil на следующую строку без Mr., надо писать Mr.~Evil (~ это тот же пробел).

Можно поставить одинаковый glue stretching (после слов, запятых и точек) командой \frenchspacing.

Можно сформировать длины строк в форме полукруга (в The TeXbook есть примеры) командой \parshape, и так далее...

ПРОДОЛЖЕНИЕ В КОММЕНТАРИИ
Please open Telegram to view this post
VIEW IN TELEGRAM
2025/06/29 19:08:10
Back to Top
HTML Embed Code: