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

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

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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
65
0
74 415
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Ответ принят как подходящий

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

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

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

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

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

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

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

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

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

Я предпочитаю все глобальные объекты в одном модуле компиляции ... :-)

paercebal 17.10.2008 13:00

Желательно вообще не нуждаться в глобальных объектах.

Martin York 17.10.2008 13:02

Вы оба правы, но, к сожалению, это было неизвестно поколениям программистов, которые написали тонны библиотек и стороннего кода, который мы должны были использовать ...

Dmitry Khalatov 24.12.2009 11:34

+1 к этому ответу, особенно в части помещения всех глобальных переменных в одну единицу компиляции

Moataz Elmasry 09.07.2012 14:37

@LokiAstari: Я всегда думал, что порядок инициализации глобальных переменных в одном блоке компиляции был в том же порядке, что и определение. Но сегодня меня укусил мой компилятор, который этого не сделал, и я был удивлен этим ТАК ответ, указав сначала на определенную последовательность между статическая инициализация, затем динамическая инициализация. 5 лет спустя, не могли бы вы прокомментировать справедливость этого? (Я знаю, я здесь о многом спрашиваю, но я очень смущен)

Ad N 05.11.2013 23:16

@AdN: Да, есть разница между статической и динамической инициализацией. Статическое происходит раньше, чем динамическое (потому что оно выполняется во время компиляции и запекается в базовые сегменты сборки (блок BSS и т. д.)). Когда люди говорят о порядке инициализации, мы имеем в виду только динамическую часть (часть, которая должна выполнять код во время выполнения для инициализации (C++ 11 constexpt, по сути, является другой постоянной времени компилятора)). Это не должно приводить к изменению каких-либо аргументов по этому поводу или заставлять вас что-то укусить. Не могли бы вы задать вопрос, чтобы мы могли изучить его более подробно.

Martin York 05.11.2013 23:56

@MartinYork Речь идет не о глобальных переменных, а о переменных статический, которые в равной степени могут находиться в пространстве имен или в области видимости класса.

j b 02.04.2019 19:42

@MartinYork Да, я имею в виду ваше упоминание «Обычно это проблема только при инициализации Глобальный, которые ссылаются друг на друга» и «Желательно, чтобы глобалы вообще не требовалось» ... Я считаю, что термин «глобальный» здесь вводит в заблуждение поскольку речь идет о статической инициализации, а это не то же самое, что global, который описывает область видимости объекта. По сути, все глобальные объекты имеют статическую продолжительность хранения, но не все статические объекты имеют глобальную область видимости.

j b 02.04.2019 21:02

@jb Обновлено с пометкой в ​​тексте.

Martin York 02.04.2019 21:09

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

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

Новая ссылка явно указать, которая пока работает.

Doncho Gunchev 08.06.2012 13:34

Я использовал этот атрибут, но gcc выдает предупреждение: запрошенный init_priority зарезервирован для внутреннего использования. Будучи предупреждением, мне все же позволили это сделать. Есть ли другой способ установки приоритета инициализации?

Andrew Falanga 31.05.2013 22:32

@Andrew - Вы должны нет использовать init, fini или init_priority. Вместо этого используйте Атрибут constructor. Также есть атрибут destructor. Возможно, вам придется использовать init и fini на других компиляторах и платформах, но для GCC вы его не используете. Также см. "Уточнение атрибут init_priority" в списке рассылки GCC.

jww 05.08.2015 05:05

@jww constructor и destructor не имеют отношения: они существуют для того, чтобы заставить определенные функции вызываться до и после main() соответственно. Они не имеют ничего общего с порядком инициализации конкретных объектов, для которых init_priority - как раз подходящий инструмент. Предупреждение, которое увидел Эндрю, вероятно, было связано с тем, что он использовал значение, не «ограниченное от 101 до 65535 включительно», как указано в документации для init_priority; Таким образом, можно сделать вывод, что значения за пределами этого диапазона зарезервированы, как ясно сказано в предупреждении. Я не думаю, что ни одна из этих вещей имеет отношение к вопросу.

underscore_d 08.08.2016 00:40

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

underscore_d 11.02.2017 16:13

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

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

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

Mike F 17.10.2008 11:42

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

SmacL 17.10.2008 11:51

@smacl: Конечно, но тогда вы должны обработать и завершить функцию, чтобы освободить данные, и обработать тот факт, что иногда и Init, и Finalize вызываются несколько раз, а иногда и одновременно. Идиома RAII в сочетании с автоматической инициализацией глобальных объектов в DLL весьма приветствуется.

paercebal 17.10.2008 12:58

Существуют отдельные сегменты для статических объектов с постоянной и динамической инициализацией. Первые инициализируются перед вторыми и могут быть встроены в исполняемый файл. Последние не могут, и только если они находятся в одной единице перевода, можно зависеть от их порядка инициализации (== порядка определения). Если они охватывают разные ЕП, полагаться на их порядок «вероятно» не значит быть плохой практикой: это определенно плохо. :П

underscore_d 11.02.2017 16:20

http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 - эта ссылка перемещается. этот один более стабилен, но вам придется поискать его.

edit: osgx предоставил лучший связь.

В веб-архиве есть копия: http://web.archive.org/web/20080512011623/http://www.parashi‌ ft.com/c++-faq-lite/‌ ctors.html # faq-10.12‌; "[10.12] Что за" фиаско с порядком статической инициализации "?" раздел C++ FAQ Lite Маршалла Клайна. Аналогичный раздел есть в isocpp.org/wiki/faq/ctors

osgx 06.07.2016 06:16

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

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

underscore_d 14.08.2016 19:18

Поскольку вы уже знаете, что не следует полагаться на эту информацию без крайней необходимости, вот она. Мое общее наблюдение по различным цепочкам инструментов (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. Будьте осторожны, так как ваш глобальный объект может быть оптимизирован в этом случае, и вы можете использовать флаг - весь архив, чтобы заставить компоновщик не удалять неиспользуемые символы для этого конкретного крошечного архивного файла.

Заключительный совет

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

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

Надежным решением является использование функции получения, которая возвращает ссылку на статическую переменную. Ниже показан простой пример, а сложный вариант - в нашем Промежуточное ПО контроллера SDG.

// 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> будет гарантированно инициализирован до того, как в него будут вставлены какие-либо значения.

Это «одноэлементная» модель функции, которая является хорошим исправлением для запуска, с небольшими накладными расходами во время выполнения. Проблема, о которой следует знать с синглтонами, заключается в выключении: если один синглтон-объект создает другой дочерний синглтон в своих методах выполнения (после своего собственного построения), этот дочерний синглтон будет уничтожен раньше родительского синглетона. Если родительский деструктор вызывает дочерний элемент, он потерпит ужасную неудачу, поскольку дочерний элемент уже разрушен!

Gem Taylor 06.01.2021 15:14

Другие вопросы по теме