Инициализация статических полей класса. Ч2



Как думаете, может ли быть такое, что статическое поле класса инициализируется после завершения вызова конструктора класса? То есть объект уже создался, а статическое поле его класса еще не инициализированно? Подумайте пару секунд над этим вопросом самостоятельно.



После вчерашнего поста вам уже немного легче должны были даться рассуждения. Загвоздка в том, что статическое объекты инициализируются в порядке определения и не важно, какого класса эти объекты.



Посмотрим на пример.



Есть у нас класс, который сохраняет все свои инстансы по ключу в статическую мапу и все созданные инстансы доступны только через эту мапу.



class InitializationTest {

public:

static std::map<std::string, std::unique_ptr<InitializationTest>> map;

static bool Create(std::string ID) {

map.insert({ID, std::move(std::unique_ptr<InitializationTest>{new InitializationTest})});

return true;

}

private:

Test() = default;

};



static bool creation_result = InitializationTest::Create("qwe");

// Somehow handle result and process object

std::map<std::string, std::unique_ptr<InitializationTest>> InitializationTest::map{};



int main() {}





Чтобы такое провернуть, создаем в классе статическую мапу, статический метод Create, который предоставляет доступ к созданию объектов и объявляем конструктор класса приватным, чтобы никто снаружи не смог втихаря создать объект. Таким образом, доступ к объектам есть только через метод Create и статическую мапу.



Не имея представления о нюансах инициализации, взбрело нам в голову создать и использовать объект перед определением мапы.



Запуская все это дело, получим сегфолт. И да, да, вы все правильно поняли. Все из-за порядка инициализации.



Так как линкеру пофиг на тип статических объектов, он спокойно может поставить инициализацию статического поля класса после завершения работы конструктора объекта в глобальном неймспейсе. Вот и получается конфуз: объект надо создавать, а используемое поле не инициализировано. От того и падаем.



Здесь ситуация игрушечная и довольно простая, потому что все находится в одной единице трансляции. Пример такой, потому что внутри одной единицы компиляции порядок инициализации детерминирован, тут легче показать причину и следствие. Но когда мы выходим за ее пределы и пытаемся создать объект InitializationTest в другой единице трансляции в глобальном скоупе, то поведение кода начинает зависеть от линкера. Порядок создания объектов между юнитами компиляции не определен и тут все будет, как решит компановщик. Можно конечно почитать документацию и действовать в соответствии с ней. Но этот код будет непереносим, а также не защитит вас от возможных изменений в поведении линкера.



Будьте аккуратны с инициализацией статических объектов и в принципе поменьше их используйте.



Be careful. Stay cool.