вторник, июля 12, 2011

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

Порядок инициализации глобальных статических переменных из разных единиц трансляции не определен. По-английский это называется красивым термином static initialization order fiasco. Это означает, что если у вас в разных файлах определены глобальные статические переменные x и y, то лучше, чтобы они не зависели одна от другой, а то тут могут быть неприятные сюрпризы.

Это было скучная теория. А вот грустные примеры из практики. Пишем много-много кода с большим количеством нетривиальных глобальных статических переменных. Они как-то самопроизвольно начинают друг от друга зависеть. До поры, до времени компиляция идет таким путем, что они нормально инициализируются. Но в один прекрасный день, после добавления какого-то куска кода, они вдруг начинают инициализироваться в немного другом порядке. И это означает, что надо переосмыслить весь проект целиком. Прямо сейчас.

Updated 12.07.2011:
В комментариях посоветовали два способа продлить агонию, т.е. заставить компилятор инициализировать глобальные переменные в определенном порядке.
Для GCC это init_priority
Для VC++ #pragma init_seg

P.S. Для примеров из жизни я обычно не указываю когда и кем были допущены какие-то ошибки. Это информация для того, чтобы научиться, а не для того, чтобы поглумиться. Мы все ошибаемся.

22 коммент.:

Анонимный комментирует...

Если не боитесь темной стороны силы, в gcc есть способ определить порядок (priority) инициализации. http://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Attributes.html

но минус сто в карму сразу.

Анонимный комментирует...

Алёна работает в Miscrosoft. Я думаю, что они не используют gcc в своих проектах.
Про -100 согласен на все 100.

Shere комментирует...

ого, это ж подводные грабли как они есть! примерно как в моей епархии все, что связано с альфа-каналами

Qehgt комментирует...

Глобальные переменные в коде - признак низкого качества кода.

Исправить/зарефакторить - религия не позволяет?

Анонимный комментирует...

И именно поэтому паттерн "singleton" получил такую популярность. ;)

Анонимный комментирует...

Помимо синглтона, есть ещё паттерн «счётчик Шварца».

Unknown комментирует...

По моему единственный случай когда оправдано использование статических переменных, - это реализация thread local storage.

Остальное все должно начинаться со стека int main(); и неторопливо уходить в heap.

Анонимный комментирует...

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

Alex Ott комментирует...

не только инициализироваться, но и удаляться в произвольном порядке при завершении программы - тоже может боком вылезти... мы наткнулись на такое на gcc 4.6

Анонимный комментирует...

Есть довольно простой обходной путь, который не требует глобального рефакторинга: http://altdevblogaday.com/2011/07/07/writing-a-pre-main-function-forcing-global-initialization-order-within-vc/

Unknown комментирует...

А я вообще не понимаю, что можно писать, чтобы сесть в такую лужу?

Maksym Motornyy комментирует...

Алена, а внедрение зависимостей не стали использовать?

Alena комментирует...

Qehgt
Глобальные переменные в коде - признак низкого качества кода.
Исправить/зарефакторить - религия не позволяет?

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

Великобратов Максим
А я вообще не понимаю, что можно писать, чтобы сесть в такую лужу?

Это только в форумах все специалисты без страха и упрека. В реальных проектах люди ошибаются.

Максим Моторный
Алена, а внедрение зависимостей не стали использовать?

Никогда не использовала, нет.

bialix комментирует...

скажу банальную вещь, но при разработке встраиваемых систем я не доверяю даже тому, что по стандарту глобальные переменные инициализируются нулями, просто потому что я могу подменить стартап-код. В результате все глобальные переменные я инициализирую (даже нулями) при помощи *_init() функций из каждого модуля. Может ли этот подход решить конкретную описанную проблему я на 100% не уверен, но по крайней мере это может оказаться минимальным workaround.

Александр комментирует...

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

Классы биндятся в скрипт. Каждый класс знает о том, какие его члены нужно связать. Само связывание происходит в свободной функции а ее вызов идет с помощью примерно такого кода:
const bool is_registered = BindFunction()

Если вызывать эти функции для каждого класса вручную в стороннем модуле, то получается не так "красиво" чтоли...

Также, отдельно есть следующая проблема:
- если поместить этот код в *.h файлы, то он вызовется многократно, что является излишним
- если же его поместить *.cpp файлы, то нет гарантии что он будет вызван вообще и я не знаю как сделать этот вызов гарантированным

Заранее спасибо и извините что в формате вопроса, хотя думаю информация по теме и может быть кому-то интересна.

Анонимный комментирует...

такие переменные надо возвращать через функцию, и все будет пучком

bar& foo() { static bar& v; return v; }

Анонимный комментирует...

Для этого счетчик шварца вполне подойдет.

Анонимный комментирует...

2 Александр

Альтернатива, например, все же поместить инициализацию в *.h файлы, но применить счетчики Шварца, уже упомянутые выше, чтобы не вызывать ее многократно.

Анонимный комментирует...

>Они как-то самопроизвольно начинают друг от друга зависеть.

ошибки проектирования
фсё ф топку

Анонимный комментирует...

> фсё ф топку

А проекту уже 3 года (это хорошо, если 3). Переписать с нуля - много денег.

Анонимный комментирует...

Никто тут про это пока не говорил, но для полей класса общих для всех членов (static) возникает та же проблема. А без них бывает трудно обойтись.

Unknown комментирует...

Все знают, как писать хороший код, но никто не пишет:) Но никто не знает, что делать с существующим кодом :) Ну вот есть оно, соглашусь, что кривое, но его 300+ мегов(реальный проект) и месяц времени... будем ругать архитектуру и переписывать с нуля? Успехов...

А теперь по делу.

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

Затем найдите в проекте объектные файлы с нетривиальными глобальными объектами. Под линуксами в таких объектниках присутствует функция `__GLOBAL_sub_ что-то там`... Остальные объектники на результат не влияют.

Утилитой `nm` можно получить список импорта экспорта каждого объектника. Глобальные переменные лежет в секции `T`. Соответственно импорт обозначен как `t`

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

Делаем топологическую сортировку графа. Инвертируем массив если нужно.

Линкуем бинарник в нужном порядке.

Для красоты для все эти действия реализуем в скриптах и заносим всю процедуру в мейкфайл.


Это работает уже 5 лет в проектах по 4М строк кода.

Успехов.