Как обеспечить надлежащее пересечение границ динамической библиотеки, используя средства удаления смарт-указателей

от автора

Многие эксперты С++ агитируют использовать интеллектуальные указатели, утверждая, что из современного С++, явное использование 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 

ссылка на оригинал статьи http://habrahabr.ru/post/182970/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *