Напоминание по C++: статические переменные внутри функции инициализируются лишь один раз, даже если кажется иначе

Когда вы объявляете статическую переменную внутри функции, она инициализируется только один раз — конкретно в тот первый раз, когда выполнение доходит до объявления этой переменной. Если выполнение снова доходит до этой переменной в будущем, инициализация не происходит. Переменная просто сохраняет своё предыдущее значение.

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

Оригинальный код использовал следующий заголовок из внешней библиотеки виджетов:

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

Код приложения вызывал это так:

Загвоздка в том, что конструктор WidgetOptions принимает ссылку на GadgetOptions и сохраняет эту ссылку. Позже, когда виджет спрашивает контроллер о флагах, контроллер будет искать ответ в структуре BasicWidgetInfo, но этот BasicWidgetInfo уже был разрушен, когда CreateCustomWidget вернул управление, поэтому возвращается мусор (или может даже случиться крах).

Чтобы решить эту проблему, сделали info статическим. Статические объекты продолжают существовать даже после возврата из функции.

Теперь программа не падает. Ура!

Однако есть одна загвоздка: если два Gadget оба пытаются создать виджет, у всех них будут одинаковые опции, как у первого, потому что локальные статические переменные функции разделяются между всеми экземплярами класса и инициализируются только в первый раз, когда выполнение доходит до переменной. Какие бы флаги ни были переданы при первом вызове, они «запечатываются» в info, и не важно, какие флаги вы передаёте при последующих вызовах — info уже инициализирован; она не инициализируется снова.

Если вы хотите, чтобы инициализация происходила каждый раз, то вы должны модифицировать её каждый раз.

На этот раз мы установили значения в шаге, отдельном от конструирования, что означает, что этот код выполняется при каждом вызове, и info обновляется самыми недавними флагами.

Разумеется, это всё ещё проблема, если два Gadget создают Widgets с пересекающимся временем жизни, потому что два BasicWidgetController разделяют одно и то же info. При втором вызове CreateWidget его обновления info тайно изменят значения, используемые первым.

Плюс, конечно, если CreateWidget вызывается двумя потоками одновременно, возникает гонка данных при записи в переменную info, и тогда результаты будут непредсказуемы.

Корневая проблема в том, что BasicWidgetController хочет продлить время жизни своего info, но ссылка не даёт способа это сделать, поэтому он вынужден полагаться на добрую волю извне.

Одна идея — поместить info в другое место, так чтобы его время жизни можно было продлить каким-то другим способом. Возможно, поместить его в Gadget:

Теперь ваша задача — убедиться, что m_info не будет разрушен раньше, чем последний shared_ptr на BasicWidgetController. Это непросто, поскольку вы на самом деле не знаете, когда последний shared_ptr на BasicWidgetController будет разрушен, хотя у вас могут быть некоторые эвристики, учитывая, что его время жизни, вероятно, связано с виджетом.

Есть ли способ «подслушать» момент разрушения последнего shared_ptr?

Да, и на самом деле мы уже использовали эту возможность, не осознавая этого.

Вы можете использовать алиасный shared_ptr, который указывает на BasicWidgetController, но чей контроль жизни распространяется как на BasicWidgetController, так и на связанный с ним BasicWidgetInfo.

Мы используем алиасный конструктор с указателем на контроллер, но указываем ему контролировать время жизни BasicWidgetControllerWithInfo.

Разумеется, всё это — проблема приложения, созданная самим приложением. Им следовало бы просто исправить BasicWidgetController, чтобы он копировал BasicWidgetInfo вместо того, чтобы принимать ссылку.

Raymond участвует в эволюции Windows более 30 лет. В 2003 году он запустил веб-сайт, известный как The Old New Thing, который стал гораздо популярнее, чем он мог себе представить, что до сих пор внушает ему беспокойство. Сайт породил книгу, случайно тоже названную The Old New Thing (Addison Wesley, 2007). Время от времени он появляется в аккаунте Windows Dev Docs в Twitter, рассказывая истории, которые не несут полезной информации.

Обсуждение закрыто. Войдите, чтобы редактировать/удалять существующие комментарии.

Стоило бы заставить конструктор нового BasicWidgetController принимать BasicWidgetInfo по значению, чтобы явно показать, что он хранит собственную копию? Возможно, придётся немного изменить вызов (вставить локальную переменную inline).

Как ведёт себя семантика «выполнить один раз» при выгрузке библиотек?

Стандарт C++ не содержит понятия динамических библиотек. Инструменты компиляции и платформы, с которыми я знаком, привязывают время жизни глобальных/статических объектов к времени жизни динамической библиотеки, так что при её выгрузке вызываются их деструкторы, а при повторной загрузке — как будто библиотека загружается впервые. На более экзотических платформах или в иных тулчейнах поведение может отличаться.

Спасибо. YMMV всегда лучший ответ. 🙂

«Загвоздка в том, что конструктор WidgetOptions принимает ссылку на GadgetOptions и сохраняет эту ссылку.» — это наследие раннего черновика?

Вы, вероятно, имели в виду: «Загвоздка в том, что конструктор BasicWidgetController принимает ссылку на BasicWidgetInfo и сохраняет эту ссылку.»

Если кто-то пытается хот-патчить уже хот-патченную функцию, как избежать конфликтов?

Почему компилятор иногда использует ud2, а иногда int 3 для кода, который не должен выполняться?