Страница:
Эта последняя методика может быть до некоторой степени смягчена, если разработчик объекта желает поупражняться с выражением спецификации СОМ «barring catastrophic failure» (за исключением катастрофического сбоя).
Эти ограничения не означают, что два объекта одного и того же класса реализации не могут давать различные ответы «да/нет» при запросе одного и того же интерфейса. Например, класс может реализовать показанные ранее интерфейсы ICar, IBoat и IPlane , но может разрешить только одному интерфейсу быть использованным в каком-то определенном объекте. Эти ограничения также не означают, что объект не может использовать постоянную или временную информацию для решения вопроса о том, дать ли исходное «да» или «нет» для данного интерфейса. В примере для класса, который разрешает только один из трех интерфейсов, следующая идиома была бы вполне допустимой:
class СВР : public ICar, public IPlane, public IBoat
{
enum TYPE { CAR, BOAT, PLANE, NONE };
TYPE m_type;
CBP(void) : m_type(NONE) { }
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
if (md == IID_ICar)
{
// 1st QI Initializes type of object
// первая QI инициализирует тип объекта
if (m_type == NONE) m_type = CAR;
// only satisfy request if this object is a car
// удовлетворяем запрос, только если данный объект
// является car (автомобилем)
if (m_type == CAR) *ppv = static_cast<ICar*>(this);
else return (*ppv = 0), E_NOINTERFACE;
}
else if (md == IID_IBoat)
{
// similar treatment for IBoat and IPlane
// IBoat и IPlane обрабатываются сходным образом
}
};
Из требования, чтобы множество поддерживаемых интерфейсов было статичным, следует простой вывод, что разработчикам объектов не разрешается создавать конструкции, состоящие из одного объекта, который дает два различных ответа «да/нет» на запрос определенного интерфейса. Одна из причин того, что иерархия типов объекта должна оставаться неизменной на всем протяжении своего жизненного цикла, состоит в том, что СОМ не гарантирует отправления всех клиентских запросов QueryInterface такому объекту в случае, когда к нему имеется удаленный доступ. Неизменность иерархии типов позволяет «заместителям» на стороне клиента (client-side proxies) кэшировать результаты QueryInterface во избежание чрезмерных обменов клиент-объект. Такая оптимизация очень важна для эффективности СОМ, но она разрушает конструкции, использующие QueryInterface для передачи динамической семантической информации вызывающему объекту.
Единственность и идентификация
Предыдущий раздел был посвящен запросам QueryInterface, которые представляют собой ответы типа «да/нет» вызывающим объектам. QueryInterface действительно возвращает S_OK (да) или E_NOINTERFACE (нет). Впрочем, когда QueryInterface возвращает S_OK, то он также возвращает объекту интерфейсный указатель. Для СОМ значение этого указателя чрезвычайно важно, так как оно позволяет клиентам определить, действительно ли на один и тот же объект указывают два интерфейсных указателя.
QueryInterface и IUnknown
Свойство рефлективности QueryInterface гарантирует, что любой интерфейсный указатель сможет удовлетворить запросы на IUnknown, поскольку все интерфейсные указатели неявно принадлежат к типу IUnknown. Спецификация СОМ имеет немного больше ограничений при описании результатов запросов QueryInterface именно на IUnknown. Объект не только должен отвечать «да» на запрос, он должен также возвращать в ответ на каждый запрос в точности одно и то же значение указателя. Это означает, что в следующем коде оба утверждения всегда должны быть верны:
void AssertSameObject(IUnknown *pUnk)
{
IUnknown *pUnk1 = 0,
*pUnk2 = 0;
HRESULT hr1 = pUnk->QueryInterface(IID_IUnknown, (void **)&pUnk1);
HRESULT hr2 = pUnk->QueryInterface(IID_IUnknown, (void **)&pUnk2);
// QueryInterface(IUnknown) must always succeed
// QueryInterface(IUnknown) должно всегда быть успешным
assert(SUCCEEDED(hr1) && SUCCEEDED(hr2));
// two requests for IUnknown must always yield the
// same pointer values
// два запроса на IUnknown должны всегда выдавать
// те же самые значения указателя
assert(pUnk1 == pUnk2);
pUnk1->Release();
pUnk2->Release();
}
Это требование позволяет клиентам сравнивать два любых указателя интерфейса для выяснения того, действительно ли они указывают на один и тот же объект.
bool IsSameObject(IUnknown *pUnk1, IUnknown *pUnk2)
{ assert(pUnk1 && pUnk2);
bool bResult = true;
if (pUnk1 != pUnk2)
{
HRESULT hr1, hr2; IUnknown *p1 = 0, *p2 = 0;
hr1 = pUnk1->QueryInterface(IID_IUnknown, (void **)&p1);
assert(SUCCEEDED(hr1));
hr2 = pUnk2->QueryInterface(IID_IUnknown, (void **)&p2);
assert(SUCCEEDED(hr2));
// compare the two pointer values, as these
// represent the identity of the object
// сравниваем значения двух указателей,
// так как они идентифицируют объект
bResult = (р1 == р2); p1->Release();
p2->Release();
}
return bResult;
}
В главе 5 будет рассмотрено, что понятие идентификации является фундаментальным принципом, так как он используется в архитектуре удаленного доступа СОМ с целью эффективно представлять интерфейсные указатели на объекты в сети.
Вооружившись знанием правил IUnknown, полезно исследовать реализацию объекта и убедиться в том, что она придерживается всех этих правил. Следующая реализация выставляет каждый из четырех интерфейсов средств транспорта и IUnknown:
class CarBoatPlane : public ICar, public IBoat, public IPlane
{
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);
// IBoat methods – методы
IBoat STDMETHODIMP Sink(void);
// IPlahe methods – методы
IPlane STDMETHODIMP TakeOff(void); };
Ниже приведена стандартная реализация QueryInterface в CarBoatPlane:
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<ICar*>(this);
else if (riid == IID_IVehicle) *ppv = static_cast<ICar*>(this);
else if (riid == IID_ICar) *ppv = static_cast<ICar*>(this);
else if (riid == IID_IBoat) *ppv = static_cast<IBoat*>(this);
else if (riid == IID_IPlane) *ppv = static_cast<IPlane*>(this);
else return (*ppv = 0), E_NOINTERFACE;
((IUnknown*)*ppv)->AddRef();
return S_OK;
}
Для того чтобы быть объектом СОМ, реализация CarBoatPlane QueryInterface должна полностью придерживаться правил IUnknown , приведенных в данной главе.
Класс CarBoatPlane выставляет интерфейсы только типа ICarIPlane, IBoat, IVehicle и IUnknown . Каждая таблица vtbl CarBoatPlane будет ссылаться на единственную реализацию QueryInterface, показанную выше. К каждому поддерживаемому интерфейсу можно обращаться через эту реализацию QueryInterface, так что невозможно найти два несимметричных интерфейса, то есть не существует двух интерфейсов A и B, для которых неверно следующее:
If QI(A)->B Then QI(QI(A)->B)->A
Если следовать той же логике, то поскольку все пять интерфейсов принадлежат к одной и той же реализации QueryInterface, не существует трех интерфейсов А, В и С , для которых неверно следующее:
If QI(QI(A)->B)->C Then QI(A)->C
Наконец, поскольку реализация QueryInterface всегда удовлетворяет запросы на пять возможных интерфейсных указателей, которые могут поддерживаться клиентом, то следующее утверждение должно быть верным для каждого из пяти поддерживаемых интерфейсов:
QI(A)->A
Поскольку из множественного наследования вытекает единственная реализация QueryInterface для всех интерфейсов объекта, в действительности очень трудно нарушить требования симметричности, транзитивности и рефлективности.
Реализация также корректно выполняет правило СОМ об идентификации, возвращая только одно значение указателя при запросе IUnknown:
if (riid == IID_IUnknown) *ppv = static_cast<ICar*>(this);
Если бы реализация QueryInterface возвращала различные указатели vptr для каждого запроса:
if (riid == IID_IUnknown)
{
int n = rand() % 3;
if (n == 0) *ppv = static_cast<ICar*>(this);
else if (n == 1) *ppv = static_cast<IBoat*>(this);
else if (n == 2) *ppv = static_cast<IPlane*>(this);
}
то реализация была бы корректной только в терминах чисто С++-отношений типа (то есть все три интерфейса были бы совместимы по типу с запрошенным типом IUnknown). Эта реализация, однако, не является допустимой с точки зрения СОМ, поскольку правило идентификации для QueryInterface было нарушено.
void AssertSameObject(IUnknown *pUnk)
{
IUnknown *pUnk1 = 0,
*pUnk2 = 0;
HRESULT hr1 = pUnk->QueryInterface(IID_IUnknown, (void **)&pUnk1);
HRESULT hr2 = pUnk->QueryInterface(IID_IUnknown, (void **)&pUnk2);
// QueryInterface(IUnknown) must always succeed
// QueryInterface(IUnknown) должно всегда быть успешным
assert(SUCCEEDED(hr1) && SUCCEEDED(hr2));
// two requests for IUnknown must always yield the
// same pointer values
// два запроса на IUnknown должны всегда выдавать
// те же самые значения указателя
assert(pUnk1 == pUnk2);
pUnk1->Release();
pUnk2->Release();
}
Это требование позволяет клиентам сравнивать два любых указателя интерфейса для выяснения того, действительно ли они указывают на один и тот же объект.
bool IsSameObject(IUnknown *pUnk1, IUnknown *pUnk2)
{ assert(pUnk1 && pUnk2);
bool bResult = true;
if (pUnk1 != pUnk2)
{
HRESULT hr1, hr2; IUnknown *p1 = 0, *p2 = 0;
hr1 = pUnk1->QueryInterface(IID_IUnknown, (void **)&p1);
assert(SUCCEEDED(hr1));
hr2 = pUnk2->QueryInterface(IID_IUnknown, (void **)&p2);
assert(SUCCEEDED(hr2));
// compare the two pointer values, as these
// represent the identity of the object
// сравниваем значения двух указателей,
// так как они идентифицируют объект
bResult = (р1 == р2); p1->Release();
p2->Release();
}
return bResult;
}
В главе 5 будет рассмотрено, что понятие идентификации является фундаментальным принципом, так как он используется в архитектуре удаленного доступа СОМ с целью эффективно представлять интерфейсные указатели на объекты в сети.
Вооружившись знанием правил IUnknown, полезно исследовать реализацию объекта и убедиться в том, что она придерживается всех этих правил. Следующая реализация выставляет каждый из четырех интерфейсов средств транспорта и IUnknown:
class CarBoatPlane : public ICar, public IBoat, public IPlane
{
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);
// IBoat methods – методы
IBoat STDMETHODIMP Sink(void);
// IPlahe methods – методы
IPlane STDMETHODIMP TakeOff(void); };
Ниже приведена стандартная реализация QueryInterface в CarBoatPlane:
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<ICar*>(this);
else if (riid == IID_IVehicle) *ppv = static_cast<ICar*>(this);
else if (riid == IID_ICar) *ppv = static_cast<ICar*>(this);
else if (riid == IID_IBoat) *ppv = static_cast<IBoat*>(this);
else if (riid == IID_IPlane) *ppv = static_cast<IPlane*>(this);
else return (*ppv = 0), E_NOINTERFACE;
((IUnknown*)*ppv)->AddRef();
return S_OK;
}
Для того чтобы быть объектом СОМ, реализация CarBoatPlane QueryInterface должна полностью придерживаться правил IUnknown , приведенных в данной главе.
Класс CarBoatPlane выставляет интерфейсы только типа ICarIPlane, IBoat, IVehicle и IUnknown . Каждая таблица vtbl CarBoatPlane будет ссылаться на единственную реализацию QueryInterface, показанную выше. К каждому поддерживаемому интерфейсу можно обращаться через эту реализацию QueryInterface, так что невозможно найти два несимметричных интерфейса, то есть не существует двух интерфейсов A и B, для которых неверно следующее:
If QI(A)->B Then QI(QI(A)->B)->A
Если следовать той же логике, то поскольку все пять интерфейсов принадлежат к одной и той же реализации QueryInterface, не существует трех интерфейсов А, В и С , для которых неверно следующее:
If QI(QI(A)->B)->C Then QI(A)->C
Наконец, поскольку реализация QueryInterface всегда удовлетворяет запросы на пять возможных интерфейсных указателей, которые могут поддерживаться клиентом, то следующее утверждение должно быть верным для каждого из пяти поддерживаемых интерфейсов:
QI(A)->A
Поскольку из множественного наследования вытекает единственная реализация QueryInterface для всех интерфейсов объекта, в действительности очень трудно нарушить требования симметричности, транзитивности и рефлективности.
Реализация также корректно выполняет правило СОМ об идентификации, возвращая только одно значение указателя при запросе IUnknown:
if (riid == IID_IUnknown) *ppv = static_cast<ICar*>(this);
Если бы реализация QueryInterface возвращала различные указатели vptr для каждого запроса:
if (riid == IID_IUnknown)
{
int n = rand() % 3;
if (n == 0) *ppv = static_cast<ICar*>(this);
else if (n == 1) *ppv = static_cast<IBoat*>(this);
else if (n == 2) *ppv = static_cast<IPlane*>(this);
}
то реализация была бы корректной только в терминах чисто С++-отношений типа (то есть все три интерфейса были бы совместимы по типу с запрошенным типом IUnknown). Эта реализация, однако, не является допустимой с точки зрения СОМ, поскольку правило идентификации для QueryInterface было нарушено.
Множественные интерфейсы и имена методов
Множественное наследование является очень эффективной и простой технологией для реализации интерфейсов СОМ в классе C++. Это требует написания очень короткого явного кода, так как большая часть работы компилятора и компоновшика заключается в построении соответствующих СОМ указателей vptr и таблиц vtbl. Если имя метода появляется более чем в одном базовом классе с идентичными типами параметров, то компилятор и компоновщик заполняют каждый элемент vtbl таким образом, чтобы он указывал на одну реализацию метода в классе. Этот режим применяется к таким методам, как QueryInterface, AddRef и Release, так как все интерфейсы СОМ начинаются с этих методов, и все же разработчику класса требуется написать каждый метод только один раз (и это хорошо). Этот же режим применяется и к методам любых интерфейсов, где происходит повтор имени и сигнатуры. Здесь есть одна возможная ловушка множественного наследования.
Иерархия транспортных интерфейсов из этой главы содержит конфликт имен. В интерфейсе ICar (автомобиль) имеется метод, названный GetMaxSpeed (развить максимальную скорость). В интерфейсах IBoat (лодка) и IPlane (самолет) также имеются методы, именуемые GetMaxSpeed с идентичной сигнатурой. Это означает, что при использовании множественного наследования разработчик класса пишет метод GetMaxSpeed один раз, а компилятор и компоновщик инициализируют таблицы vtbl , совместимые с ICar, IBoat и IPlane так, чтобы они указывали только на эту реализацию.
Возможно, это вполне разумное поведение для большого числа реализации. Но что если объекту нужно было вернуть другую максимальную скорость, зависящую от интерфейса, на который был сделан запрос? Поскольку имя и сигнатуры одинаковы, то необходимо принимать неординарные меры для разрешения множественных реализации конфликтного метода. Один из возможных способов состоит в создании промежуточного класса C++, производного от интерфейса и реализующего конфликтный метод путем создания чисто виртуального вызова неконфликтного имени:
struct IXCar : public ICar {
// add new non-clashing method as pure virtual
// добавляем новый неконфликтный метод как чисто виртуальный
virtual HRESULT STDMETHODCALLTYPE GetMaxCarSpeed(long *pval) = 0;
// implement clashing method by upcalling
// non-clashing implementation in derived class
// реализуем конфликтный метод путем вызова
// неконфликтной реализации в производном классе
STDMETHODIMP GetMaxSpeed(long *pval)
{ return GetMaxCarSpeed(pval); }
};
Допуская, что интерфейсы IBoat и IPlane подвергнуты подобной операции, можно реализовывать различные версии GetMaxSpeed простым наследованием от расширенных версий интерфейсов и переопределением неконфликтных версий каждого метода GetMaxSpeed:
class CarBoatPlane: public IXCar, public IXBoat, public IXPlane
{
public:
// Unknown methods – методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
// IVehicle methods – методы IVehicle
// do not override GetMaxSpeed!
// не подменяем GetMaxSpeed!
// ICar methods – методы ICar
STDMETHODIMP Brake(void);
// IBoat methods – методы IBoat
STDMETHODIMP Sink(void);
// IXPlane methods – методы IXPlane
STDMETHODIMP TakeOff(void);
// upcalled from IXCar::GetMaxSpeed
// вызвано из IXCar::GetMaxSpeed
STDMETHODIMP GetMaxCarSpeed(long *pval);
// upcalled from IXBoat::GetMaxSpeed
// вызвано из IXBoat::GetMaxSpeed
STDMETHODIMP GetMaxBoatSpeed(long *pval);
// called from IXPlane::GetMaxSpeed
// вызвано из IXPlane::GetMaxSpeed
STDMETHODIMP GetMaxPlaneSpeed(long *pval);
}
Рисунок 4.6 иллюстрирует представление этого класса и форматы таблиц vtbl. Отметим, что конфликтный метод GetMaxSpeed не реализован в этом классе. Поскольку каждый из базовых классов CarBoatPlane подменяет этот чисто виртуальный метод, то CarBoatPlane не нуждается в создании своей собственной реализации. Действительно, если бы в CarBoatPlane нужно было подменить GetMaxSpeed, то одна его реализация этого метода подменила бы версии, вызываемые из каждого базового класса, аннулировав результат использования IXCar, IXBoat и IXPlane. В силу этой проблемы данная технология годится только в тех ситуациях, когда можно быть уверенным, что класс реализации (или любые возможные производные классы) никогда не станет подменять конфликтный метод.
Другой способ обеспечения множественных реализации конфликтных методов состоит в том, чтобы усилить правила IUnknown . Спецификация СОМ не требует, чтобы объект был реализован как класс C++. Хотя существует весьма естественное соответствие между объектами СОМ и классами C++, базирующимися на множественном наследовании, это всего лишь одна из возможных технологий реализации. Для создания объекта СОМ может быть использована любая программная технология, производящая таблицы vtbl в нужном формате и удовлетворяющая правилам СОМ для QueryInterface. Один стандартный метод разрешения конфликтов имен состоит в реализации интерфейсов с конфликтующими именами как отдельных классов C++ и последующей компоновке целевого класса C++ из экземпляров этих отдельных классов. Для гарантии того, что каждый из этих составных элементов данных появится во внешнем мире как единый объект СОМ, часто назначается одна главная реализация QueryInterface, которой каждый составной элемент данных будет передавать функции. Следующий код демонстрирует эту технологию:
class CarPlane
{
LONG m_cRef;
CarPlane(void) : m_cRef(0) {}
public:
// Main IUnknown methods
// Главные методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
private:
// define nested class that implements ICar
// определяем вложенный класс, реализующий
ICar struct XCar : public ICar
{
// get back pointer to main object
// получаем обратный указатель на главный объект
inline CarPlane* This();
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP Brake(void);
};
// define nested class that implements IPlane
// определяем вложенный класс, реализующий IPlane
struct XPlane : public IPlane {
// Get back pointer to main object
// получаем обратный указатель на главный объект
inline CarPlane* This();
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP TakeOff(void);
};
// declare instances of nested classes
// объявляем экземпляры вложенных классов
XCar m_xCar;
XPlane m_xPlane;
};
Использование вложенных классов не является обязательным, но оно подчеркивает, что эти подчиненные классы не имеют смысла вне контекста класса CarPlane. Рисунок 4.7 показывает двоичное размещение этого класса и размещения соответствующих vtbl .
Отметим, что имеется два определения вложенного класса, по одному для каждого реализованного им интерфейса. Это позволяет разработчику объекта обеспечить две различных реализации GetMaxSpeed:
STDMETHODIMP CarPlane::XCar::GetMaxSpeed(long *pn) {
// set *pn to max speed for cars
// устанавливаем *pn для максимальной скорости автомобилей
}
STDMETHODIMP CarPlane::XPlane::GetMaxSpeed(long *pn) {
// set *pn to max speed for planes
// устанавливаем *pn для максимальной скорости самолетов
}
Тот факт, что две реализации GetMaxSpeed встречаются в различных определениях вложенных классов, позволяет определить метод дважды и к тому же гарантирует то, что таблицы vtbl, соответствующие ICar и IPlane, будут иметь различные элементы для GetMaxSpeed.
Необходимо также отметить, что хотя класс CarPlane, находящийся на верхнем уровне, реализует методы IUnknown, он не наследует никакому производному от IUnknown классу. Вместо этого объекты CarPlane имеют элементы данных, которые наследуют интерфейсам СОМ. Это значит, что вместо того, чтобы использовать static_cast для вхождения в объект и нахождения определенного указателя vptr, реализация QueryInterface в CarPlane должна возвратить указатель на тот элемент данных, который реализует запрашиваемый интерфейс:
STDMETHODIMP CarPlane::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(&m_xCar);
else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*> (&m_xCar);
else if (riid == IID_ICar) *ppv = static_cast<ICar*>(&m_xCar);
else if (riid == IID_IPlane) *ppv = static_cast<IPlane*>(&m_xPlane);
else return (*ppv = 0), E_NOINTERFACE;
((IUnknown*)(*ppv))->AddRef();
return S_OK;
}
Для обеспечения идентификации объекта каждый из элементов данных CarPlane должен или воспроизвести этот код в своей собственной реализации QueryInterface, или просто передать управление главной функции QueryInterface в CarPlane. Чтобы осуществить это, необходим механизм перехода к главному объекту со стороны функции-члена составного элемента данных. Определение класса CarPlane::XCar содержит встроенную подпрограмму, которая использует фиксированные смещения для вычисления указателя this главного объекта от указателя this составного элемента данных.
inline CarPlane CarPlane::XCar::This(void)
{
return (CarPlane*)((char*)this
// ptr to composite – указатель на композит – offsetof (CarPlane, m_xCar)); }
inline CarPlane CarPlane::XPlane::This(void)
{
return (CarPlane*)((char*)this
// ptr to composite – указатель на композит
– offsetof(CarPlane, m_xPlane));
}
Такая технология вычисления обратного указателя (back-pointer) компактна и чрезвычайно эффективна, так как не требует явных элементов данных для нахождения главного объекта внутри реализации метода элемента данных. При наличии таких алгоритмов вычисления обратного указателя реализация композитного QueryInterface становится тривиальной:
STDMETHODIMP CarPlane::XCar::QueryInterface(REFIID r, void**p)
{
return This()->QueryInterface(r, p);
}
STDMETHODIMP CarPlane::XPlane::QueryInterface(REFIID r, void**p)
{
return This()->QueryInterface(r, p);
}
Такая же передача this потребуется для AddRef и Release для получения обобщенного представления о времени жизни объекта в случае составных (композитных) элементов данных.
Технология, основанная на использовании композиции для реализации интерфейсов, требует значительно больше кода, чем при простом множественном наследовании. Кроме того, качество генерированного кода, вероятно, не лучше (а возможно, и хуже), чем в случае множественного наследования. Из того факта, что классу CarPlane не понадобилось наследовать ни одному интерфейсу СОМ, следует, что композиция является разумной технологией для внесения СОМ в старые библиотеки классов. Например, MFC (Microsoft Foundation Classes – библиотека базовых классов Microsoft) использует эту технологию. Причиной применения композиции при реализации новых классов является получение отдельных реализации метода, определенного одинаково более чем в одном интерфейсе. К счастью, стандартные интерфейсы, определяемые СОМ, очень редко создают такие конфликты, а те немногие, которые создают, почти всегда преобразуются в семантически эквивалентные функции. Для разрешения коллизий, подобных тем, что произошли в сценарии с GetMaxSpeed , композиция, вероятно, и не требуется, так как в первом приближении для преобразования двойников в уникальные объекты достаточно использования промежуточных классов. Эта методика проста, эффективна и фактически не требует дополнительного кода. Основная причина использования композиции в новом коде заключается в том, что нужно обеспечить подсчет ссылок в каждом интерфейсе.
Иногда желательно разместить ресурсы в объекте на базе уже использующихся интерфейсов. В то же время из использования множественного наследования для реализации интерфейсов СОМ следует, что в каждой таблице vtbl будет использована только одна реализация AddRef и Release. Хотя можно выявить первый запрос на заданный интерфейс и разместить ресурсы по требованию:
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IBoat)
{
// allocate resource the first time through
// размещаем ресурсы при первом проходе
if (m_pTonsOfMemory == 0) m_pTonsOfMemory = new char[4096 * 4096];
*ppv = static_cast<IBoat*>(this);
}
else if
…
}
не существует способа определить момент, когда больше нет внешних указателей интерфейса IBoat, так как вызов Release, который клиент делает через интерфейс IBoat, неотличим от вызова Release, сделанного через любой другой интерфейс объекта. В обычной ситуации именно это и нужно, но в данном случае вызовы AddRef и Release через интерфейсы IBoat необходимо рассматривать иначе. Если бы интерфейс IBoat был реализован с использованием композиции, то он имел бы свои собственные уникальные реализации AddRef и Release, в которых он мог бы поддерживать свой собственный счетчик ссылок, отличный от счетчика главного объекта:
class CarBoatPlane : public ICar, public IPlane
{
LONG m_cRef;
char *m_pTonsOfMemory;
CarBoatPlane (void) : m_cRef(0),
m_pTonsOfMemory (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);
// IPlane methods – методы IPlane
STDMETHODIMP TakeOff(void);
// define nested class that implements IBoat
// определяем вложенный класс, реализующий IBoat
struct XBoat : public IBoat {
// get back pointer to main object
// получаем обратный указатель на главный объект
inline CarBoatPlane* This();
LONG m_cBoatRef;
// per-interface ref count
// счетчик ссылок на каждый интерфейс
XBoat(void) : m_cBoatRef(0) {}
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP Sink(void);
};
XBoat m_xBoat; };
Реализация AddRef и Release из IBoat могут теперь следить за числом ссылок типа IBoat и высвободить ресурсы, когда они больше не нужны:
STDMETHODIMP_(ULONG) CarBoatPlane::XBoat::AddRef()
{
ULONG res = InterlockedIncrement(&m_cBoatRef);
if (res == 1)
{
// first AddRef – первый AddRef
// allocate resource and forward AddRef to object
// размещаем ресурсы и пересылаем AddRef на объект
This()->m_pTonsOfMemory = new char[4096*4096];
This()->AddRef(); }
return res; }
STDMETHODIMP_(ULONG) CarBoatPlane::XBoat::Release()
{
ULONG res = InterlockedDecrement(&m_cBoatRef);
if (res == 0) {
// last Release – последний Release
// free resource and forward Release to object
// освобождаем ресурсы и пересылаем Release на объект
delete [] This()->m_pTonsOfMemory;
This()->Release();
} return res; }
Чтобы эта методика работала, все пользующиеся интерфейсными указателями должны придерживаться требований спецификации СОМ: функция Release должна вызываться через указатель, посредством которого вызывается соответствующая функция AddRef. Поэтому правильной концовкой QueryInterface будет следующая:
((IUnknown*)(*ppv))->AddRef();
// use exact ptr
// используем точный указатель return S_OK;
вместо такого:
AddRef();
// just call this->AddRef
// только вызов
this->AddRef return S_OK;
Первый вариант гарантирует, что если клиент пишет следующий правильный код
IBoat *pBoat = 0;
HRESULT hr = pUnk->QueryInterface(IID_IBoat, (void**)&pBoat);
if (SUCCEEDED(hr))
{ hr = pBoat->Sink(); pBoat->Release(); }
то для AddRef и для Release обязательно будет использовано одно и то же значение указателя.
Можно осуществлять композицию в контексте управляемой таблицами реализации QueryInterface. При наличии семейства макросов препроцессора, показанного в предыдущей главе, достаточно всего одного дополнительного макроса, чтобы определить, что вместо базового класса используется элемент данных, и второго макроса, чтобы реализовать методы IUnknown в композите:
class CarBoatPlane : public ICar, public IPlane
{ public: struct XBoat : public IBoat
{
// composite QI/AddRef/Release/This()
// композит из QI/AddRef/Release/This()
IMPLEMENT_COMPOSITE_UNKNOWN(CarBoatPlane, XBoat, m_xBoat) STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP Sink(void);
};
XBoat m_xBoat;
// IVehicle methods
// методы IVehicle
STDMETHODIMP GetMaxSpeed(long *pMax);
// ICar methods
// методы ICar
STDMETHODIMP Brake(void);
// IPlane methods
// методы IPlane
STDMETHODIMP TakeOff(void);
// standard heap-based QI/AddRef/Release
// стандартные расположенные в «куче» QI/AddRef/Release
IMPLEMENT_UNKNOWN(CarBoatPlane)
BEGIN_INTERFACE_TABLE(CarBoatPlane)
IMPLEMENTS_INTERFACE_AS(IVehicle, ICar)
IMPLEMENTS_INTERFACE(ICar)
IMPLEMENTS_INTERFACE(IPlane)
// macro that calculates offset of data member
// макрос, вычисляющий смещение элемента данных
IMPLEMENTS_INTERFACE_WITH_COMPOSITE(IBoat, XBoat, m_xBoat)
END_INTERFACE_TABLE() };
В приведенном выше определении класса опущены только определения методов объекта вне QueryInterfасе, AddRef и Release. Два новых макроса, использованных в определении класса, определяются следующим образом:
// inttable.h
// (book-specific header file)
// (заголовочный файл, специфический для данной книги)
#define COMPOSITE_OFFSET(ClassName, BaseName, \
MemberType, MemberName) \
(DWORD(static_cast<BaseName*>(\
reinterpret_cast<MemberType*>(0x10000000 + \
offsetof(ClassName, MemberName)))) – 0х10000000)
#define IMPLEMENTS_INTERFACE_WITH_COMPOSITE(Req,\
MemberType, MemberName) \
{ &IID_##Req,ENTRY_IS_OFFSET, COMPOSITE_OFFSET(_IT,\
Req, MemberType, MemberName) },
// impunk.h
// (book-specific header file)
// (заголовочный файл, специфический для данной книги)
#define IMPLEMENT_COMPOSITE_UNKNOWN(OuterClassName,\
InnerClassName, DataMemberName) \
OuterClassName *This() \
{ return (OuterClassName*)((char*)this – \
offsetof(OuterClassName, DataMemberName)); }\
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)\
{ return This()->QueryInterface(riid, ppv); }\
STDMETHODIMP_(ULONG) AddRef(void) \
{ return This()->AddRef(); }\
STDMETHODIMP_(ULONG) Release(void) \
{ return This()->Release(); }
Эти макросы препроцессора просто дублируют фактические реализации QueryInterface, AddRef и Release , использованные в композиции.
Иерархия транспортных интерфейсов из этой главы содержит конфликт имен. В интерфейсе ICar (автомобиль) имеется метод, названный GetMaxSpeed (развить максимальную скорость). В интерфейсах IBoat (лодка) и IPlane (самолет) также имеются методы, именуемые GetMaxSpeed с идентичной сигнатурой. Это означает, что при использовании множественного наследования разработчик класса пишет метод GetMaxSpeed один раз, а компилятор и компоновщик инициализируют таблицы vtbl , совместимые с ICar, IBoat и IPlane так, чтобы они указывали только на эту реализацию.
Возможно, это вполне разумное поведение для большого числа реализации. Но что если объекту нужно было вернуть другую максимальную скорость, зависящую от интерфейса, на который был сделан запрос? Поскольку имя и сигнатуры одинаковы, то необходимо принимать неординарные меры для разрешения множественных реализации конфликтного метода. Один из возможных способов состоит в создании промежуточного класса C++, производного от интерфейса и реализующего конфликтный метод путем создания чисто виртуального вызова неконфликтного имени:
struct IXCar : public ICar {
// add new non-clashing method as pure virtual
// добавляем новый неконфликтный метод как чисто виртуальный
virtual HRESULT STDMETHODCALLTYPE GetMaxCarSpeed(long *pval) = 0;
// implement clashing method by upcalling
// non-clashing implementation in derived class
// реализуем конфликтный метод путем вызова
// неконфликтной реализации в производном классе
STDMETHODIMP GetMaxSpeed(long *pval)
{ return GetMaxCarSpeed(pval); }
};
Допуская, что интерфейсы IBoat и IPlane подвергнуты подобной операции, можно реализовывать различные версии GetMaxSpeed простым наследованием от расширенных версий интерфейсов и переопределением неконфликтных версий каждого метода GetMaxSpeed:
class CarBoatPlane: public IXCar, public IXBoat, public IXPlane
{
public:
// Unknown methods – методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
// IVehicle methods – методы IVehicle
// do not override GetMaxSpeed!
// не подменяем GetMaxSpeed!
// ICar methods – методы ICar
STDMETHODIMP Brake(void);
// IBoat methods – методы IBoat
STDMETHODIMP Sink(void);
// IXPlane methods – методы IXPlane
STDMETHODIMP TakeOff(void);
// upcalled from IXCar::GetMaxSpeed
// вызвано из IXCar::GetMaxSpeed
STDMETHODIMP GetMaxCarSpeed(long *pval);
// upcalled from IXBoat::GetMaxSpeed
// вызвано из IXBoat::GetMaxSpeed
STDMETHODIMP GetMaxBoatSpeed(long *pval);
// called from IXPlane::GetMaxSpeed
// вызвано из IXPlane::GetMaxSpeed
STDMETHODIMP GetMaxPlaneSpeed(long *pval);
}
Рисунок 4.6 иллюстрирует представление этого класса и форматы таблиц vtbl. Отметим, что конфликтный метод GetMaxSpeed не реализован в этом классе. Поскольку каждый из базовых классов CarBoatPlane подменяет этот чисто виртуальный метод, то CarBoatPlane не нуждается в создании своей собственной реализации. Действительно, если бы в CarBoatPlane нужно было подменить GetMaxSpeed, то одна его реализация этого метода подменила бы версии, вызываемые из каждого базового класса, аннулировав результат использования IXCar, IXBoat и IXPlane. В силу этой проблемы данная технология годится только в тех ситуациях, когда можно быть уверенным, что класс реализации (или любые возможные производные классы) никогда не станет подменять конфликтный метод.
Другой способ обеспечения множественных реализации конфликтных методов состоит в том, чтобы усилить правила IUnknown . Спецификация СОМ не требует, чтобы объект был реализован как класс C++. Хотя существует весьма естественное соответствие между объектами СОМ и классами C++, базирующимися на множественном наследовании, это всего лишь одна из возможных технологий реализации. Для создания объекта СОМ может быть использована любая программная технология, производящая таблицы vtbl в нужном формате и удовлетворяющая правилам СОМ для QueryInterface. Один стандартный метод разрешения конфликтов имен состоит в реализации интерфейсов с конфликтующими именами как отдельных классов C++ и последующей компоновке целевого класса C++ из экземпляров этих отдельных классов. Для гарантии того, что каждый из этих составных элементов данных появится во внешнем мире как единый объект СОМ, часто назначается одна главная реализация QueryInterface, которой каждый составной элемент данных будет передавать функции. Следующий код демонстрирует эту технологию:
class CarPlane
{
LONG m_cRef;
CarPlane(void) : m_cRef(0) {}
public:
// Main IUnknown methods
// Главные методы IUnknown
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
private:
// define nested class that implements ICar
// определяем вложенный класс, реализующий
ICar struct XCar : public ICar
{
// get back pointer to main object
// получаем обратный указатель на главный объект
inline CarPlane* This();
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP Brake(void);
};
// define nested class that implements IPlane
// определяем вложенный класс, реализующий IPlane
struct XPlane : public IPlane {
// Get back pointer to main object
// получаем обратный указатель на главный объект
inline CarPlane* This();
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP TakeOff(void);
};
// declare instances of nested classes
// объявляем экземпляры вложенных классов
XCar m_xCar;
XPlane m_xPlane;
};
Использование вложенных классов не является обязательным, но оно подчеркивает, что эти подчиненные классы не имеют смысла вне контекста класса CarPlane. Рисунок 4.7 показывает двоичное размещение этого класса и размещения соответствующих vtbl .
Отметим, что имеется два определения вложенного класса, по одному для каждого реализованного им интерфейса. Это позволяет разработчику объекта обеспечить две различных реализации GetMaxSpeed:
STDMETHODIMP CarPlane::XCar::GetMaxSpeed(long *pn) {
// set *pn to max speed for cars
// устанавливаем *pn для максимальной скорости автомобилей
}
STDMETHODIMP CarPlane::XPlane::GetMaxSpeed(long *pn) {
// set *pn to max speed for planes
// устанавливаем *pn для максимальной скорости самолетов
}
Тот факт, что две реализации GetMaxSpeed встречаются в различных определениях вложенных классов, позволяет определить метод дважды и к тому же гарантирует то, что таблицы vtbl, соответствующие ICar и IPlane, будут иметь различные элементы для GetMaxSpeed.
Необходимо также отметить, что хотя класс CarPlane, находящийся на верхнем уровне, реализует методы IUnknown, он не наследует никакому производному от IUnknown классу. Вместо этого объекты CarPlane имеют элементы данных, которые наследуют интерфейсам СОМ. Это значит, что вместо того, чтобы использовать static_cast для вхождения в объект и нахождения определенного указателя vptr, реализация QueryInterface в CarPlane должна возвратить указатель на тот элемент данных, который реализует запрашиваемый интерфейс:
STDMETHODIMP CarPlane::QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(&m_xCar);
else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*> (&m_xCar);
else if (riid == IID_ICar) *ppv = static_cast<ICar*>(&m_xCar);
else if (riid == IID_IPlane) *ppv = static_cast<IPlane*>(&m_xPlane);
else return (*ppv = 0), E_NOINTERFACE;
((IUnknown*)(*ppv))->AddRef();
return S_OK;
}
Для обеспечения идентификации объекта каждый из элементов данных CarPlane должен или воспроизвести этот код в своей собственной реализации QueryInterface, или просто передать управление главной функции QueryInterface в CarPlane. Чтобы осуществить это, необходим механизм перехода к главному объекту со стороны функции-члена составного элемента данных. Определение класса CarPlane::XCar содержит встроенную подпрограмму, которая использует фиксированные смещения для вычисления указателя this главного объекта от указателя this составного элемента данных.
inline CarPlane CarPlane::XCar::This(void)
{
return (CarPlane*)((char*)this
// ptr to composite – указатель на композит – offsetof (CarPlane, m_xCar)); }
inline CarPlane CarPlane::XPlane::This(void)
{
return (CarPlane*)((char*)this
// ptr to composite – указатель на композит
– offsetof(CarPlane, m_xPlane));
}
Такая технология вычисления обратного указателя (back-pointer) компактна и чрезвычайно эффективна, так как не требует явных элементов данных для нахождения главного объекта внутри реализации метода элемента данных. При наличии таких алгоритмов вычисления обратного указателя реализация композитного QueryInterface становится тривиальной:
STDMETHODIMP CarPlane::XCar::QueryInterface(REFIID r, void**p)
{
return This()->QueryInterface(r, p);
}
STDMETHODIMP CarPlane::XPlane::QueryInterface(REFIID r, void**p)
{
return This()->QueryInterface(r, p);
}
Такая же передача this потребуется для AddRef и Release для получения обобщенного представления о времени жизни объекта в случае составных (композитных) элементов данных.
Технология, основанная на использовании композиции для реализации интерфейсов, требует значительно больше кода, чем при простом множественном наследовании. Кроме того, качество генерированного кода, вероятно, не лучше (а возможно, и хуже), чем в случае множественного наследования. Из того факта, что классу CarPlane не понадобилось наследовать ни одному интерфейсу СОМ, следует, что композиция является разумной технологией для внесения СОМ в старые библиотеки классов. Например, MFC (Microsoft Foundation Classes – библиотека базовых классов Microsoft) использует эту технологию. Причиной применения композиции при реализации новых классов является получение отдельных реализации метода, определенного одинаково более чем в одном интерфейсе. К счастью, стандартные интерфейсы, определяемые СОМ, очень редко создают такие конфликты, а те немногие, которые создают, почти всегда преобразуются в семантически эквивалентные функции. Для разрешения коллизий, подобных тем, что произошли в сценарии с GetMaxSpeed , композиция, вероятно, и не требуется, так как в первом приближении для преобразования двойников в уникальные объекты достаточно использования промежуточных классов. Эта методика проста, эффективна и фактически не требует дополнительного кода. Основная причина использования композиции в новом коде заключается в том, что нужно обеспечить подсчет ссылок в каждом интерфейсе.
Иногда желательно разместить ресурсы в объекте на базе уже использующихся интерфейсов. В то же время из использования множественного наследования для реализации интерфейсов СОМ следует, что в каждой таблице vtbl будет использована только одна реализация AddRef и Release. Хотя можно выявить первый запрос на заданный интерфейс и разместить ресурсы по требованию:
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
if (riid == IID_IBoat)
{
// allocate resource the first time through
// размещаем ресурсы при первом проходе
if (m_pTonsOfMemory == 0) m_pTonsOfMemory = new char[4096 * 4096];
*ppv = static_cast<IBoat*>(this);
}
else if
…
}
не существует способа определить момент, когда больше нет внешних указателей интерфейса IBoat, так как вызов Release, который клиент делает через интерфейс IBoat, неотличим от вызова Release, сделанного через любой другой интерфейс объекта. В обычной ситуации именно это и нужно, но в данном случае вызовы AddRef и Release через интерфейсы IBoat необходимо рассматривать иначе. Если бы интерфейс IBoat был реализован с использованием композиции, то он имел бы свои собственные уникальные реализации AddRef и Release, в которых он мог бы поддерживать свой собственный счетчик ссылок, отличный от счетчика главного объекта:
class CarBoatPlane : public ICar, public IPlane
{
LONG m_cRef;
char *m_pTonsOfMemory;
CarBoatPlane (void) : m_cRef(0),
m_pTonsOfMemory (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);
// IPlane methods – методы IPlane
STDMETHODIMP TakeOff(void);
// define nested class that implements IBoat
// определяем вложенный класс, реализующий IBoat
struct XBoat : public IBoat {
// get back pointer to main object
// получаем обратный указатель на главный объект
inline CarBoatPlane* This();
LONG m_cBoatRef;
// per-interface ref count
// счетчик ссылок на каждый интерфейс
XBoat(void) : m_cBoatRef(0) {}
STDMETHODIMP QueryInterface(REFIID, void**);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP Sink(void);
};
XBoat m_xBoat; };
Реализация AddRef и Release из IBoat могут теперь следить за числом ссылок типа IBoat и высвободить ресурсы, когда они больше не нужны:
STDMETHODIMP_(ULONG) CarBoatPlane::XBoat::AddRef()
{
ULONG res = InterlockedIncrement(&m_cBoatRef);
if (res == 1)
{
// first AddRef – первый AddRef
// allocate resource and forward AddRef to object
// размещаем ресурсы и пересылаем AddRef на объект
This()->m_pTonsOfMemory = new char[4096*4096];
This()->AddRef(); }
return res; }
STDMETHODIMP_(ULONG) CarBoatPlane::XBoat::Release()
{
ULONG res = InterlockedDecrement(&m_cBoatRef);
if (res == 0) {
// last Release – последний Release
// free resource and forward Release to object
// освобождаем ресурсы и пересылаем Release на объект
delete [] This()->m_pTonsOfMemory;
This()->Release();
} return res; }
Чтобы эта методика работала, все пользующиеся интерфейсными указателями должны придерживаться требований спецификации СОМ: функция Release должна вызываться через указатель, посредством которого вызывается соответствующая функция AddRef. Поэтому правильной концовкой QueryInterface будет следующая:
((IUnknown*)(*ppv))->AddRef();
// use exact ptr
// используем точный указатель return S_OK;
вместо такого:
AddRef();
// just call this->AddRef
// только вызов
this->AddRef return S_OK;
Первый вариант гарантирует, что если клиент пишет следующий правильный код
IBoat *pBoat = 0;
HRESULT hr = pUnk->QueryInterface(IID_IBoat, (void**)&pBoat);
if (SUCCEEDED(hr))
{ hr = pBoat->Sink(); pBoat->Release(); }
то для AddRef и для Release обязательно будет использовано одно и то же значение указателя.
Можно осуществлять композицию в контексте управляемой таблицами реализации QueryInterface. При наличии семейства макросов препроцессора, показанного в предыдущей главе, достаточно всего одного дополнительного макроса, чтобы определить, что вместо базового класса используется элемент данных, и второго макроса, чтобы реализовать методы IUnknown в композите:
class CarBoatPlane : public ICar, public IPlane
{ public: struct XBoat : public IBoat
{
// composite QI/AddRef/Release/This()
// композит из QI/AddRef/Release/This()
IMPLEMENT_COMPOSITE_UNKNOWN(CarBoatPlane, XBoat, m_xBoat) STDMETHODIMP GetMaxSpeed(long *pval);
STDMETHODIMP Sink(void);
};
XBoat m_xBoat;
// IVehicle methods
// методы IVehicle
STDMETHODIMP GetMaxSpeed(long *pMax);
// ICar methods
// методы ICar
STDMETHODIMP Brake(void);
// IPlane methods
// методы IPlane
STDMETHODIMP TakeOff(void);
// standard heap-based QI/AddRef/Release
// стандартные расположенные в «куче» QI/AddRef/Release
IMPLEMENT_UNKNOWN(CarBoatPlane)
BEGIN_INTERFACE_TABLE(CarBoatPlane)
IMPLEMENTS_INTERFACE_AS(IVehicle, ICar)
IMPLEMENTS_INTERFACE(ICar)
IMPLEMENTS_INTERFACE(IPlane)
// macro that calculates offset of data member
// макрос, вычисляющий смещение элемента данных
IMPLEMENTS_INTERFACE_WITH_COMPOSITE(IBoat, XBoat, m_xBoat)
END_INTERFACE_TABLE() };
В приведенном выше определении класса опущены только определения методов объекта вне QueryInterfасе, AddRef и Release. Два новых макроса, использованных в определении класса, определяются следующим образом:
// inttable.h
// (book-specific header file)
// (заголовочный файл, специфический для данной книги)
#define COMPOSITE_OFFSET(ClassName, BaseName, \
MemberType, MemberName) \
(DWORD(static_cast<BaseName*>(\
reinterpret_cast<MemberType*>(0x10000000 + \
offsetof(ClassName, MemberName)))) – 0х10000000)
#define IMPLEMENTS_INTERFACE_WITH_COMPOSITE(Req,\
MemberType, MemberName) \
{ &IID_##Req,ENTRY_IS_OFFSET, COMPOSITE_OFFSET(_IT,\
Req, MemberType, MemberName) },
// impunk.h
// (book-specific header file)
// (заголовочный файл, специфический для данной книги)
#define IMPLEMENT_COMPOSITE_UNKNOWN(OuterClassName,\
InnerClassName, DataMemberName) \
OuterClassName *This() \
{ return (OuterClassName*)((char*)this – \
offsetof(OuterClassName, DataMemberName)); }\
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)\
{ return This()->QueryInterface(riid, ppv); }\
STDMETHODIMP_(ULONG) AddRef(void) \
{ return This()->AddRef(); }\
STDMETHODIMP_(ULONG) Release(void) \
{ return This()->Release(); }
Эти макросы препроцессора просто дублируют фактические реализации QueryInterface, AddRef и Release , использованные в композиции.
Динамическая композиция
Если для реализации интерфейса в классе C++ используется множественное наследование или композиция, то в каждом объекте этого класса будут содержаться служебные данные (overhead) указателя vptr размером в четыре байта на каждый поддерживаемый интерфейс (принимая, что sizeof (void*) == 4). Если число интерфейсов, экспортируемых объектом, невелико, то эти служебные данные не играют важной роли, особенно в свете преимуществ, предоставляемых программной моделью СОМ. Если, однако, число поддерживаемых интерфейсов велико, то размер служебных данных vptr может вырасти до такой степени, что часть объекта, не связанная с СОМ, будет казаться маленькой по сравнению с ними. При использовании каждого из этих интерфейсов все время без служебных данных не обойтись. Если же, однако, эти интерфейсы не будут использоваться никогда или использоваться в течение короткого времени, то можно воспользоваться лазейкой в Спецификации СОМ и оптимизировать vptr некоторых неиспользуемых объектов.