Написание приложений Win32 Apps с помощью одних классов C++ (часть 1) - Обертки долевого владения и архитектура

ОГЛАВЛЕНИЕ

Обертки долевого владения

Это ключ для всей реализации: в Win32 API полно разных описателей, приходится "получать", "использовать" и "освобождать".

Обертка не должна переделывать C API: нет особой нужды заменять ShowWindow(hWnd, SW_SHOW) на pWnd->ShowWindow(SW_SHOW): "участники" точно такие же, и нет смысла писать тысячи прототипов, чтобы вынести первый параметр за скобки.

Интересней обернуть описатели, чтобы защитить их от неправильных политик уничтожения или от забывания уничтожить их, или чтобы предоставить более эффективные конструкторы для функций, имеющих много параметров, принимающих одинаковые значения.

В данной реализации обертка похожа на "интеллектуальный указатель с подсчетом ссылок" (по сути, является обобщением): она может быть "владельцем" обертываемого описателя (уничтожает его, когда нет владеющих им владельцев) или "наблюдателем", работающим как "недействительный" при исчезновении всех владельцев.

Но так как может быть несколько разных описателей с разными политиками управления, было решено разделить реализацию на несколько шаблонных классов, наследующих друг от друга.

Архитектура

Ядром всего механизма является взаимодействие между WrpBase и XRefCount.

WrpBase предоставляет "протокол" для обертки: ссылается на "счетчик ссылок" и может быть "прикреплен к" и "отцеплен от" обертываемого им объекта. Он также может быть назначен "владельцем" или "наблюдателем", быть "действительным" или нет, и возвращает итераторы STL для обхода коллекции оберток.

XRefCount хранит информацию о том, сколько (и "которые") оберток обертывает указанное значение или описатель, и предоставляет ряд вспомогательных функций для WrpBase для извлечения и обновления этой информации.

WrpBase является наследуемым (один из его шаблонных параметров, W, является классом, наследуемым от него) и вызывает все свои функции после приведения своего указателя - "this" к W* посредством static_cast, что позволяет переопределить его функции.

Также поведение WrpBase зависит от ряда "помощников", предоставляемых классом типажей, от которого наследует WrpBase. Еще одной задачей этого "класса типажей" является предоставление удобного числа typedef, определяющего способ, посредством которого обернутый объект хранится в обертке, получается в качестве параметра из функций-членов, задается в качестве возвращаемого значения, или разыменовывается.

Классы типажей реализуются в разных версиях и наследуют определение типа от "классов типов".
Чтобы заключить все в нечто, что может быть присвоено, скопировано и т.д., класс Wrp наследует от WrpBase или производного класса, применяя оператор присваивания, конструкторы копирования, операторы равенства (==) и упорядоченности (<), доступа (->) и разыменования (*) и т.д., с помощью определенного протокола WrpBase (после кастомизации, при наличии таковой).

Следующий рисунок иллюстрирует все вышесказанное.

Наследование идет сверху вниз. XRefCount знает (из-за шаблонного параметра) о "YourClass", Wrp знает из-за наследования и вызывает прототипные функции WrpBase способом, позволяющим их переопределить. WrpBase, в свою очередь, использует то, что унаследовано от "типажей" и от "типов".

Реализация

Типы

"types" реализуются двумя разными способами:
•    types_wrp_hnd<H>: предположим, H является описателем, или "маленьким объектом", или указателем, или ... чем-то, что можно привести к LPVOID без потери информации и что хранится "как есть".
•    types_wrp_ptr<H>: предположим, H является объектом в куче, на который ссылается интеллектуальный указатель. Следовательно, H* хранится оберткой, имеющей "семантику указателя".
Оба класса определяют следующие типы:

Тип

Описание

types_wrp_hnd

types_wrp_ptr

SH

Что обертка хранит для представления H

H

H*

IH

Что обертка будет получать в качестве входного параметра в функциях

const H&

H*

OH

Что обертка будет возвращать из своих функций

const H&

H*

RH

Что обертка будет возвращать в качестве "разыменования"

const H&

H&

В конкретных реализациях SH, IH, OH и RH могут отличаться (например: можно возвращать объект-заместитель для хранения ссылки ...).

Неявное преобразование обязательно должно работать при преобразовании IH в SH, SH в OH, и OH в IH.

Типажи

