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

Разработка классов-дескрипторов на 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 нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

У нас DevOps. Давайте уволим всех тестировщиков

Можно ли автоматизировать всё, что угодно? Потом всех тестировщиков уволим, конечно. Зачем они теперь нужны, «ручного» тестирования не осталось. Правильно ведь? Здесь будут конкретные цифры и чисто практические выводы, как так получается, что у хороших специалистов всегда есть работа. Это ...

Современный PHP — прекрасен и продуктивен

Почти 8 месяцев тому назад я пересел с проектов python/java на проект на php (мне предложили условия от которых было бы глупо отказываться), и я внезапно не ощутил боли и отчаяния, о которых проповедуют бывшие разработчики на ПХП. И вот ...