Порядок инициализации статических переменных


C++ гарантирует, что переменные в единице компиляции (.cpp file) инициализируются в порядке объявления. Для количества единиц компиляции это правило работает для каждого отдельно (я имею в виду статические переменные вне классов).

но порядок инициализации переменных не определен в разных единицах компиляции.

где я могу увидеть некоторые объяснения об этом заказе для gcc и MSVC (я знаю, что полагаться на это очень плохая идея - это просто понять проблемы, которые мы можем иметь с устаревшим кодом при переходе на новый GCC major и другую ОС)?

6 51

6 ответов:

Как вы говорите, порядок не определен в разных единицах компиляции.

внутри той же единицы компиляции порядок четко определен: тот же порядок, что и определение.

Это потому, что это не решается на уровне языка, но на уровне линкера. Поэтому вам действительно нужно проверить документацию компоновщика. Хотя я очень сомневаюсь, что это поможет каким-либо полезным образом.

для gcc: Проверьте ld

Я обнаружил, что даже изменение порядка связывания файлов объектов может изменить порядок инициализации. Таким образом, вам нужно беспокоиться не только о вашем компоновщике, но и о том, как компоновщик вызывается вашей системой сборки. Даже попытка решить проблему практически не стартер.

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

есть методы, чтобы обойти проблема.

  • ленивая инициализация.
  • Шварц Счетчика
  • Поместите все сложные глобальные переменные в одну и ту же единицу компиляции.

Я ожидаю, что порядок конструктора между модулями в основном является функцией того, в каком порядке вы передаете объекты компоновщику.

однако GCC позволяет вам использовать init_priority чтобы явно указать порядок глобальные конструкторы:

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 для создания своих систем сборки, что я часто делаю, я обнаружил, что порядок входных исходных файлов будет порядком их результирующих объектных файлов, заданных компоновщику. Тем не менее, часто раз ваш application / DLL также связывается в других библиотеках, и в этом случае эти библиотеки будут находиться на линии связи после ввод исходных файлов. Если вы хотите, чтобы один из ваших глобальных объектов был первый для инициализации, то вам повезло, и вы можете поместить исходный файл, содержащий этот объект будет первым в списке исходных файлов. Однако, если вы хотите иметь один последний раз для инициализации (который может эффективно реплицировать DllMain () поведение!) затем вы можете сделать вызов add_library() с этим одним исходным файлом для создания статической библиотеки и добавить полученную статическую библиотеку в качестве самой последней зависимости ссылки в вызове target_link_libraries() для вашего приложения/DLL. Будьте осторожны, что ваш глобальный объект может быть оптимизирован в этом случае, и вы можете использовать --whole-archive флаг, чтобы заставить компоновщик не удалять неиспользуемые символы для этого конкретного крошечного архива файл.

Закрытие Наконечником

чтобы точно знать результирующий порядок инициализации вашего связанного приложения / общей библиотеки, передайте --print-map в LD linker и grep for .init_array (или в GCC до 4.7, grep for .артисты). Каждый глобальный конструктор будет напечатан в том порядке, в котором он будет инициализирован, и помните, что порядок противоположен в GCC до 4.7 (см. пункт (1) выше).

мотивирующим фактором для написания этого ответа что мне нужно было знать эту информацию, у меня не было другого выбора, кроме как полагаться на порядок инициализации, и я нашел только разреженные биты этой информации в других сообщениях SO и интернет-форумах. Большая часть этого была изучена в результате многочисленных экспериментов, и я надеюсь, что это сэкономит некоторым людям время на это!

http://www.parashift.com/c++-часто задаваемые вопросы-лайт/конструкторы.сообщение: вопросы и ответы-10.12 - это звено движется вокруг. это один более стабилен, но вам придется искать его.

edit: osgx поставляется лучше ссылке.

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

Edit: в зависимости от порядка построения статических объектов может быть непереносимым и, вероятно, следует избегать.

Если вы действительно хотите узнать окончательный порядок, я бы рекомендовал вам создать класс, конструктор которого регистрирует текущую метку времени и создает несколько статических экземпляров класса в каждом из ваших файлов cpp, чтобы вы могли узнать окончательный порядок инициализации. Убедитесь, что в конструкторе есть небольшая трудоемкая операция, чтобы вы не получили одинаковую отметку времени для каждого файла.