"traits" реализуются тремя разными способами:
•    traits_wrp_hnd<H, Types>: предположим, H является описателем. Большинство из его функций надо переопределить, как только H известен и его политика управления определена.
•    traits_wrp_ptr<H,Types>: предположим, H является "указываемым объектом" (обертка работает как H*) и реализует Create(создать) как new и Destroy(уничтожить) как delete(удалить). static_cast управляет преобразованиями между указателями.
•    traits_wrp_dynptr<H,Types>: то же самое, но с применением dynamic_cast.

Как правило, параметр "Types" принимает значение по умолчанию types_wrp_hnd<H> - в первом случае, и types_wrp_ptr<H> - в остальных двух случаях. Но – для некоторых особых случаев – могут предусматриваться другие решения.

Назначение классов "типажей" – предоставлять ряд низкоуровневых вспомогательных функций, стандартизирующих интерфейс для работы с разными типами для классов верхнего уровня.
Ниже приведен код traits_wrp_hnd:

template<class H, class Types=types_wrp_hnd<H> >
struct traits_wrp_hnd: public Types
{
    typedef Types TT;
    static bool equal(TT::IH left, TT::IH right) { return left == right; }
    static bool less(TT::IH left, TT::IH right) { return left < right; }
    static LPVOID Key(TT::IH h)  { return (LPVOID)h; }
    template<class A> static void from(const A& a, TT::SH& h) { h = (H)a; }
    static TT::IH invalid() {static TT::SH h(NULL); return h; }
    static TT::OH Access(TT::IH h) { return h; }
    static TT::RH Dereference(TT::IH h) { return h; }
    void Create(TT::SH& h) { h = invalid(); }
    void Destroy(TT::SH& h) { h = invalid(); }
};

Заметьте, что "недействительный" реализован так, чтобы возвращать значение "NULL". Для определенного вида описателей это, вероятно, будет переопределено.

WrpBase

Определяется следующим образом:

template<
    class H,    //что обертывается
    class W,    //внешняя обертка (унаследовано отсюда)
    class t_traits, //поведение недействительного H
    class t_refCount    //используемый тип счетчика ссылок
>
class WrpBase:
    public t_traits
{
public:
    typedef WrpBase TWrpBase;
    typedef W TW;
    typedef t_refCount TRefCount;
    typedef t_traits TTraits;
    typedef H TH;
    typedef TTraits::TT TT;
    friend WrpBase;
    friend TRefCount;
    friend TTraits;
protected:
    TT::SH _hnd;
    t_refCount* _pRC;
    bool _bOwn;
    ...
}

Он предоставляет хранилище для члена SH, указатель на класс t_refCount (должен иметь такой же прототип, как и XRefCount_base), и bool, определяющий, является ли данная обертка "владельцем" или "наблюдателем". Он также реализует следующее:
•    void Attach(TT::IH h) прикрепляет обертку к переданному "тупому значению". Ищутся существующие счетчики ссылок для значения и, если не найдены, создаются.
•    void AttachWrp(const WrpBase& w) прикрепляет обертку к значению, хранимому другой оберткой.
•    template<class A> void AttachOther(const A& a) прикрепляет обертку к значению, обнаруживаемому путем преобразований из типа A.
•    void Detach() отцепляет обертку. Вызывает Destroy (унаследовано от TTraits), если обертка-владелец, связанная с обернутым значением, является последней.
•    bool IsValid() const возвращает результат, если обертка и обернутый объект являются  действительными
•    bool IsOwner() const возвращает, независимо от того, является ли обертка оберткой-владельцем или нет.
•    void SetOwnership(bool bOwn) делает обертку "владельцем" (true) или "наблюдателем" (false).
•    typedef TRefCount::iterator iterator; static void get_range(iterator& first, iterator& last, TT::IH h) получает диапазон итератора из конечной коллекции оберток, связанной с обернутым значением. Если first==last –  коллекция пустая или не существует.

Они реализуются через другие функции (всегда через функцию pThis(), преобразующую this в W*) и через функции TRefCount. Он также предоставляет TRefCOunt набор ничего не делающих функций обратного вызова:
•    void OnFirstAttach(XRefCount_base* pRC) вызывается, когда счетчик ссылок считает самую первую ссылку на обернутый объект.
•    void OnAddRef(XRefCount_base* pRC) вызывается всегда, когда добавляется новая ссылка.
•    void OnRelease(XRefCount_base* pRC) вызывается всегда, когда ссылка освобождается.
•    void OnLastRelease(XRefCount_base* pRC) вызывается при последнем освобождении.

Подсчёт ссылок

Должен наследоваться от XRefCount_base, переопределяющего его функции:

class XRefCount_base
{
public:
    bool _bValid;
    XRefCount_base() { _bValid = true; }

