Порядок инициализации статических переменных
C++ гарантирует, что переменные в модуле компиляции (файл.cpp) инициализируются в порядке объявления. Для числа единиц компиляции это правило работает для каждого отдельно (я имею в виду статические переменные вне классов).
Но порядок инициализации переменных не определен в разных единицах компиляции.
Где можно найти некоторые пояснения об этом заказе для gcc и MSVC (я знаю, что полагаться на это очень плохая идея - это просто понять проблемы, которые могут возникнуть у нас с унаследованным кодом при переходе на новую основную GCC и другие ОС)?
6 ответов
Как вы говорите, порядок не определен в разных единицах компиляции.
В одной и той же единице компиляции порядок четко определен: тот же порядок, что и в определении.
Это потому, что это решается не на уровне языка, а на уровне компоновщика. Так что вам действительно нужно проверить документацию компоновщика. Хотя я действительно сомневаюсь, что это поможет любым полезным способом.
Для gcc: Проверьте ld
Я обнаружил, что даже изменение порядка связывания файлов объектов может изменить порядок инициализации. Так что вам нужно беспокоиться не только о своем компоновщике, но и о том, как компоновщик вызывается вашей системой сборки. Даже попытаться решить проблему это практически не стартер.
Это, как правило, только проблема при инициализации глобальных, которые ссылаются друг на друга во время их собственной инициализации (поэтому затрагивает только объекты с конструкторами).
Есть методы, чтобы обойти проблему.
- Ленивая инициализация.
- Счетчик Шварца
- Поместите все сложные глобальные переменные в один и тот же модуль компиляции.
Я ожидаю, что порядок конструктора между модулями в основном зависит от того, в каком порядке вы передаете объекты компоновщику.
Тем не менее, GCC позволяет вам использовать init_priority
чтобы явно указать порядок для глобальных ctors:
class Thingy
{
public:
Thingy(char*p) {printf(p);}
};
Thingy a("A");
Thingy b("B");
Thingy c("C");
выводит "ABC", как и следовало ожидать, но
Thingy a __attribute__((init_priority(300))) ("A");
Thingy b __attribute__((init_priority(200))) ("B");
Thingy c __attribute__((init_priority(400))) ("C");
выводит "BAC".
Поскольку вы уже знаете, что вам не следует полагаться на эту информацию, если она не является абсолютно необходимой, вот она. Мое общее наблюдение по различным цепочкам инструментов (MSVC, gcc/ld, clang/llvm и т. Д.) Заключается в том, что порядок, в котором ваши объектные файлы передаются компоновщику, является порядком, в котором они будут инициализированы.
Есть исключения из этого, и я не претендую на них всех, но вот те, с которыми я столкнулся:
1) Версии GCC до 4.7 фактически инициализируются в обратном порядке линии связи. Этот тикет в GCC произошел, когда произошло изменение, и он сломал множество программ, которые зависели от порядка инициализации (включая мою!).
2) В GCC и Clang использование приоритета функции конструктора может изменить порядок инициализации. Обратите внимание, что это применимо только к функциям, которые объявлены как "конструкторы" (т.е. они должны запускаться так же, как и конструктор глобального объекта). Я попытался использовать приоритеты, подобные этому, и обнаружил, что даже с наивысшим приоритетом для функции конструктора все конструкторы без приоритета (например, обычные глобальные объекты, функции конструктора без приоритета) будут инициализироваться первыми. Другими словами, приоритет относится только к другим функциям с приоритетами, но настоящие первоклассные граждане - это те, у кого нет приоритета. Что еще хуже, это правило фактически противоположно в GCC до 4.7 из-за пункта (1) выше.
3) В Windows есть очень аккуратная и полезная функция точки входа разделяемой библиотеки (DLL) под названием DllMain(), которая, если она определена, будет запускаться с параметром "fdwReason", равным DLL_PROCESS_ATTACH, сразу после инициализации всех глобальных данных и до того, как приложение-потребитель сможет вызвать какие-либо функции в DLL. В некоторых случаях это чрезвычайно полезно, и на других платформах с GCC или Clang с C или C++ такого поведения нет. Самое близкое, что вы найдете, - это создание функции конструктора с приоритетом (см. Пункт (2) выше), что абсолютно не то же самое и не будет работать во многих случаях использования, для которых работает DllMain().
4) Если вы используете CMake для генерации своих систем сборки, что я часто делаю, я обнаружил, что порядок входных исходных файлов будет соответствовать порядку их результирующих объектных файлов, передаваемых компоновщику. Однако часто ваше приложение /DLL также связывается с другими библиотеками, и в этом случае эти библиотеки будут в строке ссылкипосле ваших исходных файлов. Если вы хотите, чтобы один из ваших глобальных объектов был инициализирован первым, то вам повезло, и вы можете поставить исходный файл, содержащий этот объект, первым в списке исходных файлов. Однако, если вы хотите, чтобы один инициализировался последним (который может эффективно реплицировать поведение DllMain()!), Вы можете вызвать add_library() с этим одним исходным файлом для создания статической библиотеки и добавить результирующая статическая библиотека как самая последняя зависимость ссылки в вашем вызове target_link_libraries() для вашего приложения /DLL. Будьте осторожны, в этом случае ваш глобальный объект может быть оптимизирован, и вы можете использовать флаг --whole-archive, чтобы компоновщик не удалял неиспользуемые символы для этого конкретного крошечного архивного файла.
Закрывающий совет
Чтобы полностью узнать результирующий порядок инициализации вашего связанного приложения / shared-библиотеки, передайте --print-map в ld linker и grep для.init_array (или в GCC до 4.7, grep для.ctors). Каждый глобальный конструктор будет напечатан в том порядке, в котором он будет инициализирован, и помните, что в GCC этот порядок противоположен до 4.7 (см. Пункт (1) выше).
Мотивирующим фактором для написания этого ответа является то, что мне нужно было знать эту информацию, у меня не было другого выбора, кроме как полагаться на порядок инициализации, и я обнаружил только редкие фрагменты этой информации в других сообщениях SO и интернет-форумах. Большая часть этого была изучена через много экспериментов, и я надеюсь, что это экономит время некоторых людей на это!
http://www.parashift.com/c++-faq-lite/ctors.html - эта ссылка перемещается. этот вариант более стабилен, но вам придется его осмотреть.
редактировать: osgx предоставил лучшую ссылку.
Надежным решением является использование функции-получателя, которая возвращает ссылку на статическую переменную. Ниже показан простой пример, сложный вариант в нашем промежуточном программном обеспечении SDG Controller.
// Foo.h
class Foo {
public:
Foo() {}
static bool insertIntoBar(int number);
private:
static std::vector<int>& getBar();
};
// Foo.cpp
std::vector<int>& Foo::getBar() {
static std::vector<int> bar;
return bar;
}
bool Foo::insertIntoBar(int number) {
getBar().push_back(number);
return true;
}
// A.h
class A {
public:
A() {}
private:
static bool a1;
};
// A.cpp
bool A::a1 = Foo::insertIntoBar(22);
Инициализация будет с единственной статической переменной-членом bool A::a1
. Тогда это вызоветFoo::insertIntoBar(22)
. Тогда это вызоветFoo::getBar()
в котором инициализация статического std::vector<int>
переменная возникнет перед возвратом ссылки на инициализированный объект.
Если static std::vector<int> bar
были помещены непосредственно как переменная-член Foo class
, может быть, в зависимости от порядка именования исходных файлов, что bar
будет инициализирован после insertIntoBar()
были вызваны, что привело к сбою программы.
Если несколько статических переменных-членов вызовут insertIntoBar()
во время их инициализации порядок не будет зависеть от имен исходных файлов, т.е. случайный, но std::vector<int>
будет гарантированно инициализирован до того, как в него будут вставлены какие-либо значения.
В дополнение к комментариям Мартина, исходящим из фона Си, я всегда думаю о статических переменных как о части исполняемого файла программы, включающей и выделяющей пространство в сегменте данных. Таким образом, статические переменные можно рассматривать как инициализируемые при загрузке программы перед выполнением любого кода. Точный порядок, в котором это происходит, может быть определен путем просмотра сегмента данных, выводимого компоновщиком в файл карты, но для большинства целей и задач инициализация является одновременной.
Изменить: В зависимости от порядка построения статических объектов может быть непереносимым и, вероятно, следует избегать.
Если вы действительно хотите знать окончательный порядок, я бы порекомендовал вам создать класс, конструктор которого регистрирует текущую временную метку, и создать несколько статических экземпляров класса в каждом из ваших файлов cpp, чтобы вы могли знать конечный порядок инициализации. Обязательно добавьте в конструктор немного трудоемкой операции, чтобы вы не получали одинаковую отметку времени для каждого файла.