mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
200 lines
13 KiB
Markdown
200 lines
13 KiB
Markdown
# Static initialization order fiasco
|
|
|
|
Проблемы с использованием объектов до окончания их полной инициализации наигрывается во многих языках программирования. Сомнительный дизайн с разрывом объявления, конструирования и инициализации можно воплотить в жизнь чуть ли ни где угодно. Но обычно для этого все-таки надо
|
|
приложить некоторые усилия. А в C/C++ можно вляпаться незаметно, случайно и очень долго об этого не подозревать.
|
|
|
|
В C/C++ мы можем разделять код программы по разным, независимым единицам трансляции
|
|
(в разные .c/.cpp файлы). Они могут компилироваться параллельно.
|
|
Скорость сборки повышается. И все было бы хорошо.
|
|
|
|
Но только в одном «модуле» появляется глобальная переменная, используемая в другом модуле, начинаются проблемы. И проблемы не только от того, что глобальные переменные в принципе признак не самого удачного дизайна. Проблема в том, что связи между модулями нет (заголовочные файлы ничего не связывают). И после объединения модулей код с инициализацией глобальной переменной может оказаться ПОСЛЕ кода с использованием.
|
|
|
|
Стандарты C и С++ гарантируют, что глобальные переменные будут сконструированы в порядке их объявления внутри единицы трансляции. А между единицами трансляции — неопределен. И вместе с порядком неопределено и поведение программы.
|
|
|
|
```C++
|
|
// module.h
|
|
extern int global_value;
|
|
|
|
// module.cpp
|
|
#include "module.h"
|
|
|
|
int init_func() {
|
|
return 5 * 5;
|
|
}
|
|
int global_value = init_func();
|
|
|
|
// main.cpp
|
|
#include "module.h"
|
|
|
|
#include <iostream>
|
|
|
|
static int use_global = global_value * 5;
|
|
|
|
int main() {
|
|
std::cout << use_global;
|
|
}
|
|
```
|
|
Результат будет [зависеть](https://godbolt.org/z/zvffd1) от того, в каком порядке будут обработаны `main.cpp` и `module.cpp`.
|
|
|
|
До C++11 в следующем простеньком примере было неопределенное поведение. Как раз из-за возможности неправильного порядка инициализации статических объектов.
|
|
|
|
```C++
|
|
#include <iostream>
|
|
|
|
struct Init {
|
|
Init() {
|
|
std::cout << "Init!\n";
|
|
}
|
|
} init; // до C++11 не было гарантии,
|
|
// что std::cout сконструирован к этому моменту
|
|
|
|
int main() {
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
Бороться с неправильным порядком инициализации можно, например, организовав доступ к
|
|
глобальной переменной через вызов функции.
|
|
|
|
```C++
|
|
// module.h
|
|
int global_variable();
|
|
|
|
// module.cpp
|
|
int global_variable() {
|
|
static int glob_var = init_func();
|
|
return glob_var;
|
|
}
|
|
```
|
|
|
|
В таком случае при первом же доступе инициализация гарантировано произойдет.
|
|
|
|
-----------------
|
|
|
|
Помимо неопределенного поведения из-за неправильного порядка инициализации, наиграть можно
|
|
проблемы и с порядком деинициализации!
|
|
|
|
Стандарт C++ гарантирует, что деструкторы объектов всегда вызываются в порядке, обратном порядку завершения работы конструкторов.
|
|
|
|
```C++
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
const std::string& static_name() {
|
|
static const std::string name = "Hello! Hello! long long string!";
|
|
return name;
|
|
}
|
|
|
|
struct TestStatic {
|
|
TestStatic() {
|
|
std::cout << "ctor: " << "ok" << "\n";
|
|
}
|
|
~TestStatic() {
|
|
std::cout << "dctor: " << static_name() << "\n";
|
|
}
|
|
} test;
|
|
|
|
|
|
int main() {
|
|
std::cout << static_name() << "\n";
|
|
}
|
|
```
|
|
|
|
Сначала отрабатывает конструктор `TestStatic`. Затем `main`, вызвав `static_name`, конструирует строку.
|
|
По завершении программы СНАЧАЛА уничтожается строка, а затем деструктор `TestStatic`
|
|
обращается к [уже уничтоженной строке](https://godbolt.org/z/b5Krcz).
|
|
|
|
Чтобы избежать подобного, можно либо в конструкторе `TestStatic` вызвать функцию
|
|
`static_name` — тогда конструктор строки завершится до завершения конструктора `TestStatic` и
|
|
порядок уничтожения объектов будет другим.
|
|
|
|
Либо (и так иногда делают) в принципе предотвратить уничтожение статической строки: [создать ее в куче](https://godbolt.org/z/j7aY7q).
|
|
|
|
```C++
|
|
const std::string& static_name() {
|
|
static const std::string* name
|
|
= new std::string("Hello! Hello! long long string!");
|
|
return *name;
|
|
}
|
|
```
|
|
|
|
Но тогда вы соглашаетесь на утечку памяти. Конечно, никакой утечки на самом деле не будет — статический объект умрет при завершении работы программы. И память все равно будет освобождена.
|
|
Однако утилиты, используемые для обнаружения утечек, обязательно укажут на ваш статический объект в куче. И вам придется их отфильтровывать, чтобы не мешали искать настоящие утечки.
|
|
|
|
|
|
## Initialization order fiasco и неиспользуемые заголовки
|
|
|
|
Для ускорения процесса сборки хорошей практикой в C++ является уменьшение количества подключаемых заголовков. Подключать стараются только то, что действительно используется. Если размер структур в конкретном файле не важен (например, используются только ссылки и указатели), то можно подключить отдельный маленький заголовок с предобъявлениями (например, `iosfwd` вместо `iostream`). Есть линтеры ([cpplint](https://github.com/cpplint/cpplint), например), которые могут подсказывать, какие заголовочные файлы у вас совсем не используются. Все неиспользуемое — в мусор!
|
|
|
|
Если следовать подобным советам и подходам, исходники после препроцессинга получаются меньше. Меньше неиспользуемых символов. Повторяющихся символов тоже меньше — меньше работы для линкера. Красота. Все только выигрывают... Вроде бы.
|
|
|
|
На самом деле есть подводные камни, об которые легко разбиться. И они связаны с порядком инициализации статических объектов (спасибо [Egor Suvorov](https://github.com/yeputons) за концепцию примера).
|
|
|
|
Допустим, вы пишете библиотеку логгирования. Ее интерфейс скромен
|
|
|
|
```C++
|
|
// logger.h
|
|
|
|
#include <string_view>
|
|
void log(std::string_view message);
|
|
```
|
|
|
|
В интерфейсе используется только минимально необходимый заголовок.
|
|
|
|
В первой реализации вы решили логгировать в `stdout` с помощью стандартной библиотеки потоков ввода/вывода.
|
|
|
|
```C++
|
|
// logger.cpp
|
|
#include "logger.h"
|
|
|
|
#include <iostream>
|
|
|
|
void log(std::string_view message) {
|
|
std::cout << "INFO: " << message << std::endl;
|
|
}
|
|
```
|
|
|
|
Вы отладили свой логгер и выдали его чуть более широкому кругу пользователей.
|
|
И один из них, любящий, например, создавать плагины с саморегистрирующимися фабриками, не ожидая никакого подвоха, воспользовался вашим логгером в своем любимом деле:
|
|
|
|
|
|
```C++
|
|
// main.cpp
|
|
#include "logger.h"
|
|
|
|
struct StaticFactory {
|
|
StaticFactory() {
|
|
log("factory created");
|
|
}
|
|
} factory;
|
|
|
|
|
|
int main() {
|
|
log("start main");
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
Он, располагая компилятором `gcc version 10.3.0 (Ubuntu 10.3.0-1ubuntu1)`, собрал приложение командой:
|
|
```
|
|
g++ -std=c++17 -o test main.cpp logger.cpp
|
|
```
|
|
|
|
Запустил, и оно сразу же упало с ошибкой сегментации.
|
|
Тогда озадаченный пользователь отключил вашу библиотеку, вернулся к использованию проверенного временем `iostream` и написал вам баг-репорт, в котором почему-то привел только исходник. А команду компиляции не приложил.
|
|
|
|
Вы пытаетесь воспроизвести падение на том же сборочном тулчейне и используете строку компиляции
|
|
```
|
|
g++ -std=c++17 -o test2 logger.cpp main.cpp
|
|
```
|
|
Запускаете. И, о чудо!, ничего не падает. Закрываем баг-репорт?
|
|
|
|
----------
|
|
|
|
В этом примере очень злобная ошибка с нарушением порядка инициализации статических объектов. C++11 гарантирует, что объекты `std::cin`, `std::cout`, `std::cerr` и их «широкие» аналоги будут инициализированы ДО любого статического объекта, объявленного в вашем файле, ТОЛЬКО ЕСЛИ заголовок `<iostream>` подключен ПЕРЕД объявлением ваших объектов. Достигается это в глубинах `<iostream>` созданием статического объекта `std::ios_base::Init`. До C++11 гарантий не было. Темные времена.
|
|
|
|
В своей заботе о минимизации зависимостей и размере обработанных препроцессором исходников (или просто последовав совету линтера), вы не включили `iostream` в интерфейсный заголовок библиотеки, но использовали его в реализации. Пользователь, не знающий об этом, получает проблемы. Не самое удачное решение.
|
|
|
|
Объекты стандартных потоков не единственная возможность для подобных ошибок. Любая библиотека, использующая глобальные статические объекты, не позаботившаяся об их инициализации ДО любых действий пользователя — потенциальный источник проблем.
|
|
Если вы автор библиотеки, внимательнее относитесь к проектированию ее интерфейса. В C++ он не ограничивается только лишь сигнатурами функций и описанием классов.
|