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

6.5 KiB

Static initialization order fiasco

Проблемы с использованием объектов до окончания их полной инициализации наигрывается во многих языках программирования. Сомнительный дизайн с разрывом объявления, конструирования и инициализации можно воплотить в жизнь чуть ли ни где угодно. Но обычно для этого все-таки надо приложить некоторые усилия. А в C/C++ можно вляпатся незаметно, случайно и очень долго об этого не подозревать.

В C/C++ мы можем разделять код программы по разным, независимым единицам трансляции (в разные .c/.cpp файлы). Они могут компилироваться параллельно. Скорость сборки повышается. И все было бы хорошо.

Но только в одном «модуле» появляется глобальная переменная, используемая в другом модуле, начинаются проблемы. И проблемы не только от того, что глобальные переменные в принципе признак не самого удачного дизайна. Проблема в том, что связи между модулями нет (заголовочные файлы ничего не связывают). И после объединения модулей код с инициализацией глобальной переменной может оказаться ПОСЛЕ кода с использованием.

Стандарты 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;
}

Результат будет зависеть от того, в каком порядке будут обработаны main.cpp и module.cpp.

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

#include <iostream>

struct Init {
    Init() {
        std::cout << "Init!\n"; 
    }
} init; // до C++11 не было гарантии,
        // что std::cout сконструирован к этому моменту

int main() {
    return 0;
}

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

// module.h
int global_variable();

// module.cpp
int global_variable() {
    static int glob_var = init_func();
    return glob_var;
}

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


Помимо неопределенного поведения из-за неправильного порядка инициализации, наиграть можно проблемы и с порядком деинициализации!

Стандарт 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 обращается к уже уничтоженной строке.

Чтобы избежать подобного, можно либо в конструкторе TestStatic вызвать функцию static_name — тогда конструктор строки завершится до завершения конструктора TestStatic и порядок уничтожения объктов будет другим.

Либо (и так иногда делают) в принципе предотвратить уничтожение статической строки: создать ее в куче.

const std::string& static_name() {
    static const std::string* name 
        = new std::string("Hello! Hello! long long string!");        
    return *name;
}

Но тогда вы соглашаетесь на утечку памяти. Конечно, никакой утечки на самом деле не будет — статический объект умрет при завершении работы программы. И память все равно будет освобождена. Однако утилиты, используемые для обнаружения утечек, обязательно укажут на ваш статический объект в куче. И вам придется их отфильтровывать, чтобы не мешали искать настоящие утечки.