В C++/CLI достаточно часто используются так называемые классы-дескрипторы — управляемые классы, имеющие указатель на родной класс в качестве члена. В статье рассматривается удобная и компактная схема управления временем жизни соответствующего родного объекта, основанная на использовании управляемых шаблонов. Рассмотрены сложные случаи финализации.
Оглавление
Введение
1. Шаблон Basic Dispose в C++/CLI
1.1. Определение деструктора и финализатора
1.2. Использование семантики стека
2. Управляемые шаблоны
2.1. Интеллектуальные указатели
2.2. Пример использования
2.3. Более сложные варианты финализации
2.3.1. Блокировка финализаторов
2.3.2. Использование SafeHandle
Список литературы
Введение
C++/CLI — один из языков платформы .NET Framework — редко используется для разработки больших самостоятельных проектов. Его главное назначение — создание сборок для взаимодействия .NET с родным (неуправляемым) кодом. Соответственно, весьма широко используются классы, называемые классами-дескрипторами, управляемые классы, имеющие указатель на родной класс в качестве члена. Обычно такой класс-дескриптор владеет соответствующим родным объектом, то есть он должен его удалить в надлежащий момент. Вполне естественно сделать такой класс освобождаемым, то есть реализующим интерфейс System::IDisposable
. Реализация этого интерфейса в .NET должна следовать специальному шаблону, называемому Basic Dispose [Cwalina]. Замечательной особенностью C++/CLI является то, что компилятор берет на себя практически всю рутинную работу по реализации этого шаблона, тогда как в C# почти все приходится делать руками.
1. Шаблон Basic Dispose в C++/CLI
Существуют два основных способа реализовать этот шаблон.
1.1. Определение деструктора и финализатора
В этом случае в управляемом классе должен быть определен деструктор и финализатор, все остальное компилятор сделает сам.
public ref class X
{
~X() {/* ... */} // деструктор
!X() {/* ... */} // финализатор
// ...
};
В частности компилятор делает следующее:
- Для класса
X
реализует интерфейсSystem::IDisposable
. - В
X::Dispose()
обеспечивает вызов деструктора, вызов деструктора базового класса (если он есть) и вызовGC::SupressFinalize()
. - Переопределяет
System::Object::Finalize()
, где обеспечивает вызов финализатора и финализаторов базовых классов (если они есть).
Наследование от System::IDisposable
можно указать явно, а вот самостоятельно определить X::Dispose()
нельзя.
1.2. Использование семантики стека
Шаблон Basic Dispose также реализуется компилятором, если в классе имеется член освобождаемого типа и он объявлен с использованием семантики стека. Это означает, что для объявления используется имя типа без крышки ('^
'), а инициализация происходит в списке инициализации конструктора, а не с помощью gcnew
. Семантика стека описана в [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(/* аргументы */)
{/* ... */}
// ...
};
Компилятор в этом случае делает следующее:
- Для класса
X
реализует интерфейсSystem::IDisposable
. - В
X::Dispose()
обеспечивает вызовR::Dispose()
дляm_R
.
Финализация определяется соответствующей функциональностью класса R
. Как и в предыдущем случае, наследование от System::IDisposable
можно указать явно, а самостоятельно определить X::Dispose()
нельзя. Естественно, класс может иметь еще другие члены, объявленные с использованием семантики стека, и для них также обеспечивается вызов их Dispose()
.
2. Управляемые шаблоны
И наконец, еще одна замечательная особенность C++/CLI позволяет максимально упростить создание классов-дескрипторов. Речь идет об управляемых шаблонах (managed templates). Это не обобщения (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(); }
// ...
};
В этих примерах классы U
и V
становятся освобождаемыми без всяких дополнительных усилий, их Dispose()
обеспечивает вызов оператора delete
для указателя на N
. Второй вариант, с использованием ImplPtrM<>
, позволяет в одном классе-дескрипторе управлять несколькими родными классами.
2.3. Более сложные варианты финализации
Финализация является достаточно проблемным аспектом работы .NET. В нормальных сценариях работы приложения финализаторы вызываться не должны, освобождение ресурсов происходить в Dispose()
. Но в аварийных сценариях это может произойти и финализаторы должны работать корректно.
2.3.1. Блокировка финализаторов
Если родной класс находится в DLL, которая загружается и выгружается динамически — с использованием LoadLibrary()/FreeLibrary()
, — то может возникнуть ситуация, когда после выгрузки DLL остались неосвобожденные объекты, имеющие ссылки на экземпляры этого класса. В этом случае через некоторое время сборщик мусора попытается их финализировать, а так как DLL выгружена, то скорее всего произойдет аварийное завершение программы. (Характерный признак — аварийное завершение через несколько секунд после видимого закрытия приложения.) Поэтому после выгрузки DLL финализаторы должны быть блокированы. Этого можно достичь небольшой модификацией базового шаблона ImplPtrBase
.
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)
.
2.3.2. Использование SafeHandle
Класс SafeHandle
реализует достаточно сложный и максимально надежный алгоритм финализации, см. [Richter]. Шаблон ImplPtrBase<>
можно переработать так, чтобы он использовал SafeHandle
. Остальные шаблоны менять не нужно.
using SH = System::Runtime::InteropServices::SafeHandle;
using PtrType = System::IntPtr;
template <typename T, typename D>
public ref class ImplPtrBase : SH
{
protected:
ImplPtrBase(T* p) : SH(PtrType::Zero, true)
{
handle = PtrType(p);
}
T* Ptr() { return static_cast<T*>(handle.ToPointer()); }
bool ReleaseHandle() override
{
if (!IsInvalid)
{
D del;
del(Ptr());
handle = PtrType::Zero;
}
return true;
}
public:
property bool IsInvalid
{
bool get() override
{
return (handle == PtrType::Zero);
}
}
};
Список литературы
[Richter]
Рихтер, Джеффри. Программирование на платформме Microsoft .NET Framework 4.5 на языке C#. 4-е изд.: Пер. с англ. — СПб.: Питер, 2016.
[Cwalina]
Цвалина, Кржиштов. Абрамс, Бред. Инфраструктура программных проектов: соглашения, идиомы и шаблоны для многократно используемых библиотек .NET.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2011.
[Hogenson]
Хогенсон, Гордон. С++/CLI: язык Visual C++ для среды .NET.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2007.