Должен ли я использовать целочисленный идентификатор или указатели для моих непрозрачных объектов?
Я пишу слой абстракции поверх некоторого графического API (DirectX9 и DirectX11), и мне хотелось бы узнать ваше мнение.
Традиционно я бы создал базовый класс для каждой концепции, которую я хочу абстрагировать.
Таким образом, в типичной ОО-моде у меня есть, например, класс Shader и 2 подкласса DX9Shader и DX11Shader.
Я бы повторил процесс для текстур и т. Д., И когда мне нужно создать их экземпляр, у меня есть абстрактная фабрика, которая будет возвращать соответствующий подкласс в зависимости от текущего графического API.
После RAII возвращенный указатель будет инкапсулирован в std::shared_ptr.
Пока все хорошо, но в моем случае есть несколько проблем с этим подходом:
- Мне нужно придумать открытый интерфейс, который инкапсулирует функциональность обоих API (и других API в будущем).
- Производный класс хранится в отдельных библиотеках DLL (одна для DX9, другая для DX11 и т. Д.), И наличие для них shared_ptr в клиенте является проклятием: при выходе графические библиотеки выгружаются, а если у клиента все еще есть shared_ptr для один из графических объектов гремел, зависал из-за вызова кода из незагруженной DLL.
Это побудило меня переделать способ, которым я делаю вещи: я думал, что смогу просто возвращать сырые указатели на ресурсы и иметь графический интерфейс API чистым после себя, но все еще остается проблема висячих указателей на стороне клиента и проблемы интерфейса. Я даже рассматривал ручной подсчет ссылок как COM, но думал, что это будет шагом назад (поправьте меня, если я ошибаюсь, если исходить из мира shared_ptr, ручной подсчет ссылок кажется примитивным).
Затем я увидел работу Хумуса, где все его графические классы представлены целочисленными идентификаторами (очень похоже на то, что делает OpenGL). Создание нового объекта возвращает только его целочисленный идентификатор и сохраняет указатель внутри; это все совершенно непрозрачно!
Классы, представляющие абстракцию (например, DX9Shader и т. Д.), Скрыты за API устройства, который является единственным интерфейсом.
Если кто-то хочет установить текстуру, это просто вопрос вызова устройства->SetTexture(ID), а все остальное происходит за кулисами.
Недостатком является то, что скрытая часть API является раздутой, для ее работы требуется много кода, и я не являюсь поклонником класса "сделай все".
Есть идеи / мысли?
3 ответа
Вы говорите, что основная проблема заключается в том, что DLL выгружается, все еще имея указатель на свои внутренние компоненты. Ну... не делай этого. У вас есть экземпляр класса, члены которого реализованы в этой DLL. По сути, ошибка заключается в том, что эта DLL выгружается, пока существуют эти экземпляры классов.
Поэтому вы должны нести ответственность за то, как вы используете эту абстракцию. Точно так же, как вы должны нести ответственность за любой код, который вы загружаете из DLL: вещи, которые приходят из DLL, должны быть очищены перед выгрузкой DLL. Как вы это делаете, зависит от вас. У вас может быть внутренний счетчик ссылок, который увеличивается для каждого объекта, который возвращает DLL, и выгружает DLL только после того, как все объекты, на которые есть ссылки, исчезнут. Или что угодно, правда.
В конце концов, даже если вы используете эти непрозрачные числа или что-то еще, что произойдет, если вы вызовете одну из этих функций API для этого номера, когда DLL выгружается? Упс... Так что это не дает тебе никакой защиты. Вы должны нести ответственность в любом случае.
Недостатки числового метода, о котором вы, возможно, не думаете:
Снижение способности знать, что на самом деле является объектом. Вызовы API могут потерпеть неудачу, потому что вы передали номер, который на самом деле не является объектом. Или хуже, что произойдет, если вы передадите шейдерный объект в функцию, которая принимает текстуру? Может быть, мы говорим о функции, которая принимает шейдер и текстуру, и вы случайно забыли порядок аргументов? Правила C++ не позволяют этому коду компилироваться, даже если это были указатели на объекты. Но с целыми числами? Все хорошо; вы получите только ошибки во время выполнения.
Спектакль. Каждый вызов API должен искать этот номер в хеш-таблице или что-то в этом роде, чтобы получить фактический указатель для работы. Если это хеш-таблица (т. Е. Массив), то она, вероятно, довольно незначительная. Но это все-таки косвенность. А поскольку ваша абстракция кажется очень низкоуровневой, любая потеря производительности на этом уровне действительно может повредить в критических ситуациях.
Отсутствие RAII и других механизмов определения объема. Конечно, вы могли бы написать
shared_ptr
-esque объект, который будет создавать и удалять их. Но вам бы не пришлось этого делать, если бы вы использовали реальный указатель.
Это просто не кажется стоящим.
Это имеет значение? Для пользователя объекта это просто непрозрачный дескриптор. его фактический тип реализации не имеет значения, пока я могу передать дескриптор вашим функциям API и заставить их что-то делать с объектом.
Вы можете легко изменить реализацию этих дескрипторов, поэтому сделайте так, как вам будет проще.
Просто объявите тип дескриптора как typedef для указателя или целого числа и убедитесь, что весь код клиента использует имя typedef, тогда код клиента не зависит от конкретного типа, который вы выбрали для представления своих дескрипторов.
Теперь перейдите к простому решению, и если / когда вы столкнетесь с проблемами, потому что это было слишком просто, измените его.
Что касается вашего р. 2: Клиент всегда выгружается перед библиотеками.
Каждый процесс имеет свое дерево библиотечных зависимостей, с.exe в качестве корневого дерева, пользователем Dll на промежуточных уровнях и системными библиотеками на низком уровне. Процесс загружается с низкого на высокий уровень, корень дерева (exe) загружается последним. Процесс выгружается начиная с корня, низкоуровневые библиотеки выгружаются последними. Это сделано для предотвращения ситуаций, о которых вы говорите.
Конечно, если вы загружаете / выгружаете библиотеки вручную, этот порядок меняется, и вы несете ответственность за поддержание указателей в силе.