Как правильно выполнить любой код до и после 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Пример на godbolt
do main
В этом коде можно делать что угодно (читать файлы, записывать в поток и т.д.), окружение там полностью готово. Я часто использую лямбды:
Пример на 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://t.me/cxx95/76