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;
}