Static initialization order fiasco



Добрались мы наконец-то до этого мерзопакостного явления. По сути, про статики мы говорили ради нескольких тем и эта одна из них.



В чем суть. Как вы уже поняли, что с порядком инициализации у статиков все очень плохо. Но внутри одной единицы трансляции он хотя бы определен и предсказуем! С божественными способностями предсказания, конечно. Ну или с томиком стандарта и нашими статьями под рукой. Но он этот порядок хотя бы какой-то есть. Один раз нормально сделай и можно надеяться на обратную совместимость языка, что все будет работать как надо.



Но вот между разными юнитами трансляции порядок вообще не определен.



Static initialization order fiasco отсылается к неопределенности в порядке, в котором инициализируются объекты со статической продолжительностью хранения в разных единицах трансляции. Если мы пытаемся создать объект в одном юните, который полагается на существующий объект в другом, то мы можем знатно утяжелить штаны, если получится так, что объект еще не существует. То есть он просто zero-инициализирован. В общем случае, поведение в такой программе неопределено.



Простейший воспроизводимый пример:



// source.cpp

int quad(int n) {

return n * n;

}



auto staticA = quad(5);



// main.cpp

#include <iostream>



extern int staticA;

auto staticB = staticA;



int main() {

std::cout << "staticB: " << staticB << std::endl;

}





Если скомпилировать это дело как: g++ main.cpp source.cpp -std=c++17, то результат будет такой:



staticB: 0





А если файлы передать в другом порядке: g++ source.cpp main.cpp -std=c++17, то такой:



staticB: 25





Очевидно, что результат зависит от того, в каком порядке линкер увидит единицы трансляции. И это зашквар!



Например, GCC версии до 4.7 инициализировал единицы трансляции в обратном порядке их появления в строке компиляции. И в один момент это поведение поменялось на обратное, что с хренам поломало кучу проектов, которые были завязаны на инициализации именно в таком порядке.



Кстати, линкер инициализирирует единицы трансляции не в рандомном порядке. Есть разные способы: в алфавитном порядке, в передаваемом ему на вход порядке и так далее. То есть система есть, но у каждого она своя.



Это можно видеть даже на нашем примере:



В первом случае staticB равен нулю, потому что main.cpp стоит первым в строке компиляции и линкер инициализирует глобальные переменные этой единицы трансляции первыми. А так как на этот момент staticA не получила своего окончательного значения, а была лишь zero-инициализирована, то staticB инициализируется нулем.



Во втором случае source.cpp инициализируется первым и теперь все в правильном порядке. staticB получает свое значение от уже инициализированного staticA.



На эти порядки ни в коем случае нельзя надеяться! Опять же пример с гцц говорит нам, что неследование стандарту чревато надеванием кастрюли на голову и ударами по ней поварешкой. Но для понимания процессов, это примерно так происходит.



Define the order of your life. Stay cool.



#cppcore #NONSTANDARD