Files
ubbook/runtime/static_initialization_order_fiasco.md
T
2021-03-05 18:18:29 +03:00

123 lines
6.5 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;
}
```
Но тогда вы соглашаетесь на утечку памяти. Конечно, никакой утечки на самом деле не будет — статический объект умрет при завершении работы программы. И память все равно будет освобождена.
Однако утилиты, используемые для обнаружения утечек, обязательно укажут на ваш статический объект в куче. И вам придется их отфильтровывать, чтобы не мешали искать настоящие утечки.