На первый взгляд, отделяемые интерфейсы кажутся лучшей из всех возможностей. Когда интерфейс не используется, то на его служебные данные отводится нуль байт объекта. Когда же интерфейс используется, объект косвенно тратит 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 + счетчик ссылок). Хотя производящий класс может показаться немного запутанным, постоянные интерфейсы СОМ принадлежат к аналогичной категории, так как в настоящее время существует восемь различных постоянных интерфейсов, но объект обычно выставляет только один из них на экземпляр. В то же время разработчик класса не всегда может предсказать, какой из интерфейсов будет запрошен определенным клиентом (и будет ли какой-либо). Кроме того, каждый из восьми интерфейсов требует своего набора поддерживающих элементов данных для корректной реализации методов интерфейса. Если эти элементы данных были созданы как часть отделяемого интерфейса, а не главного объекта, то для каждого объекта будет назначен только один набор элементов данных. Этот тип сценария идеален для отделяемых интерфейсов, но опять же, для большей эффективности, указатель на отделяемый интерфейс следует кэшировать в главном объекте.