Многие эксперты С++ агитируют использовать интеллектуальные указатели, утверждая, что из современного С++, явное использование new
должно вообще исчезнуть (ну, по крайней мере, когда в С++14 пофиксят отсутствие std::make_unique
). Все динамические выделения памяти должны быть инкапсулированы или в стандартную библиотеку, или контейнеры типа std::vector
, или интеллектуальные указатели.
Смарт-указатели стандартной библиотеки могут быть настроены так, чтобы они сами занимались освобождением занимаемой ими памяти. Эта возможность и заложена в основу ответа на вопрос, поставленного в заголовке статьи.
Объект является пересекающим границу динамической библиотеки, если он инициализируется в одном блоке, а используется в другом. Это происходит, когда, например, в dll инициализируется объект и возвращается указатель на него.
Предположим, одна библиотека (или исполнимый модуль) связывается с другой библиотекой, используя фабрику для динамической инициализации объекта и получения указателя на него. Блок, который использует этот указатель, может удалить указатель для освобождения области памяти, на которую он указывает. Если библиотека, которая выделяет память и блок, работающий с указателем, используют различные версии динамического выделения памяти ОС (CRT в Windows), то возникнет ошибка. Пример этой проблемы (в случае с Windows):
Как правило (до появления С++11), разработчики библиотеки должны были разрабатывать функции освобождения памяти для объектов, которые были выделены в пределах этой библиотеки, для того, чтобы избежать этой проблемы. Это имело побочный эффект: интерфейсы таких библиотек становились более «тяжелыми», к тому же от них теперь требовалось «ноу-хау» для корректного выделения и освобождения памяти для объектов библиотеки. В идеале, пользователя не должна была беспокоить сама схема выделения/освобождения, он просто должен был вызывать механизм библиотеки (например, фабрику) для выделения памяти, не заботясь об ее последующем освобождении.
Переходим к коддингу
У нас будет два проекта: первый будет состоять просто из файла main, использующий фабрику библиотеки для инициализации объектов из нее, второй будет иллюстрировать проблемную ситуацию и ее решение.
Проблемная область — синглтон фабрика (ProblematicFactory), которая инициализирует объект и возвращает указатель на него. Решение — другой сингтон, который, после инициализации объекта, возвращает указатель std::unique_ptr
, имеющий свое собственное средство удаления, производящее освобождение памяти в DLL.
Если запустить программу в режиме отладки с определением USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION
, то можно увидеть, что отладчик обнаруживает повреждение «кучи».
Файл main
// main.cpp
#include <ProblematicFactory.h>
#include <SafeFactory.h>
// измените undef на define, чтобы увидеть assert'ы о повреждении кучи
#undef USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION
int main()
{
#ifdef USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION
{
// это выделение делается в DLL
auto wMyObject = ProblematicFactory::getInstance().create();
// это освобождение происходит в текущем блоке
delete wMyObject;
// если DLL и этот блок будут линковаться с одной и той же CLR DLL,
// удаление произойдет нормально, иначе - это вызовет повреждение кучи
}
#endif
{
auto wMyObject = SafeFactory::getInstance().create();
// когда программа перейдет в следующий блок, wMyObject будет
// автоматически удален, используя пользовательское средство удаления,
// реализованное в MyClass.h (см. далее), функция освобождения
// в библиотеке вызываться не будет
}
{
std::shared_ptr< MyClass > wMyObject = SafeFactory::getInstance().create();
}
return 0;
}
Проблемная фабрика
Это — типичная реализация фабрики, которая возвращает указатель на объект, который может быть создан библиотекой.
// ProblematicFactory.h
#pragma once
#include "DllSwitch.h"
#include "MyClass.h"
class LIBRARYFACTORY_API ProblematicFactory
{
public:
static ProblematicFactory & getInstance()
{
static ProblematicFactory wProblematicFactory;
return wProblematicFactory;
}
MyClass * create() const
{
return new MyClass;
}
private:
ProblematicFactory() {};
};
Безопасная фабрика
Синтаксически, использование этой фабрики точно такое же, как и проблемной (см. main), но здесь указатель инкапсулируется в std::unique_ptr
, а не std::shared_ptr
.
// SaveFactory.h
#pragma once
#include "DllSwitch.h"
#include "MyClass.h"
#include <memory>
class LIBRARYFACTORY_API SafeFactory
{
public:
static SafeFactory & getInstance();
// Здесь std::unique_ptr не будет пересекать границу библиотеки,
// т.к. эта функция не будет входить в DLL. Создание произойдет
// на стороне клиента, поэтому используется std::unique_ptr клиента.
// Так же, нет необходимости задавать пользовательские средства
// удаления, поскольку для класса MyClass существует std::default_delete
inline std::unique_ptr< MyClass > create() const
{
return std::unique_ptr< MyClass >(doCreate());
}
private:
SafeFactory();
MyClass * doCreate() const;
};
Пересечение границы
// MyClass.h
#pragma once
#include "DllSwitch.h"
#include <memory>
class LIBRARYFACTORY_API MyClass
{
};
namespace std
{
template<>
class LIBRARYFACTORY_API default_delete< MyClass >
{
public:
void operator()(MyClass *iToDelete)
{
delete iToDelete;
}
};
}
Во всех вышеприведенных файлах подключается заголовочный файл DllSwitch.h
, определяющая LIBRARYFACTORY_API
, его содержание:
// DllSwitch.h
#pragma once
#ifdef LIBRARYFACTORY_EXPORTS
#define LIBRARYFACTORY_API __declspec(dllexport)
#else
#define LIBRARYFACTORY_API __declspec(dllimport)
#endif
Автор: Renzo