mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
754e71af72
Co-authored-by: Dmitry Volodin <mr.molkree@gmail.com>
123 lines
6.5 KiB
Markdown
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;
|
|
}
|
|
```
|
|
|
|
Но тогда вы соглашаетесь на утечку памяти. Конечно, никакой утечки на самом деле не будет — статический объект умрет при завершении работы программы. И память все равно будет освобождена.
|
|
Однако утилиты, используемые для обнаружения утечек, обязательно укажут на ваш статический объект в куче. И вам придется их отфильтровывать, чтобы не мешали искать настоящие утечки.
|