Хотя реализации QueryInterface из предыдущей главы являются вполне допустимыми для СОМ, правила IUnknown предоставляют разработчику объектов значительно больше гибкости, чем было показано до сих пор. В данной главе эти правила будут исследованы, и продемонстрированы способы реализации, которые из них вытекают.
Снова IUnknown
IUnknown не имеет реализации по умолчанию, которая являлась бы частью интерфейса системного вызова СОМ. Заголовочные файлы SDK не содержат базовых классов, макросов или шаблонов, предусматривающих реализации QueryInterface, AddRef и Release, которые должны использоваться во всех программах на С или C++. Вместо этого Спецификация СОМ (Component Object Model Specification) предоставляет очень точные правила относительно допущений, которые клиенты и объекты могут делать относительно этих трех методов. Этот набор правил формирует протокол IUnknown и позволяет каждому разработчику объекта преобразовать три указанных метода IUnknown во все, что имеет смысл для его или ее объекта.
В главе 2 представлены фактические С++-реализации трех упомянутых методов, но СОМ никоим образом не обязывает объекты использовать их. Все, что требует СОМ, – это чтобы каждая реализация придерживалась базовых правил IUnknown. Как это достигается, не имеет ни малейшего отношения к СОМ. Это делает СОМ совершенно ненавязчивой, так как эта модель не требует, чтобы объект делал системные вызовы, наследовал системным реализациям, а все, что от него требуется, – это объявлять совместимые с СОМ указатели vptr. На самом деле, как будет показано далее в этой главе, можно выставлять наследующие IUnknown указатели vptr из классов, которые не наследуют ни одному интерфейсу СОМ.
Правила IUnknown в совокупности определяют, что значит быть объектом СОМ. Чтобы понять правила IUnknown, полезно начать с конкретного примера. Рассмотрим следующую иерархию интерфейсов:
import «unknwn.idl»;
[object, uuid(CD538340-A56D-11d0-8C2F-0080C73925BA)]
interface IVehicle : IUnknown {
HRESULT GetMaxSpeed([out, retval] long *pMax); }
[object, uuid(CD53834l-A56D-11d0-8C2F-0080C73925BA)]
interface ICar : IVehicle {
HRESULT Brake(void); }
[object, uuid(CD538342-A56D-11d0-8C2F-0080C73925BA)]
interface IPlane : IVehicle {
HRESULT TakeOff(void); }
[object, uuid(CD538343-A56D-11d0-8C2F-0080C73925BA)]
interface IBoat : IVehicle {
HRESULT Sink(void); }
СОМ использует стандартную технологию для визуального представления объектов. Эта технология находится в рамках принципа СОМ отделения интерфейса от реализации и не раскрывает никаких деталей реализации объекта, кроме списка выставляемых им интерфейсов. Эта технология также визуально усиливает многие из правил IUnknown. Рисунок 4.1 показывает стандартное представление класса CarBoatPlane, который реализует все только что определенные интерфейсы. Заметим, что единственный вывод, который можно сделать из этого рисунка, таков: если не произойдет катастрофического сбоя, объекты CarBoatPlane будут выставлять пять интерфейсов: IBoat, IPlane, ICar, IVehicle и IUnknown.
Первое правило IUnknown, подлежащее исследованию, – это требование, чтобы QueryInterface был симметричным, транзитивным и рефлексивным (Symmetric/Transitive/Reflexive). Эти требования определяют отношения между всеми интерфейсными указателями объекта и начинают определять понятие идентификации (identity) объектов СОМ. Подобно всем правилам IUnknown, эти требования должны исполняться всегда, за исключением катастрофических сбоев, теми, кто хочет считаться действительным объектом СОМ.
QueryInterface симметрична
Спецификация СОМ требует, чтобы, если запрос QueryInterface на интерфейс B удовлетворяется через интерфейсный указатель типа A, то запрос QueryInterface на интерфейс A того же самого объекта через результирующий интерфейсный указатель типа В всегда был успешным. Это значит, что если верно QI(A)->B, то также должно быть верным QI(QI(A)->B)->A
Из свойства, показанного на рис. 4.2, следует, что утверждение, заключенное в следующем коде, всегда должно быть истинным:
void AssertSymmetric(ICar *pCar) { if (pCar)
{
IPlane *pPlane = 0;
// request a second type of interface
// запрашиваем второй тип интерфейса
HRESULT hr = pCar->QueryInterface(IID_IPlane, (void**)&pPlane);
if (SUCCEEDED(hr)) { ICar *pCar2 = 0;
// request original type of interface
// запрашиваем исходный тип интерфейса
hr = pPlane->QueryInterface(IID_ICar, (void**)&pCar2);
// if the following assertion fails, pCar
// did not point to a valid СОМ object
// если следующее утверждение не будет правильным,
// то pCar не укажет на правильный СОМ-объект
assert(SUCCEEDED(hr));
pCar2->Release();
}
pPlane->Release();
}
}
Симметричность QueryInterface означает, что клиенты не должны заботиться о том, какой из интерфейсов запрашивать первым, так как любые два типа интерфейсов могут быть запрошены в любом порядке.
QueryInterface транзитивна
Спецификация СОМ требует также, чтобы, если запрос QueryInterface на интерфейс В удовлетворяется через интерфейсный указатель типа A, а второй запрос QueryInterface на интерфейс C удовлетворяется через указатель типа В , то запрос QueryInterface на интерфейс C через исходный указатель типа A был бы также успешным. Это означает, что если верно QI(QI(A)->B)->C, то должно быть верным и QI(A)->C
Это условие иллюстрируется рис. 4.3 и означает, что утверждение, приведенное в нижеследующем коде, должно всегда быть верным:
void AssertTransitive(ICar *pCar)
{
if (pCar)
{
IPlane *pPlane = 0;
// request intermediate type of interface
// запрос промежуточного типа интерфейса
HRESULT hr = pCar->QueryInterface(IID_IPlane, (void**)&pPlane);
if (SUCCEEDED(hr))
{
IBoat *pBoat1 = 0;
// request terminal type of interface
// запрос конечного типа интерфейса
hr = pPlane->QueryInterface(IID_IBoat, (void**)&pBoat1);
if (SUCCEEDED(hr))
{
IBoat *pBoat2 = 0;
// request terminal type through the original pointer
// запрос конечного типа через исходный указатель
hr = pCar->QueryInterface(IID_IBoat, (void**)&pBoat2);
// if the following assertion fails, pCar
// did not point to a valid СОМ object
// если следующее утверждение неверно, то pCar
// не указывал на корректный СОМ-объект
assert(SUCCEEDED(hr));
pBoat2->Release();
}
pBoat1->Release();
}
pPlane->Release();
}
}
Из транзитивности QueryInterface следует, что все интерфейсы, которые выставляет объект, равноправны и не требуют, чтобы их вызывали в какой-то определенной последовательности. Если бы это было не так, то клиентам пришлось бы заботиться о том, какой указатель на объект использовать для различных запросов QueryInterface. Из транзитивности и симметричности QueryInterface следует, что любой интерфейсный указатель на объект выдаст тот же самый ответ «да/нет» на любой запрос QueryInterface. Единственная ситуация, не охватываемая транзитивностью и симметричностью, это повторные запросы одного и того же интерфейса. Эта ситуация требует, чтобы QueryInterface был и рефлективным.
QueryInterface рефлективна
Спецификация СОМ требует, чтобы запрос QueryInterface через интерфейсный указатель всегда достигал цели, если запрошенный тип соответствует типу указателя, с помощью которого произведен запрос. Это означает, что QI(A)->A всегда должен быть верным.
Это требование проиллюстрировано рис. 4.4 и в следующем фрагменте кода:
void AssertReflexive(ICar *pCar)
{
if (pCar)
{
ICar *pCar2 = 0;
// request same type of interface
// запрос интерфейса того же типа
HRESULT hr = pCar->QueryInterface(IID_ICar, (void**)&pCar2);
// if the following assertion fails, pCar
// did not point to a valid СОМ object
// если следующее утверждение неверно, то pCar
// не указывает на корректный объект СОМ
assert(SUCCEEDED(hr));
pCar2->Release();
}
}
Из этого кода следует, что все реализации ICar должны быть способны удовлетворить дополнительные запросы QueryInterface для ICar через интерфейсный указатель ICar. Если бы это не соблюдалось, то было бы невозможно передавать жестко типизированные интерфейсы через параметры базового типа без невосполнимой потери исходного типа:
extern void GetCar(ICar **ppcar);
extern void UseVehicle(IVehicle *pv);
ICar *pCar;
GetCar(&pCar);
UseVehicle(pCar);
// ICar-ness is syntactically lost
// ICar-ность синтаксически потеряна
void UseVehicle(IVehicle *pv)
{
ICar *pCar = 0;
// try to regain syntactic ICar-ness
// пытаемся восстановить синтаксическую ICar-ность
HRESULT hr = pv->QueryInterface(IID_ICar, (void**)&pCar);
}
Поскольку указатель, использованный в функции UseVehicle , имеет то же самое значение, что и указатель ICar , переданный вызывающим объектом, то выглядело бы неестественным (counterintuitive), если бы этот тип не мог быть восстановлен внутри функции.
Из того, что QueryInterface является симметричным, рефлексивным и транзитивным, следует, что любой интерфейсный указатель на объект должен выдавать тот же самый ответ «да/нет» на данный запрос QueryInterface. Это позволяет клиентам рассматривать иерархию типов объекта как простой граф, все вершины которого непосредственно соединены друг с другом (и с самими собой) с помощью открытых (explicit) ребер. На рис. 4.5 изображен такой граф. Отметим, что в любую вершину графа можно попасть из любой другой вершины, пройдя вдоль только одного ребра.
Объекты имеют статический тип
Один из выводов, который можно сделать из трех требований QueryInterfасе , состоит в том, что множество интерфейсов, поддерживаемых объектом, не может изменяться во времени. Спецификация СОМ четко требует, чтобы этот вывод был верен для всех объектов. Из этого требования следует, что иерархия типов объекта является статичной, несмотря на тот факт, что для определения множества поддерживаемых типов данных клиенты должны опрашивать объекты динамически. Если объект отвечает «да» на запрос интерфейса типа А, то он должен отвечать «да», начиная с этой точки. Если объект отвечает «нет» на запрос интерфейса типа А , то он должен отвечать «нет», начиная с этой точки. Фраза «начиная с этой точки» (from that point on) буквально переводится как «до тех пор, пока есть хотя бы один внешний указатель интерфейса на объект». Обычно это соответствует жизненному циклу базового объекта C++, но язык Спецификации СОМ обладает достаточной свободой, чтобы предоставить разработчикам определенную гибкость (например, иерархия типов глобальной переменной может изменяться, когда все указатели освобождены).
Из того, что все объекты СОМ имеют статическую иерархию типов, следует, что утверждение, записанное в следующем коде, никогда не должно быть ложным, несмотря на то, что идентификатор интерфейса используется в качестве второго параметра:
void AssertStaticType(IUnknown *pUnk, REFIID riid)
{
IUnknown *pUnk1 = 0,
*pUnk2 = 0;
HRESULT hr1 = pUnk->QueryInterface(riid, (void**)&pUnk1);
HRESULT hr2 = pUnk->QueryInterface(riid, (void**)&pUnk2);
// both requests for the same interface should
// yield the same yes/no answer
// оба запроса того же самого интерфейса
// должны получить тот же самый ответ да/нет
assert(SUCCEEDED(hr1) == SUCCEEDED(hr2));
if (SUCCEEDED(hr1)) pUnk1->Release();
if (SUCCEEDED(hr2)) pUnk2->Release();
}
Это требование означает, что в СОМ запрещены следующие программные технологии:
Использование временной информации при решении вопроса о том, удовлетворять или нет запрос QueryInterface (например, выдавать интерфейс IMorning (утро) только до 12:00).
Использование переменной информации о состоянии при решении вопроса о том, удовлетворять или нет запрос QueryInterface (например, выдавать интерфейс INotBusy (не занят), только если количество внешних интерфейсных указателей меньше десяти).
Использование маркера доступа (security token) вызывающего объекта для решения, удовлетворять или нет запрос QueryInterface . Как будет объяснено в главе 6, на самом деле это не обеспечивает никакой реальной безопасности из-за протокола передачи (wire protocol ), используемого СОМ.
Использование успешного захвата динамических ресурсов для решения вопроса о том, удовлетворять или нет запрос QueryInterface (например, выдавать интерфейс IHaveTonsOfMemory (у меня тонны памяти) только при успешном выполнении malloc(4096*4096)).
Эта последняя методика может быть до некоторой степени смягчена, если разработчик объекта желает поупражняться с выражением спецификации СОМ «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(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(this);
else if (riid == IID_IVehicle) *ppv = static_cast(this);
else if (riid == IID_ICar) *ppv = static_cast(this);
else if (riid == IID_IBoat) *ppv = static_cast(this);
else if (riid == IID_IPlane) *ppv = static_cast(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(this);
Если бы реализация QueryInterface возвращала различные указатели vptr для каждого запроса:
if (riid == IID_IUnknown)
{
int n = rand() % 3;
if (n == 0) *ppv = static_cast(this);
else if (n == 1) *ppv = static_cast(this);
else if (n == 2) *ppv = static_cast(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, которой каждый составной элемент данных будет передавать функции. Следующий код демонстрирует эту технологию: