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

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

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

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

Ответ 1

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

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

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

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

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

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

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

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

  • Примечание 1: глобалы:
    Свободно используется для ссылки на статические переменные продолжительности хранения, которые потенциально инициализируются перед main().
  • Примечание 2: потенциально
    В общем случае мы ожидаем, что статические переменные продолжительности хранения будут инициализированы перед main, но в некоторых ситуациях компилятору разрешается откладывать инициализацию (правила сложные, подробности см. В стандарте).

Ответ 2

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

Однако 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".

Ответ 3

Поскольку вы уже знаете, что не должны полагаться на эту информацию, если это абсолютно необходимо, вот оно. Мое общее наблюдение за различными инструментами (MSVC, gcc/ld, clang/llvm и т.д.) Заключается в том, что порядок, в котором ваши объектные файлы передаются компоновщику, - это порядок, в котором они будут инициализированы.

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

1) версии GCC до 4.7 фактически инициализируются в обратном порядке линии связи. Этот билет в GCC - это когда произошло изменение, и он нарушил множество программ, которые зависели от порядка инициализации (включая мой!).

2) В GCC и Clang использование

3) В Windows есть очень аккуратная и полезная функция входа в виде разделяемой библиотеки (DLL), называемая , которая если определено, будет работать с параметром "fdwReason", равным DLL_PROCESS_ATTACH, сразу после того, как все глобальные данные будут инициализированы и до того, как приложение-потребитель сможет вызвать любые функции в DLL. В некоторых случаях это чрезвычайно полезно, и на других платформах с GCC или Clang с C или С++ абсолютно нет аналогичного поведения. Самое близкое, что вы найдете, это сделать функцию-конструктор с приоритетом (см. Выше пункт (2)), что абсолютно не то же самое и не будет работать для многих случаев использования, для которых работает DllMain().

4) Если вы используете CMake для генерации ваших систем сборки, что я часто делаю, я обнаружил, что порядок исходных исходных файлов будет порядком их результирующих объектных файлов, предоставленных компоновщику. Тем не менее, часто ваше приложение /DLL также связывается в других библиотеках, и в этом случае эти библиотеки будут в линии ссылок после ваших исходных исходных файлов. Если вы хотите, чтобы один из ваших глобальных объектов был самым первым для инициализации, тогда вам повезло, и вы можете поместить исходный файл, содержащий этот объект, первым в списке исходных файлов. Однако, если вы хотите, чтобы один из них был последним для инициализации (который может эффективно воспроизводить поведение DllMain()!), Вы можете сделать вызов add_library() с одним исходным файлом для создания статической библиотеки и добавить результирующая статическая библиотека в качестве самой последней зависимости от ссылки в вашем запросе target_link_libraries() для вашего приложения /DLL. Будьте осторожны, чтобы ваш глобальный объект мог быть оптимизирован в этом случае, и вы можете использовать флаг , чтобы заставить компоновщик не удалять неиспользуемые символы для этого конкретного крошечного архивного файла.

Закрывающий совет

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

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

Ответ 5

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

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

Ответ 6

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