Страница:
Вспомним правило, гласящее, что все запросы QueryInterface на объект относительно IUnknown должны возвращать точно такое же значение указателя. Именно так в СОМ обеспечивается идентификация объектов. В то же время Спецификация СОМ определенно разрешает возвращать другие значения указателей в ответ на запросы QueryInterface относительно любых других типов интерфейсов, кроме IUnknown. Это означает, что для нечасто используемых интерфейсов объект может динамически выделять память для vptr по требованию, не заботясь о возврате того же самого динамически выделенного блока памяти каждый раз, когда запрашивается какой-либо интерфейс. Эта технология временного (transient) размещения композитов впервые была описана в «белой книге» Microsoft Поваренная книга для программистов СОМ (Microsoft white paper The СОМ Programmer's Cookbook), написанной Криспином Госвеллом (Crispin Goswell) (http://www.microsoft.com/oledev). В этой «белой книге» такие временные интерфейсы называются отделяемыми (tearoff).
Реализация отделяемого интерфейса подобна реализации интерфейса с использованием композиции. Для отделяемого интерфейса должен быть определен второй класс, наследующий тому интерфейсу, который он будет реализовывать. Чтобы обеспечить идентификацию, QueryInterface отделяемого интерфейса должен делегировать управление функции QueryInterface основного класса. Два основных различия заключаются в том, что:
1) главный объект динамически размещает отделяемый интерфейс вместо того, чтобы иметь элемент данных экземпляра, и
2) отделяемый композит должен содержать явный обратный указатель на главный объект, так как технология фиксированного смещения, используемая в композиции, здесь не работает, поскольку отделяемый интерфейс изолирован от основного объекта. Следующий класс реализует IBoat как отделяемый интерфейс:
class CarBoat : public ICar
{
LONG m_cRef;
CarBoat (void): m_cRef(0) {}
public:
// IUnknown methods
// методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
// IVehicle methods
// методы IVehicle
STDMETHODIMP GetMaxSpeed(long *pMax);
// ICar methods
// методы ICar
STDMETHODIMP Brake(void);
// define nested class that implements IBoat
// определяем вложенный класс, реализующий IBoat
struct XBoat : public IBoat
{
LONG m_cBoatRef;
// back pointer to main object is explicit member
// обратный указатель на главный объект – явный член
CarBoat *m_pThis;
inline CarBoat* This()
{
return m_pThis;
}
XBoat(CarBoat *pThis);
~XBoat(void);
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP Sink(void);
};
// note: no data member of type Xboat
// заметим: нет элементов данных типа Xboat
};
Для QueryInterface главного объекта необходимо динамически разместить новый отделяемый интерфейс – каждый раз, когда запрашивается IBoat:
STDMETHODIMP CarBoat::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IBoat)
*ppv = static_cast<IBoat*>(new XBoat(this));
else if (riid == IID_IUnknown)
*ppv = static_cast<IUnknown*>(this);
:
:
:
Каждый раз при получении запроса на интерфейс IBoat размещается новый отделяемый интерфейс. Согласно стандартной практике QueryInterface вызова AddRef посредством результирующего указателя: ((IUnknown*)*ppv)->AddRef();
AddRef будет обрабатывать непосредственно из QueryInterface только отделяемый интерфейс. Важно то, что главный объект остается в памяти столько времени, сколько существует отделяемый интерфейс. Простейший путь обеспечить это – заставить сам отделяемый интерфейс представлять неосвобожденную ссылку. Это можно реализовать в разработчике и деструкторе отделяемого интерфейса:
CarBoat::XBoat::XBoat(CarBoat *pThis) : m_cBoatRef(0), m_pThis(pThis)
{
m_pThis->AddRef();
}
CarBoat::XBoat::~XBoat(void)
{
m_pThis->Release();
}
Как и в случае с композицией, методу QueryInterface отделяемого интерфейса требуется идентифицировать объект, делегируя освобождение функции главного объекта. Однако отделяемый интерфейс может выявлять запросы на тот интерфейс (интерфейсы), который он сам реализует, и просто возвращать указатель, обработанный AddRef, себе самому:
STDMETHODIMP CarBoat::XBoat::QueryInterface(REFIID riid, void**ppv)
{
if (riid != IID_IBoat) return This()->QueryInterface(riid, ppv);
*ppv = static_cast<IBoat*>(this);
reinterpret_cast<IUnknown*>(*ppv)->AddRef();
return S_OK;
}
Ввиду того, что отделяемый интерфейс должен самоуничтожаться, когда он больше не нужен, он должен поддерживать свой собственный счетчик ссылок и уничтожать себя, когда этот счетчик достигнет нуля. Как отмечалось ранее, деструктор отделяемого интерфейса освободит главный объект до того, как сам исчезнет из памяти:
STDMETHODIMP_(ULONG) CarBoat::XBoat::AddRef (void)
{
return InterlockedIncrement(&m_cRef);
}
STDMETHODIMP_(ULONG) CarBoat::XBoat::Release(void)
{
ULONG res = InterlockedDecrement(&m_cBoatRef);
if (res == 0) delete this;
// dtor releases main object
// деструктор освобождает главный объект
return res;
}
Как и в случае с композицией, метод This() можно использовать в любых методах отделяемого интерфейса, которым требуется получить статус главного объекта. Разница состоит в том, что отделяемым интерфейсам требуется явный обратный указатель, в то время как нормальные композиты могут использовать фиксированные смещения, выделяя по четыре байта на композит.
На первый взгляд, отделяемые интерфейсы кажутся лучшей из всех возможностей. Когда интерфейс не используется, то на его служебные данные отводится нуль байт объекта. Когда же интерфейс используется, объект косвенно тратит 4 байта на служебные данные отделяемого интерфейса. Подобное впечатление базируется на нескольких обманчивых предположениях. Во-первых, затраты на работающий отделяемый интерфейс составляют отнюдь не только 4 байта памяти для его vptr. Отделяемому интерфейсу требуются также обратный указатель и счетчик ссылок[1]. Во-вторых, несмотря на возможность использования специального распределителя памяти (custom memory allocator ), отделяемому интерфейсу потребуется по крайней мере 4 дополнительных байта на выравнивание и/или заполнение заголовков динамически выделенной памяти, используемых С-библиотекой для реализации malloc/operator new . Это означает, что объект действительно экономит 4 байта, когда интерфейс не используется. Но когда интерфейс используется, отделяемый интерфейс тратит как минимум 12 байт, если подключен специальный распределитель памяти, и 16 байт, если, по умолчанию, подключен оператор new. Если интерфейс запрашивается редко, то такая оптимизация имеет смысл, особенно если клиент освобождает этот интерфейс вскоре после получения. Если же клиент хранит отделяемый интерфейс в течение всего времени жизни объекта, то преимущества отделяемого интерфейса теряются.
К сожалению, дело с отделяемым интерфейсом обстоит еще хуже. Как видно из показанной ранее реализации, если объект получает два запроса QueryInterface на тот же самый отделяемый интерфейс, то будут созданы две копии этого отделяемого интерфейса, так как указатель на первый из них полностью забывается главным объектом, поскольку он был возвращен вызывающему объекту. Это означает, что в этом случае отделяемый интерфейс занимает по крайней мере от 24 до 32 байт, так как в памяти находятся оба vptr отделяемого интерфейса, по одному на каждый запрос QueryInterface. Эта память не будет восстановлена, пока клиент не освободит каждый отделяемый интерфейс. Ситуация, когда два запроса QueryInterface удерживают указатель в течение всего времени жизни объекта, особенно важна, так как именно это и происходит при удаленном обращении к объекту. СОМ-слой, реализующий удаленные вызовы, будет дважды запрашивать объект (с помощью QueryInterface) на предмет одного и того же интерфейса и будет удерживать оба результата в течение всего времени жизни объекта. Это обстоятельство делает отделяемые интерфейсы особенно рискованными для объектов, к которым может осуществляться удаленный доступ.
Узнав обо всех подводных камнях отделяемых интерфейсов, задаешь себе логичный вопрос: "В каких же случаях отделяемые интерфейсы являются подходящими?" Не существует безусловного ответа; в то же время отделяемые интерфейсы очень хороши для поддержки большого числа взаимно исключающих интерфейсов. Рассмотрим случай, в котором в дополнение к трем транспортным интерфейсам, показанным ранее, имеются интерфейсы ITruck (грузовик), IMonsterТruck (грузовик-монстр), IMotorcycle (мотоцикл), IBicycle (велосипед), IUnicycle (уницикл), ISkateboard (скейтборд) и IHelicopter (вертолет), причем все они наследуют IVehicle. Если бы производящий транспортный класс хотел поддерживать любой из этих интерфейсов, но только по одному из них для каждого заданного экземпляра, то для осуществления этого отделяемые интерфейсы были бы прекрасным способом при условии, что главный объект кэшировал бы указатель на первый отделяемый интерфейс. Определение класса главного объекта выглядело бы примерно так:
class GenericVehicle : public IUnknown
{
LONG m_cRef;
IVehicle *m_pTearOff;
// cached ptr to tearoff
// кэшированный указатель на отделяемый интерфейс
GenericVehicle(void) : m_cRef(0), m_pTearOff(0) {}
// IUnknown methods
// методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release (void);
// define tearoff classes
// определяем классы отделяемых интерфейсов
class XTruck : public ITruck { … };
class XMonsterTruck : public IMonsterTruck { … };
class XBicycle : public IBicycle { … };
:
:
:
};
В этом классе в случае, когда не используется ни один из интерфейсов, объект платит за пустой кэшированный указатель только четырьмя дополнительными байтами. Когда приходит запрос QueryInterfасе на один из десяти транспортных интерфейсов, то память выделена для нового отделяемого интерфейса один раз и кэширована для более позднего использования:
STDMETHODIMP GenericVehicle::QueryInterface(REFIID riid ,void **ppv)
{ if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
else if (riid == IID_ITruck) { if (m_pTearOff == 0)
// no tearoff yet, make one
// отделяемого интерфейса еще нет, создаем один
m_pTearOff = new XTruck(this);
if (m_pTearOff)
// tearoff exists, let tearoff QI
// отделяемый интерфейс существует, пусть это QI
return m_pTearOff->QueryInterface(riid, ppv);
else
// memory allocation failure
// ошибка выделения памяти
return (*ppv = 0), E_NOINTERFACE;
}
else if (riid == IID_IMonsterTruck)
{
if (in_pTearOff == 0)
// no tearoff yet, make one
// отделяемого интерфейса еще нет, создаем один
m_pTearOff = new XMonsterTruck(this);
if (m_pTearOff)
// tearoff exists, let tearoff QI
// отделяемый интерфейс существует, пусть это QI
return m_pTearOff->QueryInterface(riid, ppv);
else
// memory allocation failure
// ошибка выделения памяти
return (*ppv = 0), E_NOINTERFACE;
}
else …
:
:
:
}
На основе показанной здесь реализации QueryInterface на каждый объект будет приходиться по большей части по одному отделяемому интерфейсу. Это значит, что в случае отсутствия запросов на транспортные интерфейсы объект будет тратить в сумме 12 байт (vptr IUnknown + счетчик ссылок + кэшированный указатель на отделяемый интерфейс). Если транспортный интерфейс запрошен, то объект будет тратить в сумме от 24 до 28 байт (исходные 12 байт + наследующий Vehicle vptr + счетчик ссылок + обратный указатель на главный объект + (необязательно) служебная запись malloc (memory allocation – выделение памяти)).
Если бы в данном случае отделяемые интерфейсы не использовались, то определение класса выглядело бы примерно так:
class GenericVehicle : public ITruck, public IHelicopter, public IBoat, public ICar, public IMonsterTruck, public IBicycle, public IMotorcycle, public ICar, public IPlane, public ISkateboard { LONG m_cRef;
// IUnknown methods – методы IUnknown
:
:
:
};
В результате этот класс создал бы объекты, тратящие всегда 44 байта (десять vptr + счетчик ссылок). Хотя производящий класс может показаться немного запутанным, постоянные интерфейсы СОМ принадлежат к аналогичной категории, так как в настоящее время существует восемь различных постоянных интерфейсов, но объект обычно выставляет только один из них на экземпляр. В то же время разработчик класса не всегда может предсказать, какой из интерфейсов будет запрошен определенным клиентом (и будет ли какой-либо). Кроме того, каждый из восьми интерфейсов требует своего набора поддерживающих элементов данных для корректной реализации методов интерфейса. Если эти элементы данных были созданы как часть отделяемого интерфейса, а не главного объекта, то для каждого объекта будет назначен только один набор элементов данных. Этот тип сценария идеален для отделяемых интерфейсов, но опять же, для большей эффективности, указатель на отделяемый интерфейс следует кэшировать в главном объекте.
Двоичная композиция
Реализация отделяемого интерфейса подобна реализации интерфейса с использованием композиции. Для отделяемого интерфейса должен быть определен второй класс, наследующий тому интерфейсу, который он будет реализовывать. Чтобы обеспечить идентификацию, QueryInterface отделяемого интерфейса должен делегировать управление функции QueryInterface основного класса. Два основных различия заключаются в том, что:
1) главный объект динамически размещает отделяемый интерфейс вместо того, чтобы иметь элемент данных экземпляра, и
2) отделяемый композит должен содержать явный обратный указатель на главный объект, так как технология фиксированного смещения, используемая в композиции, здесь не работает, поскольку отделяемый интерфейс изолирован от основного объекта. Следующий класс реализует IBoat как отделяемый интерфейс:
class CarBoat : public ICar
{
LONG m_cRef;
CarBoat (void): m_cRef(0) {}
public:
// IUnknown methods
// методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
// IVehicle methods
// методы IVehicle
STDMETHODIMP GetMaxSpeed(long *pMax);
// ICar methods
// методы ICar
STDMETHODIMP Brake(void);
// define nested class that implements IBoat
// определяем вложенный класс, реализующий IBoat
struct XBoat : public IBoat
{
LONG m_cBoatRef;
// back pointer to main object is explicit member
// обратный указатель на главный объект – явный член
CarBoat *m_pThis;
inline CarBoat* This()
{
return m_pThis;
}
XBoat(CarBoat *pThis);
~XBoat(void);
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP Sink(void);
};
// note: no data member of type Xboat
// заметим: нет элементов данных типа Xboat
};
Для QueryInterface главного объекта необходимо динамически разместить новый отделяемый интерфейс – каждый раз, когда запрашивается IBoat:
STDMETHODIMP CarBoat::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IBoat)
*ppv = static_cast<IBoat*>(new XBoat(this));
else if (riid == IID_IUnknown)
*ppv = static_cast<IUnknown*>(this);
:
:
:
Каждый раз при получении запроса на интерфейс IBoat размещается новый отделяемый интерфейс. Согласно стандартной практике QueryInterface вызова AddRef посредством результирующего указателя: ((IUnknown*)*ppv)->AddRef();
AddRef будет обрабатывать непосредственно из QueryInterface только отделяемый интерфейс. Важно то, что главный объект остается в памяти столько времени, сколько существует отделяемый интерфейс. Простейший путь обеспечить это – заставить сам отделяемый интерфейс представлять неосвобожденную ссылку. Это можно реализовать в разработчике и деструкторе отделяемого интерфейса:
CarBoat::XBoat::XBoat(CarBoat *pThis) : m_cBoatRef(0), m_pThis(pThis)
{
m_pThis->AddRef();
}
CarBoat::XBoat::~XBoat(void)
{
m_pThis->Release();
}
Как и в случае с композицией, методу QueryInterface отделяемого интерфейса требуется идентифицировать объект, делегируя освобождение функции главного объекта. Однако отделяемый интерфейс может выявлять запросы на тот интерфейс (интерфейсы), который он сам реализует, и просто возвращать указатель, обработанный AddRef, себе самому:
STDMETHODIMP CarBoat::XBoat::QueryInterface(REFIID riid, void**ppv)
{
if (riid != IID_IBoat) return This()->QueryInterface(riid, ppv);
*ppv = static_cast<IBoat*>(this);
reinterpret_cast<IUnknown*>(*ppv)->AddRef();
return S_OK;
}
Ввиду того, что отделяемый интерфейс должен самоуничтожаться, когда он больше не нужен, он должен поддерживать свой собственный счетчик ссылок и уничтожать себя, когда этот счетчик достигнет нуля. Как отмечалось ранее, деструктор отделяемого интерфейса освободит главный объект до того, как сам исчезнет из памяти:
STDMETHODIMP_(ULONG) CarBoat::XBoat::AddRef (void)
{
return InterlockedIncrement(&m_cRef);
}
STDMETHODIMP_(ULONG) CarBoat::XBoat::Release(void)
{
ULONG res = InterlockedDecrement(&m_cBoatRef);
if (res == 0) delete this;
// dtor releases main object
// деструктор освобождает главный объект
return res;
}
Как и в случае с композицией, метод This() можно использовать в любых методах отделяемого интерфейса, которым требуется получить статус главного объекта. Разница состоит в том, что отделяемым интерфейсам требуется явный обратный указатель, в то время как нормальные композиты могут использовать фиксированные смещения, выделяя по четыре байта на композит.
На первый взгляд, отделяемые интерфейсы кажутся лучшей из всех возможностей. Когда интерфейс не используется, то на его служебные данные отводится нуль байт объекта. Когда же интерфейс используется, объект косвенно тратит 4 байта на служебные данные отделяемого интерфейса. Подобное впечатление базируется на нескольких обманчивых предположениях. Во-первых, затраты на работающий отделяемый интерфейс составляют отнюдь не только 4 байта памяти для его vptr. Отделяемому интерфейсу требуются также обратный указатель и счетчик ссылок[1]. Во-вторых, несмотря на возможность использования специального распределителя памяти (custom memory allocator ), отделяемому интерфейсу потребуется по крайней мере 4 дополнительных байта на выравнивание и/или заполнение заголовков динамически выделенной памяти, используемых С-библиотекой для реализации malloc/operator new . Это означает, что объект действительно экономит 4 байта, когда интерфейс не используется. Но когда интерфейс используется, отделяемый интерфейс тратит как минимум 12 байт, если подключен специальный распределитель памяти, и 16 байт, если, по умолчанию, подключен оператор new. Если интерфейс запрашивается редко, то такая оптимизация имеет смысл, особенно если клиент освобождает этот интерфейс вскоре после получения. Если же клиент хранит отделяемый интерфейс в течение всего времени жизни объекта, то преимущества отделяемого интерфейса теряются.
К сожалению, дело с отделяемым интерфейсом обстоит еще хуже. Как видно из показанной ранее реализации, если объект получает два запроса QueryInterface на тот же самый отделяемый интерфейс, то будут созданы две копии этого отделяемого интерфейса, так как указатель на первый из них полностью забывается главным объектом, поскольку он был возвращен вызывающему объекту. Это означает, что в этом случае отделяемый интерфейс занимает по крайней мере от 24 до 32 байт, так как в памяти находятся оба vptr отделяемого интерфейса, по одному на каждый запрос QueryInterface. Эта память не будет восстановлена, пока клиент не освободит каждый отделяемый интерфейс. Ситуация, когда два запроса QueryInterface удерживают указатель в течение всего времени жизни объекта, особенно важна, так как именно это и происходит при удаленном обращении к объекту. СОМ-слой, реализующий удаленные вызовы, будет дважды запрашивать объект (с помощью QueryInterface) на предмет одного и того же интерфейса и будет удерживать оба результата в течение всего времени жизни объекта. Это обстоятельство делает отделяемые интерфейсы особенно рискованными для объектов, к которым может осуществляться удаленный доступ.
Узнав обо всех подводных камнях отделяемых интерфейсов, задаешь себе логичный вопрос: "В каких же случаях отделяемые интерфейсы являются подходящими?" Не существует безусловного ответа; в то же время отделяемые интерфейсы очень хороши для поддержки большого числа взаимно исключающих интерфейсов. Рассмотрим случай, в котором в дополнение к трем транспортным интерфейсам, показанным ранее, имеются интерфейсы ITruck (грузовик), IMonsterТruck (грузовик-монстр), IMotorcycle (мотоцикл), IBicycle (велосипед), IUnicycle (уницикл), ISkateboard (скейтборд) и IHelicopter (вертолет), причем все они наследуют IVehicle. Если бы производящий транспортный класс хотел поддерживать любой из этих интерфейсов, но только по одному из них для каждого заданного экземпляра, то для осуществления этого отделяемые интерфейсы были бы прекрасным способом при условии, что главный объект кэшировал бы указатель на первый отделяемый интерфейс. Определение класса главного объекта выглядело бы примерно так:
class GenericVehicle : public IUnknown
{
LONG m_cRef;
IVehicle *m_pTearOff;
// cached ptr to tearoff
// кэшированный указатель на отделяемый интерфейс
GenericVehicle(void) : m_cRef(0), m_pTearOff(0) {}
// IUnknown methods
// методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release (void);
// define tearoff classes
// определяем классы отделяемых интерфейсов
class XTruck : public ITruck { … };
class XMonsterTruck : public IMonsterTruck { … };
class XBicycle : public IBicycle { … };
:
:
:
};
В этом классе в случае, когда не используется ни один из интерфейсов, объект платит за пустой кэшированный указатель только четырьмя дополнительными байтами. Когда приходит запрос QueryInterfасе на один из десяти транспортных интерфейсов, то память выделена для нового отделяемого интерфейса один раз и кэширована для более позднего использования:
STDMETHODIMP GenericVehicle::QueryInterface(REFIID riid ,void **ppv)
{ if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
else if (riid == IID_ITruck) { if (m_pTearOff == 0)
// no tearoff yet, make one
// отделяемого интерфейса еще нет, создаем один
m_pTearOff = new XTruck(this);
if (m_pTearOff)
// tearoff exists, let tearoff QI
// отделяемый интерфейс существует, пусть это QI
return m_pTearOff->QueryInterface(riid, ppv);
else
// memory allocation failure
// ошибка выделения памяти
return (*ppv = 0), E_NOINTERFACE;
}
else if (riid == IID_IMonsterTruck)
{
if (in_pTearOff == 0)
// no tearoff yet, make one
// отделяемого интерфейса еще нет, создаем один
m_pTearOff = new XMonsterTruck(this);
if (m_pTearOff)
// tearoff exists, let tearoff QI
// отделяемый интерфейс существует, пусть это QI
return m_pTearOff->QueryInterface(riid, ppv);
else
// memory allocation failure
// ошибка выделения памяти
return (*ppv = 0), E_NOINTERFACE;
}
else …
:
:
:
}
На основе показанной здесь реализации QueryInterface на каждый объект будет приходиться по большей части по одному отделяемому интерфейсу. Это значит, что в случае отсутствия запросов на транспортные интерфейсы объект будет тратить в сумме 12 байт (vptr IUnknown + счетчик ссылок + кэшированный указатель на отделяемый интерфейс). Если транспортный интерфейс запрошен, то объект будет тратить в сумме от 24 до 28 байт (исходные 12 байт + наследующий Vehicle vptr + счетчик ссылок + обратный указатель на главный объект + (необязательно) служебная запись malloc (memory allocation – выделение памяти)).
Если бы в данном случае отделяемые интерфейсы не использовались, то определение класса выглядело бы примерно так:
class GenericVehicle : public ITruck, public IHelicopter, public IBoat, public ICar, public IMonsterTruck, public IBicycle, public IMotorcycle, public ICar, public IPlane, public ISkateboard { LONG m_cRef;
// IUnknown methods – методы IUnknown
:
:
:
};
В результате этот класс создал бы объекты, тратящие всегда 44 байта (десять vptr + счетчик ссылок). Хотя производящий класс может показаться немного запутанным, постоянные интерфейсы СОМ принадлежат к аналогичной категории, так как в настоящее время существует восемь различных постоянных интерфейсов, но объект обычно выставляет только один из них на экземпляр. В то же время разработчик класса не всегда может предсказать, какой из интерфейсов будет запрошен определенным клиентом (и будет ли какой-либо). Кроме того, каждый из восьми интерфейсов требует своего набора поддерживающих элементов данных для корректной реализации методов интерфейса. Если эти элементы данных были созданы как часть отделяемого интерфейса, а не главного объекта, то для каждого объекта будет назначен только один набор элементов данных. Этот тип сценария идеален для отделяемых интерфейсов, но опять же, для большей эффективности, указатель на отделяемый интерфейс следует кэшировать в главном объекте.
Двоичная композиция
Композиция и отделяемые интерфейсы – это две технологии на уровне исходного кода, предназначенные для реализации объектов СОМ на C++. Обе эти технологии требуют, чтобы разработчик объекта имел определения для каждого класса композита или отделяемого интерфейса в исходном коде C++, для возможности обработать подобъект, прежде чем возвратить его посредством QueryInterface. Для ряда ситуаций это очень разумно. В некоторых случаях, однако, было бы удобнее упаковать многократно используемую реализацию одного или большего числа интерфейсов в двоичный компонент, который мог бы обрабатываться через границы DLL, не нуждаясь в исходном коде подкомпонента. Это позволило бы более широкой аудитории повторно использовать подкомпонент, избегая слишком тесной связи с ним, как в случае повторного использования на уровне исходного кода (этот случай описан в главе 1). Однако если компонент повторного использования представляет собой двоичный композит или отделяемый интерфейс, то он должен участвовать в общей идентификации объекта.
Для полного охвата проблем, относящихся к унифицированию идентификации через границы компонентов, рассмотрим следующую простую реализацию ICar:
class Car : public ICar
{
LONG m_cRef; Car(void) : m_cRef(0) {} STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pn);
STDMETHODIMP Brake(void); };
STDMETHODIMP Car::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*>(this);
else if (riid == IID_ICar) *ppv = static_cast<ICar*>(this);
else return (*ppv = 0), E_NOINTERFACE;
((IUnknown*)*ppv)->AddRef();
return S_OK;
}
// car class object's IClassFactory::CreateInstance
// интерфейс IClassFactory::CreateInstance
// объекта класса car
STDMETHODIMP CarClass::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv)
{
Car *pCar = new Car;
if (*pCar) return (*ppv = 0), E_OUTOFMEMORY;
pCar->AddRef();
HRESULT hr = pCar->QueryInterface(riid, ppv);
pCar->Release(); return hr;
}
Этот класс просто использует фактические реализации QueryInterface, AddRef и Release.
Рассмотрим второй класс C++, который пытается использовать реализацию Car как двоичный композит:
class CarBoat : public IBoat
{
LONG m_cRef;
Unknown *m_pUnkCar;
CarBoat(void);
virtual ~CarBoat(void);
STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pn);
STDMETHODIMP Sink(void);
};
Для эмуляции композиции разработчику пришлось бы создать подобъект Car, а деструктору – освободить указатель на подобъект:
CarBoat::CarBoat (void) : m_cRef(0)
{
HRESULT hr = CoCreateInstance(CLSID_Car, 0, CLSCTX_ALL, IID_IUnknown, (void**)&m_pUnkCar);
assert(SUCCEEDED(hr));
}
CarBoat::~CarBoat(void)
{
if (m_pUnkCar) m_pUnkCar->Release();
}
Интересная проблема возникает в реализации QueryInterface:
STDMETHODIMP CarBoat::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*>(this);
else if (riid == IID_IBoat) *ppv = static_cast<IBoat*>(this);
else if (riid == IID_ICar)
// forward request…
// переадресовываем запрос…
return m_pUnkCar->QueryInterface(riid, ppv);
else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef();
return S_OK;
}
Поскольку объект Car не имеет понятия о том, что он является частью идентификационной единицы (identity) другого объекта, то он будет причиной неуспеха любых запросов QueryInterface для IBoat. Это означает, что
QI(IBoat)->ICar
пройдет успешно, а запрос
QI(QI(IBoat)->ICar)->IBoat
потерпит неудачу, так как полученная QueryInterface будет несимметричной. Вдобавок запросы QueryInterface о IUnknown через интерфейсные указатели ICar и IBoat вернут различные значения, а это означает, что будет идентифицировано два различных объекта. Из подобных нарушений протокола IUnknown следует, что объекты CarBoat попросту не являются действительными объектами СОМ.
Идея составления объекта из двоичных композитов звучит красиво. Действительно, Спецификация СОМ четко и подробно указывает, как реализовать эту идею в стандартной и предсказуемой манере. Технология выставления клиенту двоичного подкомпонента непосредственно через QueryInterface называется СОМ-агрегированием. СОМ-агрегирование является лишь набором правил, определяющих отношения между внешним объектом (агрегирующим) и внутренним (агрегируемым). СОМ-агрегирование – это просто набор правил IUnknown, позволяющих более чем одному двоичному компоненту фигурировать в качестве идентификационной единицы (identity) СОМ.
Агрегирование СОМ несомненно является главной движущей силой для повторного использования в СОМ. Намного проще приписывать объекту значения и использовать его методы в реализации методов других объектов. Только в редких случаях кто-то захочет выставлять интерфейсы другого объекта непосредственно клиенту как часть той же самой идентификационной единицы. Рассмотрим следующий сценарий:
class Handlebar : public IHandlebar { … };
class Wheel : public IWheel {};
class Bicycle : public IBicycle
{
IHandlebar * m_pHandlebar;
IWheel *m_pFrontWheel;
IWheel *m_pBackWheel;
}
Было бы опрометчиво для класса Вicycle объявлять интерфейсы IHandlebar (велосипедный руль) или IWheel (колесо) в собственном методе QueryInterface. QueryInterface зарезервирован для выражения отношений «является» (is-a), а велосипед (bicycle) очевидно не является колесом (wheel) или рулем (handlebar). Если разработчик Bicycle хотел бы обеспечить прямой доступ к этим сторонам объекта, то интерфейс IBicycle должен был бы иметь для этой цели аксессоры определенных свойств:
[object, uuid(753A8A60-A7FF-11d0-8C30-0080C73925BA)] interface IBicycle : IVehicle
{
HRESULT GetHandlebar([out,retval] IHandlebar **pph);
HRESULT GetWheels([out] IWheel **ppwFront, [out] IWheel **ppwBack);
}
Реализация Bicycle могла бы тогда просто возвращать указатели на свои подобъекты:
STDMETHODIMP Bicycle::GetHandlebar(IHandlebar **pph)
{
if (*pph = m_pHandlebar) (*pph)->AddRef();
return S_OK;
}
STDMETHODIMP Bicycle::GetWheels(IWheel **ppwFront, IWheel **ppwBack)
{
if (*ppwFront = m_pFrontWheel) (*ppwFront)->AddRef();
if (*ppwBack = m_pBackWheel) (*ppwBack)->AddRef();
return S_OK;
}
При использовании данной технологии клиент по-прежнему получает прямой доступ к подобъектам. Однако поскольку указатели получены через явные методы, а не через QueryInterface, то между различными компонентами не существует никаких идентификационных отношений.
Несмотря на этот пример, все же остаются сценарии, где желательно обеспечить реализацию интерфейса, которая могла бы быть внедрена в идентификационную единицу другого объекта. Чтобы осуществить это, в СОМ-агрегировании требуется, чтобы внутренний объект (агрегируемый) уведомлялся во время его создания, что он создается как часть идентификационной единицы другого объекта. Это означает, что создающая функция (creation function), обычно используемая для создания объекта, требует один дополнительный параметр: указатель IUnknown на идентификационную единицу, которой агрегирующий объект должен передать функции в ее методы QueryInterface, AddRef и Release. Покажем определение метода CreateInstance интерфейса IClassFactory:
HRESULT CreateInstance([in] Unknown *pUnkOuter, [in] REFIID riid, [out, iid_is(riid)] void **ppv);
Этот метод (и соответствующие API-функции CoCreateInstanceEx и CoCreateInstance) перегружен с целью поддержки создания автономных (stand-alone ) объектов и агрегатов. Если вызывающий объект передает нулевой указатель и качестве первого параметра CreateInstance (pUnkOuter ), то результирующий объект будет автономной идентификационной единицей самого себя. Если же вызывающий объект передает в качестве первого параметра ненулевой указатель, то результирующий объект будет агрегатом с идентификационной единицей, ссылка на которую содержится в pUnkOuter. В случае агрегации агрегат должен переадресовывать все запросы QueryInterface, AddRef и Release непосредственно и безусловно на pUnkOuter. Это необходимо для обеспечения идентификации объекта.
Имея прототип функции, приведенный выше, класс CarBoat после небольшой модификации будет удовлетворять правилам агрегации:
CarBoat::CarBoat(void) : m_cRef(0)
{
// need to pass identity of self to Create routine
// to notify car object it 1s an aggregate
// нужно передать свою идентификацию подпрограмме
// Create для уведомления объекта car, что он – агрегат
HRESULT hr = CoCreateInstance(CLSID_Car, this, CLSCTX_ALL, IID_IUnknown, (void**)&m_pUnkCar);
assert(SUCCEEDED(hr));
}
Реализация CarBoat QueryInterface просто переадресовывает запрос ICar внутреннему агрегату:
STDMETHODIMP CarBoat::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
else if (riid == IID_ICar)
// forward request…
// переадресовываем запрос…
return m_pUnkCar->QueryInterface(riid, ppv);
else if (riid == IID_IBoat)
:
:
:
Теоретически это должно работать, так как агрегат будет всегда переадресовывать любые последующие запросы QueryInterface обратно главному объекту, проводя таким образом идентификацию объекта.
В предыдущем сценарии метод CreateInstance класса Car возвращает внешнему объекту указатель интерфейса, наследующего IUnknown. Если бы этот интерфейсный указатель должен был просто делегировать вызовы функций интерфейсу IUnknown внешнего объекта, то невозможно было бы: 1) уведомить агрегат, что он больше не нужен; 2) запросить интерфейсные указатели при выделении их клиентам главного объекта. На деле результатом приведенной выше реализации QueryInterface будет бесконечный цикл, поскольку внешний объект делегирует функции внутреннему, который делегирует их обратно внешнему.
Для решения этой проблемы необходимо сделать так, чтобы начальный интерфейсный указатель, который возвращается внешнему объекту, не делегировал вызовы реализации IUnknown внешнего объекта. Это означает, что объекты, поддерживающие СОМ– агрегирование, должны иметь две реализации IUnknown. Делегирующая, то есть передающая функции, реализация переадресовывает все запросы QueryInterface, AddRef и Release внешней реализации. Это и есть реализация по умолчанию, на которую ссылаются таблицы vtbl всех объектов, и это именно та версия, которую видят внешние клиенты. Объект должен также иметь неделегирующую реализацию IUnknown, которая выставляется только агрегирующему внешнему объекту.
Имеется несколько возможностей обеспечить две различные реализации IUnknown от одного объекта. Самый прямой путь[1] – это использование композиции и элемента данных для реализации неделегирующих методов IUnknown. Ниже показана реализация Car, поддающаяся агрегации:
class Car : public ICar
{
LONG m_cRef;
IUnknown *m_pUnk0uter;
public: Car(IUnknown *pUnk0uter);
// non-delegating IUnknown methods
// неделегирующие методы
IUnknown STDMETHODIMP InternalQueryInterface(REFIID, void **);
STDMETHODIMP (ULONG) InternalAddRef(void);
STDMETHODIMP_(ULONG) InternalRelease(void);
// delegating IUnknown methods
// делегирующие методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(*long *pn);
STDMETHODIMP Brake(void);
// composite to map distinguished IUnknown vptr to
// non-delegating InternalXXX routines in main object
// композит для преобразования определенного vptr IUnknown
// в неделегирующие подпрограммы InternalXXX в главном
// объекте
class XNDUnknown : public IUnknown
{ Car* This()
{
return (Car*)((BYTE*)this – offsetof(Car, m_innerUnknown));
}
STDMETHODIMP QueryInterface(REFIID r, void**p)
{
return This()->InternalQueryInterface(r,p);
}
STDMETHODIMP_(ULONG) AddRef(void)
{
return This()->InternalAddRef();
}
STDMETHODIMP_(ULONG) Release(void)
{
return This()->InternalRelease();
}
};
XNDUnknown m_innerUnknown;
// composite instance
// экземпляр композита };
Двоичное размещение этого объекта показано на рис. 4.8. Методы делегирования класса чрезвычайно просты:
STDMETHODIMP Car::QueryInterface(REFIID riid, void **ppv) { return m_pUnkOuter->QueryInterface(riid, ppv); }
STDMETHODIMP_(ULONG) Car::AddRef(void) { return m_pUnkOuter->AddRef(); }
STDMETHODIMP_(ULONG) Car::Release (void) { return m_pUnkOuter->Release(); }
Эти подпрограммы являются версиями, которые будут заполнять таблицы vtbl всех интерфейсов объекта, так что какой бы интерфейс клиент ни получил, методы IUnknown всегда передают функции основной идентификационной единице объекта.
Для полного охвата проблем, относящихся к унифицированию идентификации через границы компонентов, рассмотрим следующую простую реализацию ICar:
class Car : public ICar
{
LONG m_cRef; Car(void) : m_cRef(0) {} STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pn);
STDMETHODIMP Brake(void); };
STDMETHODIMP Car::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*>(this);
else if (riid == IID_ICar) *ppv = static_cast<ICar*>(this);
else return (*ppv = 0), E_NOINTERFACE;
((IUnknown*)*ppv)->AddRef();
return S_OK;
}
// car class object's IClassFactory::CreateInstance
// интерфейс IClassFactory::CreateInstance
// объекта класса car
STDMETHODIMP CarClass::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv)
{
Car *pCar = new Car;
if (*pCar) return (*ppv = 0), E_OUTOFMEMORY;
pCar->AddRef();
HRESULT hr = pCar->QueryInterface(riid, ppv);
pCar->Release(); return hr;
}
Этот класс просто использует фактические реализации QueryInterface, AddRef и Release.
Рассмотрим второй класс C++, который пытается использовать реализацию Car как двоичный композит:
class CarBoat : public IBoat
{
LONG m_cRef;
Unknown *m_pUnkCar;
CarBoat(void);
virtual ~CarBoat(void);
STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pn);
STDMETHODIMP Sink(void);
};
Для эмуляции композиции разработчику пришлось бы создать подобъект Car, а деструктору – освободить указатель на подобъект:
CarBoat::CarBoat (void) : m_cRef(0)
{
HRESULT hr = CoCreateInstance(CLSID_Car, 0, CLSCTX_ALL, IID_IUnknown, (void**)&m_pUnkCar);
assert(SUCCEEDED(hr));
}
CarBoat::~CarBoat(void)
{
if (m_pUnkCar) m_pUnkCar->Release();
}
Интересная проблема возникает в реализации QueryInterface:
STDMETHODIMP CarBoat::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*>(this);
else if (riid == IID_IBoat) *ppv = static_cast<IBoat*>(this);
else if (riid == IID_ICar)
// forward request…
// переадресовываем запрос…
return m_pUnkCar->QueryInterface(riid, ppv);
else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef();
return S_OK;
}
Поскольку объект Car не имеет понятия о том, что он является частью идентификационной единицы (identity) другого объекта, то он будет причиной неуспеха любых запросов QueryInterface для IBoat. Это означает, что
QI(IBoat)->ICar
пройдет успешно, а запрос
QI(QI(IBoat)->ICar)->IBoat
потерпит неудачу, так как полученная QueryInterface будет несимметричной. Вдобавок запросы QueryInterface о IUnknown через интерфейсные указатели ICar и IBoat вернут различные значения, а это означает, что будет идентифицировано два различных объекта. Из подобных нарушений протокола IUnknown следует, что объекты CarBoat попросту не являются действительными объектами СОМ.
Идея составления объекта из двоичных композитов звучит красиво. Действительно, Спецификация СОМ четко и подробно указывает, как реализовать эту идею в стандартной и предсказуемой манере. Технология выставления клиенту двоичного подкомпонента непосредственно через QueryInterface называется СОМ-агрегированием. СОМ-агрегирование является лишь набором правил, определяющих отношения между внешним объектом (агрегирующим) и внутренним (агрегируемым). СОМ-агрегирование – это просто набор правил IUnknown, позволяющих более чем одному двоичному компоненту фигурировать в качестве идентификационной единицы (identity) СОМ.
Агрегирование СОМ несомненно является главной движущей силой для повторного использования в СОМ. Намного проще приписывать объекту значения и использовать его методы в реализации методов других объектов. Только в редких случаях кто-то захочет выставлять интерфейсы другого объекта непосредственно клиенту как часть той же самой идентификационной единицы. Рассмотрим следующий сценарий:
class Handlebar : public IHandlebar { … };
class Wheel : public IWheel {};
class Bicycle : public IBicycle
{
IHandlebar * m_pHandlebar;
IWheel *m_pFrontWheel;
IWheel *m_pBackWheel;
}
Было бы опрометчиво для класса Вicycle объявлять интерфейсы IHandlebar (велосипедный руль) или IWheel (колесо) в собственном методе QueryInterface. QueryInterface зарезервирован для выражения отношений «является» (is-a), а велосипед (bicycle) очевидно не является колесом (wheel) или рулем (handlebar). Если разработчик Bicycle хотел бы обеспечить прямой доступ к этим сторонам объекта, то интерфейс IBicycle должен был бы иметь для этой цели аксессоры определенных свойств:
[object, uuid(753A8A60-A7FF-11d0-8C30-0080C73925BA)] interface IBicycle : IVehicle
{
HRESULT GetHandlebar([out,retval] IHandlebar **pph);
HRESULT GetWheels([out] IWheel **ppwFront, [out] IWheel **ppwBack);
}
Реализация Bicycle могла бы тогда просто возвращать указатели на свои подобъекты:
STDMETHODIMP Bicycle::GetHandlebar(IHandlebar **pph)
{
if (*pph = m_pHandlebar) (*pph)->AddRef();
return S_OK;
}
STDMETHODIMP Bicycle::GetWheels(IWheel **ppwFront, IWheel **ppwBack)
{
if (*ppwFront = m_pFrontWheel) (*ppwFront)->AddRef();
if (*ppwBack = m_pBackWheel) (*ppwBack)->AddRef();
return S_OK;
}
При использовании данной технологии клиент по-прежнему получает прямой доступ к подобъектам. Однако поскольку указатели получены через явные методы, а не через QueryInterface, то между различными компонентами не существует никаких идентификационных отношений.
Несмотря на этот пример, все же остаются сценарии, где желательно обеспечить реализацию интерфейса, которая могла бы быть внедрена в идентификационную единицу другого объекта. Чтобы осуществить это, в СОМ-агрегировании требуется, чтобы внутренний объект (агрегируемый) уведомлялся во время его создания, что он создается как часть идентификационной единицы другого объекта. Это означает, что создающая функция (creation function), обычно используемая для создания объекта, требует один дополнительный параметр: указатель IUnknown на идентификационную единицу, которой агрегирующий объект должен передать функции в ее методы QueryInterface, AddRef и Release. Покажем определение метода CreateInstance интерфейса IClassFactory:
HRESULT CreateInstance([in] Unknown *pUnkOuter, [in] REFIID riid, [out, iid_is(riid)] void **ppv);
Этот метод (и соответствующие API-функции CoCreateInstanceEx и CoCreateInstance) перегружен с целью поддержки создания автономных (stand-alone ) объектов и агрегатов. Если вызывающий объект передает нулевой указатель и качестве первого параметра CreateInstance (pUnkOuter ), то результирующий объект будет автономной идентификационной единицей самого себя. Если же вызывающий объект передает в качестве первого параметра ненулевой указатель, то результирующий объект будет агрегатом с идентификационной единицей, ссылка на которую содержится в pUnkOuter. В случае агрегации агрегат должен переадресовывать все запросы QueryInterface, AddRef и Release непосредственно и безусловно на pUnkOuter. Это необходимо для обеспечения идентификации объекта.
Имея прототип функции, приведенный выше, класс CarBoat после небольшой модификации будет удовлетворять правилам агрегации:
CarBoat::CarBoat(void) : m_cRef(0)
{
// need to pass identity of self to Create routine
// to notify car object it 1s an aggregate
// нужно передать свою идентификацию подпрограмме
// Create для уведомления объекта car, что он – агрегат
HRESULT hr = CoCreateInstance(CLSID_Car, this, CLSCTX_ALL, IID_IUnknown, (void**)&m_pUnkCar);
assert(SUCCEEDED(hr));
}
Реализация CarBoat QueryInterface просто переадресовывает запрос ICar внутреннему агрегату:
STDMETHODIMP CarBoat::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
else if (riid == IID_ICar)
// forward request…
// переадресовываем запрос…
return m_pUnkCar->QueryInterface(riid, ppv);
else if (riid == IID_IBoat)
:
:
:
Теоретически это должно работать, так как агрегат будет всегда переадресовывать любые последующие запросы QueryInterface обратно главному объекту, проводя таким образом идентификацию объекта.
В предыдущем сценарии метод CreateInstance класса Car возвращает внешнему объекту указатель интерфейса, наследующего IUnknown. Если бы этот интерфейсный указатель должен был просто делегировать вызовы функций интерфейсу IUnknown внешнего объекта, то невозможно было бы: 1) уведомить агрегат, что он больше не нужен; 2) запросить интерфейсные указатели при выделении их клиентам главного объекта. На деле результатом приведенной выше реализации QueryInterface будет бесконечный цикл, поскольку внешний объект делегирует функции внутреннему, который делегирует их обратно внешнему.
Для решения этой проблемы необходимо сделать так, чтобы начальный интерфейсный указатель, который возвращается внешнему объекту, не делегировал вызовы реализации IUnknown внешнего объекта. Это означает, что объекты, поддерживающие СОМ– агрегирование, должны иметь две реализации IUnknown. Делегирующая, то есть передающая функции, реализация переадресовывает все запросы QueryInterface, AddRef и Release внешней реализации. Это и есть реализация по умолчанию, на которую ссылаются таблицы vtbl всех объектов, и это именно та версия, которую видят внешние клиенты. Объект должен также иметь неделегирующую реализацию IUnknown, которая выставляется только агрегирующему внешнему объекту.
Имеется несколько возможностей обеспечить две различные реализации IUnknown от одного объекта. Самый прямой путь[1] – это использование композиции и элемента данных для реализации неделегирующих методов IUnknown. Ниже показана реализация Car, поддающаяся агрегации:
class Car : public ICar
{
LONG m_cRef;
IUnknown *m_pUnk0uter;
public: Car(IUnknown *pUnk0uter);
// non-delegating IUnknown methods
// неделегирующие методы
IUnknown STDMETHODIMP InternalQueryInterface(REFIID, void **);
STDMETHODIMP (ULONG) InternalAddRef(void);
STDMETHODIMP_(ULONG) InternalRelease(void);
// delegating IUnknown methods
// делегирующие методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void **);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(*long *pn);
STDMETHODIMP Brake(void);
// composite to map distinguished IUnknown vptr to
// non-delegating InternalXXX routines in main object
// композит для преобразования определенного vptr IUnknown
// в неделегирующие подпрограммы InternalXXX в главном
// объекте
class XNDUnknown : public IUnknown
{ Car* This()
{
return (Car*)((BYTE*)this – offsetof(Car, m_innerUnknown));
}
STDMETHODIMP QueryInterface(REFIID r, void**p)
{
return This()->InternalQueryInterface(r,p);
}
STDMETHODIMP_(ULONG) AddRef(void)
{
return This()->InternalAddRef();
}
STDMETHODIMP_(ULONG) Release(void)
{
return This()->InternalRelease();
}
};
XNDUnknown m_innerUnknown;
// composite instance
// экземпляр композита };
Двоичное размещение этого объекта показано на рис. 4.8. Методы делегирования класса чрезвычайно просты:
STDMETHODIMP Car::QueryInterface(REFIID riid, void **ppv) { return m_pUnkOuter->QueryInterface(riid, ppv); }
STDMETHODIMP_(ULONG) Car::AddRef(void) { return m_pUnkOuter->AddRef(); }
STDMETHODIMP_(ULONG) Car::Release (void) { return m_pUnkOuter->Release(); }
Эти подпрограммы являются версиями, которые будут заполнять таблицы vtbl всех интерфейсов объекта, так что какой бы интерфейс клиент ни получил, методы IUnknown всегда передают функции основной идентификационной единице объекта.