Оглавление


    Введение
    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() {/* ... */} // финализатор
// ...
};

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


  1. Для класса X реализует интерфейс System::IDisposable.
  2. В X::Dispose() обеспечивает вызов деструктора, вызов деструктора базового класса (если он есть) и вызов GC::SupressFinalize().
  3. Переопределяет 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(/* аргументы */)
    {/* ... */}
// ...
};

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


  1. Для класса X реализует интерфейс System::IDisposable.
  2. В 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.



Комментарии (8)


  1. mayorovp
    15.10.2018 13:59

    Но все-таки, почему не SafeHandle?


    1. dm_frox Автор
      16.10.2018 11:39

      Спасибо, что обратили внимания. SafeHandle это тяжелая артиллерия. Но для полноты картины, наверное, стоит привести вариант с его использованием. Для этого шаблон ImplPtrBase надо наследовать от SafeHandle, и немного переписать его. Все остальное остается без изменений.


      1. mayorovp
        16.10.2018 11:58

        Как это — «без изменений», когда вместо деструктора и финализатора будет использоваться метод ReleaseHandle?


        1. dm_frox Автор
          16.10.2018 12:12

          Все изменения инкапсулированы в шаблоне ImplPtrBase. Вот немного упрощенный вариант (без удалителя):
          using SH = System::Runtime::InteropServices::SafeHandle;
          using PtrType = System::IntPtr;

          template <typename T>
          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
              {
                  delete Ptr();
                  return true;
              }

          public:
              property bool IsInvalid
              {
                  bool get() override
                  {
                      return (handle == PtrType::Zero);
                  }
              }
          };


  1. oleg_ra
    16.10.2018 10:07

    «Родные» классы и ресурсы — это что-то особенного!
    <sarcasm>
    Там ещё Google Translator предлагает «природные», «отечественные» и «туземные» жаль, что вы их не выбрали для перевода слова «native».
    </sarcasm>
    Всё-таки тут англицизм «нативные» более уместен и привычен, а потому меньше режет глаз.


    1. dm_frox Автор
      16.10.2018 10:24

      Термин «родной», как перевод native, используется в переводе упомянутой книги Хогенсона, и, наверное, достаточно часто в других публикациях. Еще я встречал слово «собственный». Вообще, мнения по правильному переводу иногда переходит на уровень религиозных войн, возможно, я напишу когда-нибудь статью на эту тему.


      1. oleg_ra
        16.10.2018 11:58

        Cерьёзно? Ну, значит, я отстал от прогресса… Книгу правда перевели больше десяти лет назад, но как-то она мимо меня прошла или, возможно, я её в оригинале читал. Пусть будут «родные», мне не жалко.


        1. Ryppka
          16.10.2018 21:20

          В переводе романа Microserves (Рабы майкрософт) герои разрабатывали «черезплатформенную графическую библиотеку»…
          Я тоже за «нативный», как устоявшийся термин.