Использование shared_ptr в dll-интерфейсах
У меня есть абстрактный класс в моей DLL.
class IBase {
protected:
virtual ~IBase() = 0;
public:
virtual void f() = 0;
};
я хочу получить IBase
в моем exe-файле, который загружает dll. Первый способ - создать следующую функцию
IBase * CreateInterface();
и добавить виртуальную функцию Release()
в IBase
,
Второй способ заключается в создании другой функции
boost::shared_ptr<IBase> CreateInterface();
и нет Release()
функция нужна.
Вопросы.
1) Верно ли, что освобождение деструктора и памяти вызывается в dll (а не в exe-файле) во втором случае?
2) Хорошо ли работает второй случай, если exe-файл и dll были скомпилированы с разными компиляторами (или разными настройками).
4 ответа
Ответ на ваш первый вопрос: виртуальный деструктор в вашей dll называется - информация о его местонахождении встроена в ваш объект (в vtable). В случае освобождения памяти это зависит от того, насколько дисциплинированны пользователи вашего IBase
являются. Если они знают, что они должны позвонить Release()
и учтите, что исключение может обойти поток управления в неожиданном направлении, будет использовано правильное.
Но если CreateInterface()
возвращается shared_ptr<IBase>
он может связать правильную функцию освобождения прямо с этим умным указателем. Ваша библиотека может выглядеть так:
Destroy(IBase* p) {
... // whatever is needed to delete your object in the right way
}
boost::shared_ptr<IBase> CreateInterface() {
IBase *p = new MyConcreteBase(...);
...
return shared_ptr<IBase>(p, Destroy); // bind Destroy() to the shared_ptr
} // which is called instead of a plain
// delete
Таким образом, каждый пользователь вашей DLL легко защищен от утечек ресурсов. Они никогда не должны беспокоиться о звонке Release()
или обратите внимание на исключения, удивительно обходящие их поток управления.
Чтобы ответить на ваш второй вопрос: Недостатки этого подхода четко сформулированы другими ответами: ваша аудитория должна использовать тот же компилятор, компоновщик, настройки, библиотеки, что и вы. И если их может быть довольно много, это может стать серьезным недостатком для вашей библиотеки. Вы должны выбрать: безопасность против большей аудитории
Но есть возможная лазейка: используйте shared_ptr<IBase>
в вашем приложении, т.е.
{
shared_ptr<IBase> p(CreateInterface(), DestroyFromLibrary);
...
func();
...
}
Таким образом, никакой специфичный для реализации объект не передается через границу DLL. Тем не менее ваш указатель надежно спрятан за shared_ptr
, кто звонит DestroyFromLibrary
в нужное время, даже если func()
Выкидываю исключение или нет.
Я бы посоветовал не использовать shared_ptr
в интерфейсе. Даже использование C++ вообще в интерфейсе DLL (в отличие от процедур "extern C" only) проблематично, поскольку искажение имен не позволит вам использовать DLL с другим компилятором. С помощью shared_ptr
Это особенно проблематично, потому что, как вы уже определили, нет гарантии, что клиент DLL будет использовать ту же реализацию shared_ptr
как звонящий. (Это потому что shared_ptr
является классом шаблона, и реализация полностью содержится в заголовочном файле.)
Чтобы ответить на ваши конкретные вопросы:
Я не совсем уверен, что вы спрашиваете здесь... Я предполагаю, что ваша DLL будет содержать реализации классов, полученных из
IBase
, Код для их деструкторов (так же как и остальной код), в обоих ваших случаях, будет содержаться в DLL. Однако, если клиент инициирует уничтожение объекта (путем вызоваdelete
в первом случае или позволяя последнему экземпляруshared_ptr
выйти из области видимости во втором случае), тогда деструктор будет вызван из клиентского кода.Имена имен обычно не позволяют использовать вашу DLL с другим компилятором в любом случае... но реализация
shared_ptr
может измениться даже в новой версии того же компилятора, и это может привести к неприятностям. Я бы уклонился от использования второго варианта.
- С помощью
shared_ptr
удостоверится, что функция освобождения ресурса будет вызвана в DLL. - Посмотрите на ответы на этот вопрос.
Выходом из этой проблемы является создание чистого интерфейса C и тонкой полностью встроенной оболочки C++.
На ваш первый вопрос: я делаю обоснованное предположение и не говорю из опыта, но мне кажется, что во втором случае освобождение памяти будет называться "в.exe". Есть две вещи, которые происходят, когда вы звоните delete object;
Во-первых, вызывается деструктор, а во-вторых, освобождается память для объекта. Первая часть, вызов деструктора, определенно будет работать, как вы ожидаете, вызывая правильные деструкторы в вашей dll. Однако, так как shared_ptr является шаблоном класса, его деструктор генерируется в вашем.exe, и поэтому он будет вызывать оператор delete() в вашем exe, а не тот, что в.dll. Если они были связаны с разными версиями времени выполнения (или даже статически связаны с одной и той же версией времени исполнения), это должно привести к ужасному неопределенному поведению (в этой части я не совсем уверен, но это кажется логичным), Есть простой способ проверить, является ли то, что я сказал, правдой - переопределить глобальный оператор delete в вашем exe, но не в dll, поставить точку останова в нем и посмотреть, что вызывается во втором случае (я бы сделал это сам, но я так много времени для отдыха, к сожалению).
Обратите внимание, что для первого случая существует то же самое (вы, кажется, это понимаете, но на всякий случай). Если вы делаете это в exe:
IBase *p = CreateInterface();
delete p;
тогда вы находитесь в том же самом операторе вызова, новом в dll, и вызывающем операторе удаления в exe. Вам понадобится либо соответствующая функция DeleteInterface(IBase *p) в вашей dll, либо метод Release() в IBase (который не должен быть виртуальным, просто не встроенным) для единственной цели вызова нужной памяти функция освобождения.