Хабрахабр

Разработка классов-дескрипторов на C++/CLI

Шаблон Basic Dispose в C++/CLI
        1.     Введение
    1. Определение деструктора и финализатора
        1. 1. Использование семантики стека
    2. 2. 1. Управляемые шаблоны
        2. 2. Интеллектуальные указатели
        2. 3. Пример использования
        2. Блокировка финализаторов
    Список литературы

NET Framework — редко используется для разработки больших самостоятельных проектов. C++/CLI — один из языков платформы . NET с родным (неуправляемым) кодом. Его главное назначение — создание сборок для взаимодействия . Обычно такой класс-дескриптор владеет соответствующим родным объектом, то есть он должен его удалить в надлежащий момент. Соответственно, весьма широко используются классы, называемые классами-дескрипторами, управляемые классы, имеющие указатель на родной класс в качестве члена. Реализация этого интерфейса в . Вполне естественно сделать такой класс освобождаемым, то есть реализующим интерфейс System::IDisposable. Замечательной особенностью C++/CLI является то, что компилятор берет на себя практически всю рутинную работу по реализации этого шаблона, тогда как в C# почти все приходится делать руками. NET должна следовать специальному шаблону, называемому Basic Dispose [Cwalina].

Существуют два основных способа реализовать этот шаблон.

1.1. Определение деструктора и финализатора

В этом случае в управляемом классе должен быть определен деструктор и финализатор, все остальное компилятор сделает сам.

public ref class X
// деструктор !X() {/* ... */} // финализатор
// ...
};

В частности компилятор делает следующее:

  1. Для класса X реализует интерфейс System::IDisposable.
  2. В X::Dispose() обеспечивает вызов деструктора, вызов деструктора базового класса (если он есть) и вызов GC::SupressFinalize().
  3. Переопределяет System::Object::Finalize(), где обеспечивает вызов финализатора и финализаторов базовых классов (если они есть).

Наследование от System::IDisposable можно указать явно, а вот самостоятельно определить X::Dispose() нельзя.

1.2. Использование семантики стека

Это означает, что для объявления используется имя типа без крышки ('^'), а инициализация происходит в списке инициализации конструктора, а не с помощью gcnew. Шаблон Basic Dispose также реализуется компилятором, если в классе имеется член освобождаемого типа и он объявлен с использованием семантики стека. Семантика стека описана в [Hogenson].

Приведем пример:

public ref class R : System::IDisposable
{
public: R(/* параметры */); // конструктор
// ...
}; public ref class X
{ R m_R; // а не R^ m_R public: X(/* параметры */) // конструктор : m_R(/* аргументы */) // а не m_R = gcnew R(/* аргументы */) {/* ... */}
// …
};

Компилятор в этом случае делает следующее:

  1. Для класса X реализует интерфейс System::IDisposable.
  2. В X::Dispose() обеспечивает вызов R::Dispose() для m_R.

Как и в предыдущем случае, наследование от System::IDisposable можно указать явно, а самостоятельно определить X::Dispose() нельзя. Финализация определяется соответствующей функциональностью класса R. Естественно, класс может иметь еще другие члены, объявленные с использованием семантики стека, и для них также обеспечивается вызов их Dispose().

Речь идет об управляемых шаблонах (managed templates). И наконец, еще одна замечательная особенность C++/CLI позволяет максимально упростить создание классов-дескрипторов. Инстанцирование таких шаблонов приводит к созданию управляемых классов, которые можно использовать в качестве базовых классов или членов других классов внутри сборки. Это не обобщения (generics), а настоящие шаблоны, как в классическом C++, но шаблоны не родных, а управляемых классов. Управляемые шаблоны описаны в [Hogenson].

2.1. Интеллектуальные указатели

Такие интеллектуальные указатели можно использовать в качестве базовых классов или членов (естественно, с использованием семантики стека) при разработке классов-дескрипторов, которые автоматически становятся освобождаемыми. Управляемые шаблоны позволяют создавать классы типа интеллектуальных указателей, которые содержат указатель на родной объект в качестве члена и обеспечивают его удаление в деструкторе и финализаторе.

Первый шаблон является базовым, второй предназначен для использования в качестве базового класса и третий — в качестве члена класса. Приведем пример таких шаблонов. Класс-удалитель по умолчанию удаляет объект оператором delete. Эти шаблоны имеют шаблонный параметр (родной), предназначенный для удаления объекта.

