Библиотека SFML инициализирует аудиоустройство при создании первого объекта типа AudioResource и деинициализирует его при уничтожении последнего.
Я пытаюсь упростить этот код, так как считаю, что текущее решение с использованием std::[shared/weak]_ptr является излишним.
Я предложил альтернативу, используя глобальную переменную static. Вот упрощенная версия (настоящая защищена std::mutex):
// AudioResource.hpp --------------------------------
struct AudioResource
{
AudioResource();
~AudioResource();
// ...
};
// AudioResource.cpp --------------------------------
#include "AudioResource.hpp"
static int deviceRC = 0;
AudioResource() { if (deviceRC++ == 0) { /* init device */ }; }
~AudioResource() { if (--deviceRC == 0) { /* deinit device */ }; }
// ...
Я думаю, что приведенный выше код в порядке и не подвержен фиаско инициализации статического порядка, поскольку глобальный deviceRCstatic определен и доступен только в одном TU.
Насколько я понимаю, пока нет зависимостей между несколькими глобальными static переменными из разных TU, риска нет.
Я прав, или возможно, что приведенный выше код приводит к неопределенному поведению из-за того, что каким-то образом осуществляется доступ к deviceRC до его инициализации?
Существует ли сценарий, при котором создание объектов AudioResource из нескольких TU или использование SFML в качестве общей библиотеки может вызвать проблемы?
Безопасен ли код, даже если deviceRC не инициализируется константой? Например.,
// SomeOtherTU.cpp --------------------------------
static AudioResource audioResource;
// Could `deviceRC` be accessed while uninitialized here?
Другие сопровождающие SFML предложили использовать здесь переменную области функции static:
// AudioResource.cpp --------------------------------
#include "AudioResource.hpp"
static int& deviceRC()
{
static int result = 0;
return result;
}
AudioResource() { if (deviceRC()++ == 0) { /* init device */ }; }
~AudioResource() { if (--deviceRC() == 0) { /* deinit device */ }; }
// ...
Является ли подход с использованием переменной функциональной области static более безопасным и/или необходимым по сравнению с подходом, использующим переменную static файловой области?
@JaMiT: в чем будет проблема, если deviceRC будет динамически инициализироваться во время выполнения? Единственное место, где к нему можно получить доступ, — это AudioResource.cpp TU, поэтому я ожидаю, что инициализация произойдет. Вы думаете о случае, когда глобальный объект AudioResource создается в другом TU?
Конечно, порядок инициализации вступит в силу только в том случае, если другая единица перевода создаст глобальную переменную типа AudioResource или глобальную переменную, инициализатор которой создает экземпляр AudioResource.
«настоящая переменная защищена std::mutex» Обратите внимание, что хотя static int deviceRC = 0 инициализируется статически, глобальная переменная типа std::mutex фактически инициализируется динамически. Таким образом, вы можете попытаться заблокировать мьютекс, который еще не был инициализирован.
@IgorTandetnik: один из вариантов, что все может пойти не так, — это если TU создает глобальный объект типа AudioResource, и инициализация этого глобального объекта выполняется до инициализации deviceRC (или мьютекса в реальном случае). Верно? По сути: (1) у некоторых TU есть объект static AudioResource audioResource; (2) тело AudioResource::AudioResource() выполняется до инициализации deviceRC; (3) бум – неопределенное поведение?
Да. Ну, этого не может произойти с deviceRC (он инициализируется значением 0, как только загружается исполняемый образ, до запуска любого кода); но это может произойти с мьютексом, к которому пытается получить доступ конструктор AudioResource. Обратите внимание, что создание локальной статики во вспомогательной функции не поможет - в этом случае построение будет в порядке, но разрушение станет проблемой; мьютекс будет уничтожен до того, как запустится деструктор ~AudioResource этой глобальной переменной (поскольку объекты уничтожаются в порядке, противоположном построению).
@IgorTandetnik: спасибо. Я не осознавал, что деструктор также вызовет проблему. Полагаю, что единственный способ гарантировать правильность — это придерживаться исходной реализации std::shared_ptr + std::weak_ptr? Или есть способ избежать проблемы уничтожения мьютекса?
@VittorioRomeo Я просто хотел прокомментировать именно этот сценарий. Если у другого TU есть глобальный AudioResource, вы не можете знать, был ли deviceRC уже инициализирован.
Что касается статической переменной fuction-scope, здесь она безопаснее и/или необходима? Ответ: да. Если бы оно не было статическим, оно не сохранялось бы между вызовами функций, и вы всегда получали бы новое целое число. У вас также будет висячая ссылка, поскольку вы ссылаетесь на объект, который уничтожается в конце функции.
@NathanOliver: Я понимаю, что означает создание переменной области функции static, я спрашивал, будет ли подход с использованием функции более безопасным по сравнению с подходом, использующим область действия файла static. Немного уточнил вопрос.
По сути, вам нужно сделать ваши потокобезопасные элементы защищенными от блокировок, используя только атомы (поскольку вы не можете гарантировать, что что-то более сложное, чем атомарное, будет создано к тому времени, когда вам это понадобится). Я не рассматривал текущий подход, но подозреваю, что он сводится к безблокировочной и потокобезопасной инициализации.
@IgorTandetnik: возможна ли преднамеренная утечка мьютекса? Например. static auto& getMutex() { static auto* p = new std::mutex{}; return *p; }
Я использовал подход преднамеренной утечки для объекта (например, std::mutex), у которого нет ресурсов для очистки, кроме занимаемой памяти. static auto& getMutex() { static auto* intentional_leak_p = new std::mutex{}; return *intentional_leak_p; } Память будет освобождена ОС после завершения работы программы.
Да, это должно сработать. Если вы можете смириться с накладными расходами на блокировку (которые зависят от того, как часто AudioResource объекты создаются и уничтожаются).
@IgorTandetnik: В итоге я нашел это решение, чтобы предотвратить разрушение мьютекса и при этом избежать динамического выделения. Если вы оставите свои комментарии в качестве ответа, я буду рад их принять! Спасибо.
Важно не то, находится ли переменная в одном TU, а то, со сколькими двоичными файлами связана эта TU. Его можно определить в нескольких динамических библиотеках одновременно.
@IgorTandetnik std::mutex имеет конструктор по умолчанию constexpr. Он будет иметь постоянную инициализацию. Беспокойство вызывает только разрушение, в зависимости от того, что именно означает eel.is/c++draft/basic.start.term#3.sentence-3.
@user17732522: user17732522: разве constinit не требуется для гарантии постоянной инициализации? К сожалению, мне нужно ориентироваться на C++17.
@VittorioRomeo Нет, правила, определяющие, будет ли гарантированно происходить постоянная инициализация, не зависят от constinit. consinit вызывает диагностику только в том случае, если в результате будет выполнена динамическая инициализация.
@user17732522: user17732522: разве C++11 не гарантирует инициализацию области функций static для предотвращения гонки?
@VittorioRomeo Извините, я пропустил static.
@IgorTandetnik: ты уверен насчет проблемы с приказом об уничтожении, о которой ты упоминал ранее? Я как бы принял это как должное, но после некоторого обсуждения (см. github.com/SFML/SFML/pull/3089#discussion_r1646608714) кажется, что здесь это не проблема. Мы что-то упускаем?
Да, я ошибался. Демо. Каким-то образом два объекта создаются и уничтожаются в том порядке, который получается.
Итак, вы почему-то считаете shared_ptr «излишним», и ваше решение — сделать собственный счетчик ссылок и спровоцировать огромную дискуссию о технических деталях?
@ChristianStieber да





Если deviceRc инициализирован статически, код безопасен, потому что:
Если deviceRc не инициализирован статически, то у вас проблема. audioResource действительно может быть инициализирована раньше deviceRc, поскольку две переменные определены в разных TU. Когда audioResource инициализируется, возможно, что он увидит значение deviceRc, которое было до динамической инициализации, то есть ноль. Если deviceRc имеет динамическую инициализацию, то, по-видимому, инициализация более сложна, чем просто установка нуля. Таким образом, предположительно, программа некорректна, если значение deviceRc считывается до завершения динамической инициализации.
Стратегия с локальной статической функцией гарантирует, что result инициализируется до доступа к ней, а result уничтожается после любой статической переменной, которая могла получить доступ к result во время ее собственной конструкции. Эти гарантии обычно гарантируют, что в вашем коде не будет каких-либо ошибок в порядке инициализации или уничтожения, хотя вы можете их создать, если очень постараетесь.
Обратите внимание, что этот код оказался небезопасным из-за этого конкретного случая использования: github.com/SFML/SFML/issues/3146
Ваше понимание правильное. Время жизни глобальной переменной имеет статическую продолжительность хранения. Статическая функция локальной области действия не требуется, поскольку переменная не доступна другим единицам перевода (фиаско статической инициализации). Если используется многопоточность, вы можете использовать
static std::atomic<int>. Дэйв С. говорит: «Привет!»