C++95 – Telegram
C++95
1.52K subscribers
3 photos
21 files
115 links
640K ought to be enough for anybody

Author: @cloudy_district aka izaron.github.io
Download Telegram
#creepy

Миф о виртуальных деструкторах 🍅

На собеседованиях и в реальной жизни часто встречается вопрос: "Зачем нужен виртуальный деструктор?"

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

Но есть логичный вопрос: почему бы тогда компилятору не генерировать виртуальный деструктор автоматически (для классов с виртуальными методами)? 🤔
Ведь он кучу всего генерирует сам (например move- и copy-конструкторы, move- и copy-операторы присваивания).

Ответ: Потому что утверждение выше неправильное! 😱 Виртуальный деструктор нужен только тогда, когда есть риск, что объект класса-наследника могут разрушить по указателю на класс-предок.

Пусть есть базовый виртуальный класс DrinkMachine и его наследник класс CoffeeMachine.
Каждый объект программы рано или поздно надо разрушить, это делается в 2х разных случаях:
1️⃣ Объект создан на стеке, тогда программа сама вызовет деструктор
{
CoffeeMachine cm;
// перед выходом из scope сам вызывается cm.~CoffeeMachine()
}
В этом случае неважно какой деструктор (виртуальный или нет), потому что в обоих случаях вызовется то что нужно - деструктор реального объекта.

2️⃣ Объект создан в куче, тогда программист сам делает разрушение объекта, обычно так:
DrinkMachine* dm = ...; // возможно вместо `...` здесь был `new CoffeeMachine()`
// ...
delete dm;
(или если у нас объект по типу std::unique_ptr<DrinkMachine> - происходит то же самое)

Оператор delete обычно делает две вещи: вызывает деструктор и освобождает память:
dm->~DrinkMachine();
std::free(dm);

В этом случае важно чтобы вызвался именно деструктор реального объекта, т.е. возможно мы на самом деле хотели бы вызвать ~CoffeeMachine(). В этом случае нужен виртуальный деструктор - он будет лежать во vtable, и как метод будет находиться динамически.

Если второго случая в программе не бывает, то виртуальный деструктор не нужен - например в этой программе все работает без ошибок:
void MakeDrink(DrinkMachine& dm) {
dm.MakeDrink(); // вызов виртуального метода
}
void MakeCoffee() {
CoffeeMachine cm;
MakeDrink(cm);
// cm удалится сам через `cm.~CoffeeMachine()`
}

Это может быть важно для программ, которых нужно оптимизировать, потому что вызов виртуального метода (в т.ч. виртуального деструктора) дает оверхед в виде двух memory load.

P. S. Есть такой прикол как девиртуализация - когда компилятор сразу понимает какой метод нужно вызвать (не глядя во vtable). Но для этого компилятор должен доказать, что он точно знает, какой метод нужно вызвать. Хорошая статья на эту тему.

Девиртуализация не стандартизирована, поэтому нужно проверять на своем компиляторе самому - оптимизируется ли вызов виртуального деструктора в примере с MakeCoffee выше или нет. Если да - то можно забить и всегда делать виртуальный деструктор.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥3🖕1
#creepy

ABI: три весёлых буквы 🫡

Одна из тем, которая мало заметна во внешнем мире, но вокруг которой происходит регулярный shitstorm среди тех, кто двигает C++ - это вопрос слома ABI 😀

ABI (wiki) это гарантии, которым интерфейс программного модуля (библиотека, операционная система) удовлетворяет на бинарном уровне: соглашение о вызове, размер типов данных, и многое другое.

Самый простой пример, когда это важно - когда бинарь (исполняемая программа, .exe на Windows) требует для работы динамические библиотеки (.dll на Windows или .so на Unix).
Бинарь и динамическая библиотека - две разные программы, которые должны быть всегда совместимы друг с другом. Это значит, что бинарь и библиотека должны уметь обновляться независимо друг от друга таким образом, чтобы они могли продолжать работать вместе.

(про работу динамических библиотек можно прочитать в этой статье или в этой книге)

Проблема в том, что ABI очень просто сломать - есть неполный список ломающих изменений. Одни из понятных примеров:
😀 Добавить новое поле в публичный класс
🙁 Ломает вообще всё, потому что теперь у .exe-файла (который не перекомпилируют!) неправильное смещение стека, размеры зависимых классов и так далее.
😀 Добавить новый виртуальный метод
🙁 .exe-файл теперь неправильно вычисляет адреса старых виртуальных методов.

Есть тулзы, которые проверяют совместимость ABI: abidiff.

Стандарт C++ развивается так, чтобы не ломать ABI в новых версиях стандарта - то есть чтобы перекомпилирование проекта не мешало использовать зависимые .dll/.so.
Это приводит к проблемам, которые описаны в статьях:
😀 День, когда умерла стандартная библиотека - лонгрид
😀 Цифровые демоны - лонгрид посложнее

Есть два класса проблем, связанные с сохранением ABI:
1️⃣ Нельзя сильно поменять язык, например сортировать поля класса, чтобы класс занял меньше места.
2️⃣ Нельзя нормально менять стандартную библиотеку - большинство пропозалов уничтожаются на месте (проблем этого класса в несколько раз больше).

Реализация стандартной библиотеки C++ не является частью компилятора. Стандарт C++ только определяет интерфейс, а реализаций есть несколько, и они предоставляются в виде динамической библиотеки.

Из-за этого стандартная библиотека C++ крайне бедная, ее контейнеры медленнее чем нужно, некоторые классы супер хреновые (std::initializer_list, std::regex), и так далее - и никто не может это исправить. Если задизайнить что-то новое (например парсер JSON), это потом нельзя будет нормально поменять.
Даже дизайн Win32, POSIX, протоколов Ethernet предполагает всевозможные изменения в будущем - но только не стандартная библиотека C++.