// родной шаблон, класс-удалитель по умолчанию, T — родной класс
template <typename T>
struct DefDeleter
{ void operator()(T* p) const { delete p; }
}; // управляемые шаблоны,
// интеллектуальные указатели на родной объект // базовый шаблон, T — родной класс, D — класс-удалитель
template <typename T, typename D>
public ref class ImplPtrBase : System::IDisposable
{ T* m_Ptr; void Delete() { if (m_Ptr != nullptr) { D del; del(m_Ptr); m_Ptr = nullptr; } } ~ImplPtrBase() { Delete(); } !ImplPtrBase() { Delete(); } protected: ImplPtrBase(T* p) : m_Ptr(p) {} T* Ptr() { return m_Ptr; }
}; // шаблон для использования в качестве базового класса
template <typename T, typename D = DefDeleter<T>>
public ref class ImplPtr : ImplPtrBase<T, D>
{
protected: ImplPtr(T* p) : ImplPtrBase(p) {} public: property bool IsValid { bool get() { return (ImplPtrBase::Ptr() != nullptr); } }
};
// шаблон для использования в качестве члена класса
template <typename T, typename D = DefDeleter<T>>
public ref class ImplPtrM sealed : ImplPtrBase<T, D>
{
public: ImplPtrM(T* p) : ImplPtrBase(p) {} operator bool() { return ( ImplPtrBase::Ptr() != nullptr); } T* operator->() { return ImplPtrBase::Ptr(); } T* Get() { return ImplPtrBase::Ptr(); }
};

2.2. Пример использования

class N // родной класс
{
public: N(); ~N(); void DoSomething();
// ...
}; using NPtr = ImplPtr<N>; // базовый класс public ref class U : NPtr // управляемый класс-дескриптор
{
public: U() : NPtr(new N()) {} void DoSomething() { if (IsValid) Ptr()->DoSomething(); }
// ...
}; public ref class V // управляемый класс-дескриптор, второй вариант
{ ImplPtrM<N> m_NPtr; // семантика стека
public: V() : m_NPtr(new N()) {} void DoSomething() { if (m_NPtr) m_NPtr->DoSomething(); }
// ...
};

Второй вариант, с использованием ImplPtrM<>, позволяет в одном классе-дескрипторе управлять несколькими родными классами. В этих примерах классы U и V становятся освобождаемыми без всяких дополнительных усилий, их Dispose() обеспечивает вызов оператора delete для указателя на N.

2.3. Блокировка финализаторов

В этом случае через некоторое время сборщик мусора попытается их финализировать, а так как DLL выгружена, то скорее всего произойдет аварийное завершение программы. Если родной класс находится в DLL, которая загружается и выгружается динамически — с использованием LoadLibrary()/FreeLibrary(), — то может возникнуть ситуация, когда после выгрузки DLL остались неосвобожденные объекты, имеющие ссылки на экземпляры этого класса. Этого можно достичь небольшой модификацией базового шаблона ImplPtrBase. (Характерный признак — аварийное завершение через несколько секунд после видимого закрытия приложения.) Поэтому после выгрузки DLL финализаторы должны быть блокированы.

public ref class DllFlag
{
protected: static bool s_Loaded = false; public: static void SetLoaded(bool loaded) { s_Loaded = loaded; }
}; template <typename T, typename D>
public ref class ImplPtrBase : DllFlag, System::IDisposable
{
// ... !ImplPtrBase() { if (s_Loaded) Delete(); }
// ...
};

После загрузки DLL надо вызвать DllFlag::SetLoaded(true), а перед выгрузкой DllFlag::SetLoaded(false).

Абрамс, Бред. [Cwalina]
Цвалина, Кржиштов. NET.: Пер. Инфраструктура программных проектов: соглашения, идиомы и шаблоны для многократно используемых библиотек . — М.: ООО «И.Д. с англ. Вильямс», 2011.

С++/CLI: язык Visual C++ для среды . [Hogenson]
Хогенсон, Гордон. с англ. NET.: Пер. Вильямс», 2007. — М.: ООО «И.Д.

Показать больше

Похожие публикации

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

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

Кнопка «Наверх»