    int owners() { return 0; }
    int observers() { return 0; }
    int reference() { return 0; }

    void AddRef(W* pW, bool bOwn)
    {}
   
    bool Release(W* pW, bool bOwn)
    {return false;}

    //не выполнять итерации
    iterator begin() { return NULL; }
    iterator end() { return NULL; }
};

Он реализован в двух версиях:

•    XRefCount<W>: W должен наследоваться от WrpBase. Реализует подсчёт ссылок владельцев и наблюдателей. (reference() – сумма двух). Он автоматически уничтожается при Release(), если ссылок больше нет, возвращая true (в противном случае false). Не предоставляет итераторы (возвращает "первый" и "последний" для случайных равных значений).
•    XRefChain<W>: хранит список W*, проталкивая вперед при AddRef и удаляя при Release. Автоматически удаляется, когда список пустеет.
Первый подходит для типов, требующих только подсчета ссылок, последний - для типов, для которых надо следить, чем являются обертки.

Обратное преобразование

XMap хранит статический std::map<LPVOID, XRefCount_base*>, предоставляющий функции Map, Unmap и Find.

Обертки знают, что они обертывают, и знают, чем является связанный счетчик ссылок. Но при присвоении значения обертке надо каким-то путем выяснить (в случае если "значение" уже обернуто чем-то другим), существует ли (и чем является) связанный XRefCount.

Классы типажей включают в себя функцию Key(IH). Она преобразует обернутый тип в LPVOID.
Это можно сделать с помощью приведения в стиле C (как у traits_wrp_hnd), static_cast (как у неполиморфных указателей в traits_wrp_ptr) и dynamic_cast (для полиморфных указателей, как в traits_wrp_dynptr).

Особенно интересно последнее: если дан сложный объект (с множеством базовых классов), разные указатели на разные компоненты могут отличаться (по абсолютному значению), но приведение посредством dynamic_cast к LPVOID всегда возвращает одинаковый базовый адрес (базовый адрес целого объекта). Это позволяет делить владение сложным объектом между разными интеллектуальными указателями, ссылающимися на разные компоненты: все они могут найти один и тот же объект подсчета ссылок.

Операторы (Wrp<B>)

Конструкторы преобразования, конструкторы копирования, операторы присваивания и т.д. не могут наследоваться как обычные функции. Их переопределение требует особого внимания, поэтому все они были собраны в специальный шаблонный класс. Wrp<B> является "классом верхнего уровня". B является классом, от которого наследуется Wrp. Это может быть любой класс, производный от WrpBase. Он реализует присваивание, копирование, преобразование и уничтожение через Detach/Attach.

Некоторые особые случаи: интеллектуальные указатели

Для специализации обертки метод может делать следующее:
1.    объявить struct, унаследованную от WrpBase, задающую требуемые параметры, затем
2.    определить тип Wrp<yourstruct> посредством typedef, чтобы обеспечить возможность присваивания и копирования.

Это происходит в простом случае определения интеллектуальных указателей:

template<class T>
struct Ptr
{
    struct StatBase:
        public WrpBase<T, StatBase,
          traits_wrp_ptr<T>, XRefCount<StatBase> >
    {};

    struct DynBase:
        public WrpBase<T, DynBase,
          traits_wrp_dynptr<T>, XRefCount<DynBase> >
    {};

    typedef Wrp<StatBase > Static;
    typedef Wrp<DynBase > Dynamic;
};

Две структуры определяют поведение для обертки указателя static_cast и dynamic_cast.

Если A является классом, интеллектуальный указатель на A может быть ...

typedef Ptr<A>::Dynamic PA;.

Интеллектуальный описатель

Таким же образом получается "интеллектуальный описатель" с помощью:

template<class H, class W>
struct HndBase:
    public WrpBase<H, W, traits_wrp_hnd<H>, XRefCount<W> >
{};

В то время как Ptrxxx struct закрывают наследование обертки (они передаются в WrpBase в качестве параметра W), HndBase не делает этого: все же имеется параметр W.

Причина в том, что, хотя для указателя new и delete однозначно являются политикой создания и уничтожения (Create и Destroy исходят из классов типажей таким образом), для описателей их все же надо определять. Разные описатели имеют разный API создания и уничтожения. Следовательно, HDC имеет многие из них! Его придется доделать позже.

Конечно, для отдельных конкретных объектов понадобится делать нечто, отличное от "new" или "delete": при этом придется самостоятельно определить struct, производную от WrpBase (и закрывающую его наследование), и затем обернуть ее в Wrp.