С каждым годом стоимость не-слома ABI (застой в языке) постепенно приближается к стоимости слома ABI. Пока непонятно, будет ли разрешено ломать ABI, так как минимум два вендора против:
1️⃣ GCC. Все коммерческие продукты, поставляемые в бинарном виде для Linux, надеются на неизменность ABI, благодаря чему они могут не пересобирать свой продукт под каждый новый дистрибутив.
2️⃣ Microsoft. Visual Studio с 15 версии сохраняет своё ABI, библиотеки скомпилированные в 2015 версии без проблем линкуются в 2017 и 2019.

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

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

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

Программирование с учетом ABI это не самая распространенная бизнес схема. Например, Qt ломает ABI каждый релиз, и никто от этого не умер.

(конец в комментарии)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥1🖕1
#testing #books

Обзор книги "Modern C++ Programming with Test-Driven Development" (2013 г.) 📚

(можно посмотреть тут - https://pragprog.com/noscripts/lotdd/modern-c-programming-with-test-driven-development/ в интернете есть PDF)

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

Это было мое вступление 😁 А книга описывает реалии test-driven development в C++. TDD это работа в коротких циклах, в каждом цикле сначала пишется тест на новое поведение, потом пишется реализация которая удовлетворяет тесту.
Несмотря на название, TDD это больше про дизайн системы, потому что он заставляет делать интерфейсы программы так, чтобы они были максимально тестируемы.

В принципе из книги можно понять что такое TDD, для тестов используется GoogleTest. К сожалению в книге есть минусы:

😂 Много воды
Повторяется одно и то же по десять раз, как будто тему раздувают. Также есть много капитанства.
Например, в разделе "Running the Wrong Tests" совет - если вы запускали тесты, но новый тест не запустился, то что делать? Ответ: возможно вы запускали не тот test suite, или у вас неправильный фильтр, или вы не скомпилировали тесты, или тест выключен.
В разделе "Testing the Wrong Code" - вы тестируете не тот код, если вы забыли скомпилировать модуль, или компиляция закончилась неуспешно и вы этого не заметили.
После того, как тратишь по 10 минут на какие-то банальности, начинаешь читать книгу по диагонали 😤

😂 Автор борщит
В одной главе на 50 страницах разбирается пример элементарного модуля, который типа сделали по TDD, и вместо первого приблизительного решения (как это делают в реальном мире) автор делает угарные правки, чтобы пройти следующий кейс и не более того - в итоге все переписано по сто раз.
Автор дает тупые крутые советы а-ля "Мартин Фаулер":
Your favorite tests contain one, two, or three lines with one assertion
Tests with no more than three lines and a single assertion take only minutes to write and usually only minutes to implement
Которые не имеют отношения к реальности.

😂 Слабая техническая составляющая
Очень мало написано про code coverage, CI. Время исполнения методов замеряется на коленке, хотя есть Google Benchmark для мелкого кода и Valgrind для всей программы. Dependency Inversion (чтобы можно было подсунуть мок-класс в тестах) называется сложным понятием.

😎 Вывод: Лучше прочитать доку про GoogleTest, а к тестам относиться как во "вступлении", тогда код и так будет близок к TDD без сомнительных советов.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🤔1🖕1
#creepy

Как обмануть [[nodiscard]] через std::ignore 💩

В C++17 добавили атрибут [[nodiscard]], которым можно помечать функции, чтобы тот, кто вызвал функцию, не игнорировал возвращаемое значение.

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

Оказывается - да 😀 В C++17 добавили std::ignore - это объект, которому можно присвоить любое значение, без эффекта.

[[nodiscard]] int status_code() { return -1; }
[[nodiscard]] std::string sample_text() { return "hello world"; }

void foo() {
std::ignore = status_code(); // нет warning/error
std::ignore = sample_text(); // нет warning/error
}

Но в духе C++ будет запретить std::ignore для некоторых типов. Покопавшись в реализации, можно выключить его, например для int:
template<>
const decltype(std::ignore)&
decltype(std::ignore)::operator=(const int&) const = delete;

Пример на godbolt 👦
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12😁6🤯1🖕1
#story

StarForce: привет из прошлого 💿

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

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

Сейчас про StarForce мало кто вспоминает. Системы проверки CD/DVD-носителей давно неактуальны в силу неактуальности самих дисков, а новые ноутбуки много лет производятся без дисковода.

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

Среди продуктов предлагается C++ Obfuscator, система для изменения кода до нереверсируемого хакерами состояния. Эта система добавляет в исходники лишние условия, циклы, вызовы методов. Скачать его просто так нельзя, предлагается заполнить форму, поэтому я не посмотрел его работу. По ссылке есть пример на алгоритме Евклида.

Есть несколько десятков способов обфускации.
Например, можно сделать так, чтобы нигде в исходниках не встречались строковые литералы в "чистом" виде.
В обычной программе запись
const char* key = "abacaba";
означает, что в бинарнике в секции .rodata (или аналоге) будет в открытом виде лежать последовательность байтов abacaba\0 (\0- нулевой байт). Но обфускатор может сделать так, чтобы этот литерал вычислялся хитрым способом в каком-то методе, и тогда его будет невозможно вытащить просто так.

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

После массового изменения исходного кода может произойти дурка - всплывают скрытые багов, время работы многих кусков кода меняется, ломается код с указателями и так далее...
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8😢2🔥1🖕1
#compiler

Быстрый Switch: таблица адресов 💨

В C++ оператор switch используется для передачи потока управления в разные места в зависимости от значения переменной.
Оператор switch можно представить как соответствие между значениями переменной, и кодом который должен выполниться для каждого значения.

В зависимости от целевой архитектуры, настроек оптимизации, и свойств конкретного switch-оператора, код может сгенерироваться в разном виде. Есть два варианта, какой ассемблер сгенерирует компилятор:
1️⃣ Цепочка последовательных if-ов. Это самый простой путь, потому что switch-оператор всегда представим в этом виде.
2️⃣ Таблица адресов (мой перевод), он же branch table, он же jump table.

Первый вариант неинтересен, он самый простой и самый неоптимизированный. Если в нашем switch 30 штук case-ов, то в худшем случае произойдет 30 (!) последовательных сравнений (цепочка if-ов), прежде чем программа поймет номер нужной инструкции.
На самом деле в таких случаях компиляторы умеют делать а-ля "бинарный поиск", поэтому вероятно будет log_2(30) сравнений в худшем случае 😁

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

Пример switch с таблицей адресов: https://godbolt.org/z/3debYb4vq
В этом примере в switch сравнивается значение enum-а. Для компилятора enum представляется как underlying type. По умолчанию этот тип int, то есть во всех операциях с enum происходит неявная конвертация в int.
Таким образом, можно представить, что это switch по значениям от 0 до 6 включительно.

В примере компилятор сгенерировал метки LBB0_2, LBB0_3, ..., LBB0_8 для каждого соответствующего кода case X.

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

Теперь, имея "таблицу адресов", можно вычислить номер инструкции, куда надо прыгать. Если параметр равен 0, то прыгаем по первому адресу таблицы, если 1 - по второму, и так далее.
        lea     rcx, [rip + .LJTI0_0]
movsxd rax, dword ptr [rcx + 4*rax]
add rax, rcx
jmp rax

Отступление: Как известно, метки имеют смысл только для ассемблера. Метка просто условно указывает на позицию в бинарнике (инструкцию или данные). После процесса линковки, когда в один исполняемый файл (бинарник) утрамбуются отдельные объектные файлы, вместо меток появятся нормальные адреса.
        lea     rcx, [rip + 0x012345678]

Таблица адресов может иметь другую реализацию, но такая общая идея. Например, в примере с Википедии для рандомного 8-битного ассемблера, значение переменной прибавляется к регистру счетчика команд (addwf PCL,F), а сразу после этой инструкции находится таблица с goto до нужной инструкции, и счетчик команд укажет на нужный goto.

Компилятор сам определяет, нужна ли таблица адресов. Обычно она используется для "плотных" switch, где есть case X для последовательных значений X. Если в case X поставить рандомные значения, то таблицы не получится - пример на godbolt, будут последовательные if-ы.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15👏1🖕1
#compiler #madskillz

[[assume]] - помоги компилятору сам 😎

Раньше я писал про std::unreachable (он же __builtin_unreachable до C++23) - https://news.1rj.ru/str/cxx95/58.

Эта штука делает указание компилятору, что в данную ветку исполнения программа никогда не попадет (под личную ответственность программиста), поэтому можно оптимизировать это место.

В C++23 по такому образу стандартизировали похожий функционал: атрибут [[assume(expr)]] (он же __builtin_assume до C++23).

Эта штука делает указание компилятору, что в данной ветке исполнения выражение expr следует считать равным true, и делать разные оптимизации на основе этих данных. Выражение expr вычисляться во время работы программы не будет, это подсказка времени компиляции.

На cppreference (ссылка выше) информации мало, лучше почитать "предложение" о стандартизации: https://wg21.link/p1774r8

Самый простой пример - метод, который делит число на 32:
int div32(int x) {
return x / 32;
}
Казалось бы, очевидная оптимизация - не делить на 32, а сделать битовый сдвиг на 5 битов:
int div32(int x) {
return x >> 5;
}
Но будет неправильно работать на отрицательных числах. Компилятор всегда должен учитывать возможность входа отрицательного числа, из-за этого метод больше по размеру: ссылка на godbolt.

Если программист совершенно точно знает, что все числа будут неотрицательными, то нужно сделать так:
int div32_2(int x) {
[[assume(x >= 0)]]; // или __builtin_assume(x >= 0);
return x / 32;
}
И тогда код оптимизируется: ссылка на godbolt.

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

Некоторые assume можно сделать общими для всего кода (в "предложении" есть пример с умными указателями), но в целом это вещь для узкого круга разработчиков. Есть несколько особенностей этой фичи:

1️⃣ Нужно действительно сильно зависеть от быстродействия программы, например это могут быть реалтаймовые программы. Я однажды кидал видео-выступление Тимура Думлера (автора "предложения") на эту тему - https://news.1rj.ru/str/cxx95/16.

2️⃣ Нужно понимать, за счет чего срезаются инструкции. Пример программы, которая ограничивает значения массива через std::clamp:
void limiter(float* data, size_t size) {
[[assume(size > 0)]];
[[assume(size % 32 == 0)]];
for (size_t i = 0; i < size; ++i) {
[[assume(std::isfinite(data[i]))]];
data[i] = std::clamp(data[i], -1.0f, 1.0f);
}
}
Предполагая, что размер буфера всегда больше 0 и кратен 32, а флоаты нормализованные, программист ставит assume.
Первый и третий assume не дает делать лишние проверки, а второй assume вероятно как-то связан с кэш-линией процессора.

3️⃣ Нужно постоянно лезть в ассемблер скомпилированной программы и проверять результат - а как иначе? И даже нужно делать юнит-тесты на генерируемый ассемблер (я бы по крайней мере делал). У компиляторов C++ много тестов на получающийся ассемблер, и в отдельных программах с assume они тоже нужны.

4️⃣ Стандарт отмечает, что компиляторы сами вольны оптимизировать код как смогут, никаких требований на них не налагается. Надо проверять, как работает отдельный компилятор и даже отдельная версия, для этого нужны юнит-тесты из 3️⃣ пункта

Можно сделать разные приколы с assume 😁
😱 Фиксируем вариант в switch - ссылка на godbolt.
😱 Решаем простые уравнения с переменной - ссылка на godbolt.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8😁4🤯4🔥2🖕1
#story

Самое простое объяснение std::function за 15 минут 😦

Этот пост был написан под влиянием крутого видео от Jason Turner "A Simplified std::function Implementation"

Часто люди не задумываются, как работает std::function. Чаще всего знают, что эта штука - обертка над чем-то, что можно "вызвать" как функцию. Кто-то смутно помнит, что std::function вроде как лезет в динамическую память. cppreference не сильно раскрывает внутренности реализации.

Можно сказать, что в C++ есть два типа объектов, на которых работает семантика вызова как функции. Можно условно назвать их Callable. Это:
1️⃣ Сами функции:
    int foo(int a, int b) { return a + b; }
2️⃣ Объекты типов с определенным operator(), часто их называют "функторы":
    struct foo {
int operator()(int a, int b) { return a + b; }
}
Все остальные Callable являются производными от этих двух типов. В том числе лямбды - компилятор их переделывает в структуры с operator(). Про лямбды есть хорошая книга - https://news.1rj.ru/str/cxx95/48.

А std::function<Signature> должен уметь хранить все возможные Callable с данной сигнатурой.

template<typename Ret, typename... Param>
class function<Ret(Param...)> {
// код реализации
};

Возникает проблема - у std::function должен быть фиксированный размер, но Callable типа 2️⃣ может иметь неопределенный размер. Например, размер структуры у лямбды зависит от того, какие captures он делает.

Поэтому, к сожалению, std::function хранит Callable в куче.
Также нужно использовать виртуальный класс, который для каждого отдельного типа как бы вычислит адрес вызываемого метода:

Виртуальный класс и указатель на кучу:
    struct callable_interface {
virtual Ret call(Param...) = 0;
virtual ~callable_interface() = default;
};
std::unique_ptr<callable_interface> callable_ptr;

Реализация для каждого отдельного типа Callable держит в себе сам объект Callable и метод для вызова operator() по правильному адресу:
    template<typename Callable>
struct callable_impl : callable_interface {
callable_impl(Callable callable_) : callable{std::move(callable_)} {}
Ret call(Param... param) override { return std::invoke(callable, param...); };
Callable callable;
}

Конструктор std::function принимает Callable и создает объект в куче:
    template<typename Callable>
function(Callable callable)
: callable_ptr{std::make_unique<callable_impl<Callable>>(std::move(callable))}
{}

И наконец вызов operator() у самого std::function перенаправляет вызов в содержимый Callable:
    Ret operator()(Param... param) { return callable_ptr->call(param...); }

Вот так выглядит один из способов type erasure в C++ 👍
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10🖕1
#creepy #compiler

Самое мерзкое правило в C++ для модульных программ и как его обойти 🤢

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

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

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

Есть файл module.h с такими методами (упрощенно)
    struct IModule { virtual void Do() = 0; };
void AddModule(std::unique_ptr<IModule> module);
const std::vector<std::unique_ptr<IModule>>& GetModules();

Файл main.cpp должен использовать GetModules(), а модуль должен зарегистрировать сам себя через AddModule.

Единственный способ, которым это можно адекватно сделать - добавить код, который должен вызываться на старте программы. Это делается через статическую инициализацию объектов в конструкторе объекта. Где-то в одном из .cpp-файлов модуля должно быть такое:
    struct Dummy {
Dummy() {
AddModule(std::make_shared<MyCoolModule>());
}
};
static Dummy dummy;

Дальше начинается кино. Переменная dummy и код для ее инициализации попадает в статическую библиотеку libcoolmodule.a (можно проверить через objdump), но при линковке бинарника эта переменная выбрасывается линкером как неиспользуемая. В итоге модуль не зарегистрируется.

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

1️⃣ Windows - указать на переменные, которых нельзя выбросить - ссылка
__pragma comment(linker,"/include:?variable_name@")

2️⃣ Помещение переменных в отдельные секции и пометка этих секций как невыбрасываемых - ссылка
    static Var_t g_DumbVar __attribute__((__used__, section(".var_section.g_DumbVar"))) = (const Var_t) X_MARKER;
static Var_t* g_DumbVarGuard[] __attribute__((__used__, section(".guard"))) = { &g_DumbVar };

3️⃣ Linux - также указать на невыбрасываемые переменные через параметр командной строки - ссылка1, ссылка2, переменная не должна быть static и volatile.

4️⃣ Linux - сделать link-скрипт, который указывает что и как надо линковать - ссылка

Через время поисков я нашел ответ, почему так работает линкер.
Когда собираешь бинарник, то индивидуальные объектные файлы (.o) в команде к линкеру линкуются по порядку и ничто не выбрасывается.

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

В системе сборки CMake есть метод, который позволит обойти это правило. Надо заменить такую строку:
    add_library(enum_serializer STATIC module.cpp helper.cpp)
на такую:
    add_library(enum_serializer OBJECT module.cpp helper.cpp)
И тогда, если какой-то бинарник зависит от enum_serializer, он будет линковать не libenum_serializer.a, а module.o и helper.o.
Поэтому "регистрация модуля" сработает и проблема будет решена 😁
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20🖕1
#longread

Кодогенератор Waffle++ для C++ 🧇

https://habr.com/ru/post/710744/

Я рассказал о своем проекте расширяемого кодогенератора для С++, где каждый может написать свой модуль быстро и с надежными тестами.

Уже сейчас можно получить бесплатный перевод значений enum в строку и обратно, перевод структуры в JSON и обратно, декларативный веб-сервер, систему слотов и сигналов, свой динамический полиморфизм, генератор кода для тестов...

Это может быть интересно! 🥞
https://github.com/Izaron/WafflePlusPlus
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14👏2🔥1🎉1🖕1
#advertisement

Работа на C++ в Алисе 🎙

Алиса это один из самых быстрорастущих сервисов Яндекса, где можно заниматься уникальными задачами на C++ - от embedded-разработки до машинного обучения. Я сам работаю над Алисой уже несколько лет и могу это гарантировать 🦆

Сейчас Алиса расширяется и нанимает много разработчиков на C++ (от стажеров до сениоров), поэтому я решил помочь коллегам найти крутых разработчиков из читателей этого блога.
Пишите мне (@cloudy_district) если захотите попробовать себя в разработке сервиса с миллионами пользователей! 😀
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15🖕1
#story #compiler

Порог входа для коммитов в компиляторы 🍗

Однажды в одном чате про C++ один участник высказал сомнение - имеет ли смысл делать багфиксы в существующие компиляторы, разве там нет "проблем" с тем, что желающих исправить огромно, и issues просто моментально исчезают? 😱

На деле ситуация с Clang/LLVM (и много где еще) - обратная.

Тысячи висящих issue, которых никто годами не исправляет. Ревью тоже медленное, pull request висят неделями-месяцами. Активных разработчиков - максимум несколько десятков, и то там большой перекос в единицы супер-активных. 🏃‍♂️

Многие из упомянутого актива работают "на зарплате", то есть работают в Google/Apple/%company_name% и за деньги разрабатывают Clang/LLVM, иначе наверное разработка вообще не будет двигаться.

Кроме багфиксов, реально каждый может зайти на https://clang.llvm.org/cxx_status.html#cxx20, посмотреть на то, что из C++20/23 не реализовано в Clang и поддержать новую фичу. Я такое делал пару раз. Но это были простые коммиты. Сложные предложения реализовать почти нереально, это нужно делать как фултайм работу. Поэтому не только лишь все это делают.

Например, сравнительно просто можно сделать фичу C++23 [P2324R2] Labels at the end of compound statements. У меня ушло 2 коммита - коммит 1, коммит 2.

Также какие-то подвижки можно сделать для [P0533R9] constexpr for <cmath> and <cstdlib>. В этом предложении куча новых методов помечена как constexpr. Я придумал схему для реализации - метод должен быть помечен как constexpr, если его константное вычисление поддерживается компилятором. Условный пример для std::fmax:

inline _LIBCPP_CONSTEXPR_CXX23_IF_CONSTEXPR_BUILTIN(__builtin_fmax) double fmax(double __x, double __y) {
return __builtin_fmax(__x, __y);
}

Макрос _LIBCPP..._BUILTIN внутри себя обращается к функции __has_constexpr_builtin (из моего коммита) для проверки, поддерживается ли функция в аргументе в константном вычислении.

Запись __builtin_XXX в рантайм-вычислении обычно преобразуется в просто вызов метода/какой-то код, а в константном вычислении выполняется прямо в компиляторе. Для каждой функции нужен свой коммит в компилятор (мой коммит для fmax).

Также я добавил тест, в котором можно следить за реализацией P0533 (мой коммит). Как только какой-то метод помечается как constexpr (автоматическим образом как выше), обязательно нужно поменять макрос в тесте (иначе тест упадет). Как только все нужные методы помечены как constexpr, то тест скажет что фича P0533 полностью реализована (и надо поменять статус на cxx_status.html).

Однако, спустя несколько месяцев, дело продвинулось лишь ненамного - blame теста, видно всего два коммита. Там пацан, который в основном занимается libcxx, поддержал часть функций.

Поэтому контрибьютить в компиляторы сможет любой, если есть время и желание сделать что-то новое 🚬
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14🔥7😁2🖕1
#story

Чем исключения в C++ похожи на сборщик мусора в других языках ♻️

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

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

Например, для Java и других языков со сборкой мусора это необходимость разбираться в алгоритмах сборки мусора (mark-and-sweep, etc.), в более хитрой работе с памятью чтобы сборщик собирал меньше мусора ("пул объектов"), понимать нужные настройки чтобы сборщик не фризил программу посередине HTTP-запроса, и так далее. Это все долго и нудно описывается в разных статьях.

В C++ есть подобная история с исключениями. Но здесь требуются еще более нетривиальные познания для полного понимания. Основных проблем две, первая встречается чаще, но вторая более жесткая:

1️⃣ Оверхед по времени исполнения и хуже с компиляторными оптимизациями. Есть супер крутой лонгрид Исключения C++ через призму компиляторных оптимизаций.

Вместо того, чтобы просто взять и вызвать функцию, которая теоретически может выбросить исключение, приходится делать дополнительный блок кода для обработки потенциального исключения, и переводить исполнение туда в случае исключения (лишняя проверка на КАЖДЫЙ вызов).

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

2️⃣ Сложность в разработке, в том числе в обеспечении Strong Exception Guarantee.

Strong Exception Guarantee это гарантия, что если вызвать метод, который вдруг выбросит исключение, то состояние программы будет таким же, как было до вызова метода. Контейнеры по возможности реализуют SEG, и это очень муторно. В моей статье про контейнеры я описывал, когда работает SEG.

void TryAddWidget(std::vector<Widget>& widgets) {
// выполняем некий код...
Widget w;
try {
widgets.push_back(std::move(w));
} catch (...) {
// поймали исключение...
// ожидаем, что `widgets` остался юзабельным
}
}

В Стандарте есть велосипеды специально для SEG: std::move_if_noexcept.

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

Есть понятие гарантии Basic Exception Guarantee, программа продолжает работать, но данные немного поломаны. Например, в примере выше, в зависимости от свойств класса Widget, в векторе widgets могут стереться все элементы после выброса исключения 😐

Объяснение сути BEG на примере саппорта Microsoft Word (😢 - клиент, 🎧 - саппорт):

😢 - Это поддержка MS Word? Я писал книгу, но Word вдруг все удалил.
🎧 - Все нормально, просто мы предоставляем только базовые гарантии при ошибках программы.
😢 - Большое спасибо, удачного дня!

Во многих code style (например от Google) исключения использовать запрещено.

В компиляторах есть флаг -fno-exceptions, которые удаляют весь оверхед, связанный с исключениями, не генерируют лишний код. Исключения до сих пор можно будет бросать, но они просто будут крашить программу, и никак не будут обрабатываться.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11🖕1
#creepy

char, который не char 👎

В файлах разных форматах встречаются file signature - это какая-то последовательность байт, которая помогает подтвердить точный формат файла. Часто это значит, что в начале файла расположены "магические байты" специально для этого формата.

На википедии есть список сигнатур для многих форматов. Часто байты в начале файла человекочитаемы, то есть байты кодируют буквы латиницы - например Rar!, LZIP, OggS.

Посмотрим на пример класса, который принимает четыре байта, а потом проверяет, является ли это сигнатурой RAR-файла:
class BinarySignature {
public:
BinarySignature(int32_t value)
: Value_{value}
{}

int32_t AsInt() {
return Value_;
}

bool IsRar() {
return Value_ == 'Rar!';
}

private:
int32_t Value_;
};

Заметили ли вы что-то страшное? Это сравнение int с многосимвольным char!
Value_ == 'Rar!'

Запись литерала 'X' из одного символа везде поддерживается одинаково и имеет тип char.

Запись литерала 'XXXXX' из нескольких символов имеет тип int, но компилятор вправе не поддерживать такую запись. Также у этой записи implementation-defined числовое значение, то есть также отдано на откуп компилятору.

Большинство компиляторов C++ поддерживают мульти-символьный литерал и переводят его в int как если бы это были последовательные байты.

Ссылка на godbolt.

Вот такой бывает удобный юзкейс, когда надо взаимодействовать с человекочитаемыми сигнатурами в бинарных файлах 😐
Please open Telegram to view this post
VIEW IN TELEGRAM
😱10👍9🤯3🤔2😁1🖕1
old-blog.png
168.5 KB
#offtop

Обновление izaron.github.io 🔺

На этом скриншоте - старая версия сайта сайта izaron.github.io, которая велась 5-6 лет назад (сделал скриншот перед обновлением).

Я решил перевести все посты на английский и обновить сайт. Обнаружил, что за прошедшие 6 лет доступные шаблоны для личного блога стали приближенными к магии

В постах с разметкой markdown есть такие фичи:
- можно писать LaTeX-формулы
- встраивать видео с YouTube
- с полпинка прикрутить систему комментариев и реакций
- есть адаптивная верстка, красивая подсветка кода, темная и светлая тема
- сайт обновляется сразу, как только запушить новый коммит на GitHub
- большие возможности по кастомизации всяких кнопок и внешнего вида

Шаблон Chirpy крутой, мне нравится 😁 Можно скопипастить мой репозиторий блога.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6🤔1🖕1
#creepy

std::move_only_function - самая позорная фича C++ 🤡

В С++23 добавили std::move_only_function. Это тот же std::function, но в нем основное различие - нет copy-конструкторов, то есть с объектом можно сделать только move.

(Недавно был пост про самую простую реализацию std::function).

В чем кринж этой фичи? Это НЕ добавление "нового класса", с разницей как между std::string и std::string_view.

Это просто "улучшение" старого std::function. Если бы std::function можно было нормально менять, то он бы выглядел, как сегодняшний std::move_only_function. Но менять его нельзя, по причинам описанным в посте про ABI.

Почему я думаю, что это улучшение старого класса, а не новый класс:
1️⃣ Copy-конструкторы в std::function и так не нужны совсем. Как минимум это бесполезно, как максимум это создает разные проблемы при ненамеренном копировании функторов.
2️⃣ В новом классе есть фичи наподобии "small string optimization" - Callable-объект могут не пихать в динамическую память, если у них маленький размер. Это очень нужно, большинство Callable имеют малый размер.
3️⃣ Об этом написали сами авторы класса в своем пропозале - что они фиксят разные мелкие баги std::function.

То есть Комитет по C++, не имея возможности и воли решить вопрос со сломом ABI, решает дублировать классы со стремными названиями и делает вид что так и должно быть. Это не нормально и вызывает у всех много вопросов. Еще на юзеров перекладывается обязанность переписать используемый тип, вместо того чтобы просто обновить версию libstdc++.

Для почти всех классов STL есть идеи по улучшению, и если просто делать новые классы, то это будет жесть.

Можно везде использовать std::move_only_function вместо std::function, но лучше бы std::function был переделан в нормальный вид без нового класса.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12🤔4😢41🖕1
#opensource

Обзор на GNOME 🦶

Я сделал хештег #opensource, в котором будут обзоры на opensource проекты с уклоном в C/C++. Иногда интересно поисследовать исходники и даже сделать туда патчи, чтобы узнать много нового.



GNOME это окружение рабочего стола, одно из двух самых популярных наравне с KDE.

Денисы Поповы наделали кучу его форков: BolgenOS, MATE, Cinnamon, Pantheon, Consort, etc.

GNOME или его форк используются по умолчанию в куче дистрибутивов: Ubuntu, Debian, Fedora, Arch, Linux Mate, openSUSE, etc.

Мое знакомство с GNOME началось с того, что его UI мне очень нравился, а UI у KDE - категорически нет. Поэтому я решил помочь GNOME патчами.

6 лет назад патчи отсылались по голубиной электронной почте. За это время, видимо, у последнего мейнтейнера умер его Pentium, который тянул только почтовый клиент, поэтому сейчас завели GitLab.

Так выглядят патчи в типичный проект Gnome - nautilus (файловый менеджер), на примере моих коммитов туда:
1️⃣ Фикс группового переименования директорий
2️⃣ Вроде бы фикс popup-а для двух мониторов
3️⃣ Подтверждение смены имени файла при конфликтах через Enter

Бездна баттхёрта начинается с того, что почти все проекты Gnome написаны на Си. Так выглядит обычная структура типа очереди:
struct NautilusFileQueue
{
GList *head;
GList *tail;
GHashTable *item_to_link_map;
};

GList выглядит так же стремно со всеми вытекающими:
typedef struct _GList GList;
struct _GList
{
gpointer data;
GList *next;
GList *prev;
};
Это вызывает флешбеки к задачам с leetcode, там тоже надо было вручную переворачивать списки.

Программирование на Си занятие специфичное. По заветам дедушки Ленина там бесклассовое общество. Поэтому приходится вызывать длинные функции
<имя-модуля>_<имя-класса>_<имя-метода>(<аргументы>)
Это мешает автокомплиту, который не может найти нужную функцию из миллиона других.

Активно используется уникальная идиома Си - opaque data type, например для hash table.
В этой идиоме пользователь видит просто объявление структуры struct foo; и функции которые первым аргументом берут struct foo*, и на этом всё.

Программировать на Си мне не понравилось, так как все равно нужно сначала думать в терминах ООП (как в C++), а потом переводить мысли в Си как через перевод Гоблина.

Самая страшная вещь это то, что Gnome - радикальные велосипедисты. У них есть такие велосипеды, куда вложено куча усилий, как:
1️⃣ GObject - жуткая имитация ООП "как в C++", лишь бы не писать на C++.

2️⃣ Vala - новый язык программирования для десктопных приложений Gnome. Вяло разрабатывается с 2006. Код на нем транслируется в Си. Зачем он сделан - решительно непонятно. У Gnome (точнее у GTK) есть куча обвязок в другие языки, разрабатывать приложения можно на Python, C++, JavaScript, ...

3️⃣ Builder - новая IDE, также непонятно зачем нужная. Косят под Xcode? Разрабатывать приложения можно из любой IDE, где фичей будет заведомо больше.

Для Gnome я бы отметил, что там по ощущениям вроде как всё плохо с тестами. По истории коммитов в nautilus видно, что там половина коммитов - переводы (в основном на такие важные языки как Friulian, Occitan, Catalan, Faroese...), другая половина - изменение поведения принципиально без автотестов.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13🖕2
#story

Встраивание файлов в исходники 📦

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

⭕️ Финтех: много коэффициентов и числовых констант для performance-critical алгоритмов
⭕️ Геймдев: иконки, текстуры, код шейдеров и скриптов
⭕️ Embedded: часто это единственный вариант, если микросхема не имеет ОС и соответственно файловой системы
⭕️ Бэкенд: файлы настроек (известных в build-time), SSL/TLS-сертификаты

Чаще всего для такой цели используется программа xxd.
Посмотрим на пример: файл template.cpp - это шаблон для генерации кода.

Запустим команду
xxd -i template.cpp template.cpp.data

Получим такой файл template.cpp.data:
unsigned char template_cpp[] = {
/* байты */
}
unsigned int template_cpp_len = /* кол-во байтов */;

Потом этот файл можно подключить и сделать из него строку (надо указать длину, так как байты не нуль-терминированы):
#include "template.cpp.data"
const std::string TEMPLATE{(char*)template_cpp, template_cpp_len};

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

Подобную функциональность несколько лет пытаются внести в C/C++ в виде директивы препроцессора #embed. Пока удалось это сделать для C23 - крутой блог с примерами:
  static const char sound_signature[] = {
#embed <sdk/jump.wav>
};

// verify PCM WAV resource signature
assert(sound_signature[0] == 'R');
assert(sound_signature[1] == 'I');
assert(sound_signature[2] == 'F');
assert(sound_signature[3] == 'F');
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8👏4🔥1😁1🤡1🖕1
#story

Кина не будет: цирк в комитете по C++ 🤹

Как я писал, в C++23 приняли аттрибут [[assume(expr)]]. Просматривая статус поддержки C++23 в Clang, я увидел что этот аттрибут пока не поддержан.

Я отправил патч на его поддержку - https://reviews.llvm.org/D144334

Это заняло мало времени и всего несколько строк в коде (не считая тестов и документации), потому что в Clang уже есть __builtin_assume, который сделан аналогично:
    case attr::Assume: {
llvm::Value *ArgValue = EmitScalarExpr(cast<AssumeAttr>(A)->getCond());
llvm::Function *FnAssume = CGM.getIntrinsic(llvm::Intrinsic::assume);
Builder.CreateCall(FnAssume, ArgValue);
break;
}

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

Часть переписки по патчу:

erichkeane: So one thing to note here: I'm on the fence as to whether we want to implement this feature at all. As was discussed extensively during the EWG meetings on this: multiple implementers are against this attribute for a variety of reasons, and at least 1 other implementer has stated they might 'implementer veto' this. I think there is discussion to be had among the code owners here as to whether we even want this.

Izaron: I don't quite understand how it works. The feature has been approved for C++2b, but it should have not been approved if there were concerns from implementers. <...> Could you please elaborate: if you decide to not implement this feature, you will kind of revoke the proposal or just deliberately do not support a part of C++2b in Clang?

erichkeane: Just deliberately not support a part of C++2b. Implementers have veto'ed features in the past exactly that way.

aaron.ballman: Agreed, (IMO) it should not have been approved given how many implementer concerns were expressed. But what goes into the standard is whatever gains consensus in the committee, so the committee occasionally runs the risk of voting in something that won't be implemented. We try to avoid it whenever possible, but it still happens with some regularity.

Вот так! Деды из комитета могут принимать любые изменения, которые Clang и/или GCC и/или MSVC никогда не реализуют, просто по большинству голосов. Комитет всегда умеет удивить.
🤡17👍3😱3🔥1😁1🖕1
#opensource

Обзор на Lua 👩‍💻

Lua это классический скриптовый язык, широко известный в некоторых кругах. На нем пишутся аддоны к World of Warcraft, Nmap, Nginx, Adobe Lightroom, Neovim, и еще к сотне других проектов. Я решил сделать обзор и собрал всякую редкую информацию.

Этот язык простой как пробка. Основу можно узнать в Learn Lua in 15 minutes.

В языке единственная структура данных это хэш-таблица. Там есть многочисленный синтаксический сахар, то есть эти записи:
foo.bar = 1337
function Lib.sum (x, y) return x + y end
list = {"apple", "orange", "banana"}

... аналогичны этим:
foo["bar"] = 1337
Lib["sum"] = function (x, y) return x + y end
list = {[1] = "apple", [2] = "banana", [3] = "orange"}

... то есть "массив" это тоже хэш-таблица с ключами от 1 до n (нумерация массивов в Lua с единицы)

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

Даже можно реализовать, с позволения сказать, ООП.
Объекту foo (хэш-таблице) можно придать ссылку __index на базовый класс (другую хэш-таблицу).
Если какого-то поля foo.bar (ключа bar в таблице foo) нет, то интерпретатор Lua посмотрит в таблицу foo.__index, а если и там нет, то в foo.__index.__index, и так далее.

В языке есть корутины, closure (как лямбда-функции в C++), рефлексия, и прочие нужные приколы.

В интернете есть многие сотни статей про Lua, даже я написал статью 10 лет назад, но лучше читать книгу от автора Programming in Lua. В книгах обычно самое полное изложение, в то время как статьи в интернете заведомо неполные и обычно пишутся чтобы "показать чето крутое".
В книге есть такая информация, которой больше нигде нет, например:
1️⃣ Разные флаги, например флаг LUA_32BITS скомпилирует интерпретатор "Small Lua" с 32-битными числами
2️⃣ Описание условий tail call optimization у функций
3️⃣ Метки и goto
4️⃣ Особенность сборки мусора

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

За долгое время накопилась куча библиотек и проектов. Есть даже несколько новых интерпретаторов/компиляторов, то есть от авторского Lua там только синтаксис языка. Есть полуживой менеджер пакетов LuaRocks (я туда делал пару коммитов).
Конечно, с Python объем всего добра не сравнится, но по сравнению с другими скриптовыми языками Lua выглядит хорошо.

Интересно, что в языке есть навороченный сборщик мусора. Это реализация обычного mark-and-sweep, с крутыми особенностями:
1️⃣ Раньше сборщик мусора делал stop-the-world (когда посреди исполнения программа останавливается и сборщик собирает весь мусор), а сейчас сборщик инкрементальный.
Каждый раз, когда нужно аллоцировать память в N байт, сборщик мусора заодно делает небольшой объем работы, прямо пропорциональный этому N.
В итоге не происходит никаких "фризов", просто аллокация памяти выглядит чуть замедленной.
2️⃣ На объект (= хэш-таблицу) можно повесить функцию, которая вызовется перед "удалением" этого объекта сборщиком мусора. Прикол в том, что внутри этой функции можно сохранить объект в какую-нибудь переменную, и удаления в итоге не произойдет. Это называется воскрешением объекта ✝️. Такого нет во многих языках.
В книге есть разные примеры использования этой техники.
3️⃣ В таблице можно пометить все ключи и/или значения как "weak". Тогда сборщик мусора не будет считать такие ссылки за настоящие ("strong") и в при удалении объекта удалит протухшую пару ключ-значение из таблицы.

Кстати, у Вани в канале есть сборник постов о GC, можно подписаться и почитать 😐

Интерпретатор Lua работает так - читает исходник, транслирует его в "байткод" и интерпретирует этот байткод (как в Java), это быстрее и удобнее.
Поисследовать этот процесс можно, скомпилировав исходники Lua в debug-режиме и запуская его из-под gdb.

Лексер (перевод кусков кода в "токены") и парсер (перевод "токенов" в байткод) работают одновременно, трансляция происходит в один проход, достаточно смотреть на следующий токен (это LL(1)-парсер).
Это самый простой транслятор, и наверное каждый смог бы реализовать перевод Lua в байткод.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍164🔥2🖕1
#compiler

Теория девиртуализации 😶

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

Однако в некоторых случаях компилятор может "доказать", что он точно "знает" метод, который надо вызвать, несмотря на то, что метод виртуальный 😐

Два простых примера: класс без final (нет оптимизации), класс с final (есть оптимизация - девиртуализация). Девиртуализованный вариант меньше дергает память.
void CallDo(TDerived& obj) {
obj.Do(); // будет ли девиртуализация?
}

Компилятор считает, что можно девиртуализовать вызов в таких случаях:
1️⃣ Метод класса помечен как final.
Смысл в том, что даже в случае работы с объектами TDerived*/TDerived& (которые могут указывать на наследника TDerived) нужный метод будет одним и тем же, как его определил класс TDerived.
struct TDerived : IBase {
void Do() final override; // слово `final` тут
};

2️⃣ Класс является финальным. Смысл в том, что указатель на этот класс не будет указывать на какого-то наследника, который что-то мог бы переопределить, потому что у такого класса просто не может быть наследников.
struct TDerived final : IBase { // слово final тут
void Do() override;
};

Однако есть еще одно условие, когда класс считается финальным - если у него финальный деструктор 😁 Этот прикол я обнаружил в исходнике Clang.
struct TDerived : IBase {
~TDerived() final = default; // слово final тут
void Do() override;
};

3️⃣ Мы работаем с объектом класса, а не с указателем на класс. В этом случае точный класс объекта известен на этапе компиляции.
TDerived derived;
derived.Do(); // это же TDerived, инфа 100%

4️⃣ Объект является prvalue. В C++ есть укуренная классификация объектов, где prvalue (pure value) это грубо говоря выражение которое создает новый объект. Смысл в том, что в этом случае тоже известен точный класс объекта на этапе компиляции.
TDerived MakeDerived(); // просто функция
// ...
MakeDerived().Do(); // здесь будет девиртуализация
TDerived{}.Do(); // здесь тоже девиртуализация

На этом всё! Эта оптимизация логичная и скучная, потому что никаких чудес ожидать не приходится. Смысл в том, чтобы доказать что TDerived/TDerived&/TDerived* указывает именно на объект TDerived, а не на какой-то его потомок.

В реальном мире девиртуализация отрабатывает нечасто, так как надо, чтобы совпали два редких покемона кейса, оба противоречат ООП:
(1) работа с TDerived* вместо IBase*;
(2) Класс TDerived или нужный метод финальный (не помню когда в последний раз писал final).

Если вы хотите почитать про девиртуализацию "с нуля" с картинками, то есть крутой лонгрид.

Девиртуализация в C++ не гарантирована. В большинстве своем правила выше работают, но не всегда. В каких-то случаях оптимизировать вызовы виртуальных методов запрещено.

Например, в Apple macOS👩‍💻 есть Kext (Kernel Extension) - расширения ядра, запускающие то или иное несовместимое с оригинальным маком оборудование. Особенность этих Kext в том, что они могут в рантайме менять vtable, поэтому нельзя делать оптимизации, которые обходят обращение к vtable. В Clang есть флаг -fapple-kext для такой настройки.

А в "обычных" окружениях vtable лежат в секциях наподобии .rodata. Эта секция защищена на уровне операционной системы - программа обычно сразу падает при попытке сделать туда какую-нибудь запись в рантайме.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍21🔥5🖕1