Глава 4
Подсчет ссылок

В детстве я хотел стать пожарным. Романтика приключений и опасности привлекала меня, как и большинство мальчиков. Однако по-настоящему мне хотелось быть пожарным не потому. Дело в том, что, во-первых, пожарные ездили, повиснув сзади на пожарной машине (из-за этого я еще хотел стать мусорщиком, но это другая история). Во-вторых, у пожарных была по-настоящему крутая экипировка: металлические каски, высокие ботинки, большие плащи и кислородные баллоны. Я тоже хотел носить все эти замечательные вещи. Пожарные, казалось, никогда не расставались с ними. Даже если они всего лишь снимали кошку с дерева, то все равно делали это в касках, плащах .и высоких ботинках. Пожарного было видно издалека.

Класс C++ кое в чем напоминает пожарного. Заголовок сообщает всему миру, какие сервисы и функции предоставляет класс, — точно так же, как амуниция пожарных говорит об их профессии. Однако компоненты СОМ ведут себя совершенно иначе. Компонент СОМ гораздо более скрытен, чем пожарный или класс C++. Клиент не может посмотреть на компонент и сразу увидеть, что тот реализует пожарного. Вместо этого он должен выспрашивать: «Есть ли у Вас кислородный баллон? А топор? Носите ли Вы водонепроницаемую одежду?»

На самом деле клиенту неважно, имеет ли он дело с настоящим компонентом-пожарным. Ему важно, что у компонента за «амуниция». Например, если клиент задает вопрос «Носите ли Вы водонепроницаемую одежду?», то его удовлетворят ответы не только пожарного, но и байдарочника, аквалангиста и лесоруба в непромокаемом плаще. На вопрос «Есть ли у Вас кислородный баллон?» утвердительно ответят пожарный и аквалангист. На следующий вопрос про топор положительный ответ даст уже только пожарный (или аквалангист-лесоруб, если такой найдется).

Изоляция клиента от подлинной сущности компонента делает его менее восприимчивым к изменениям последнего. Однако, поскольку клиент знает компонент только через интерфейсы, он не может прямо управлять временем жизни компонента как такового. В этой главе мы рассмотрим косвенное управление — реализованное как явное управление временем жизни отдельных интерфейсов.

Управление временем жизни

Давайте разберем, почему клиент не должен управлять временем жизни компонента напрямую. Предположим, что наш клиент обращается все к 3 тому же компоненту-пожарному. В разных местах кода клиента могут быть 1 разбросаны вызовы этого компонента через различные интерфейсы. Одна часть клиента может дышать через кислородный баллон при посредстве интерфейса IUseOxygen, а другая — крушить дом топором при помощи IUseAxe. Клиент может закончить пользоваться IUseAxe раньше, чем IUseOxygen. Однако Вы вряд ли захотите удалить компонент из памяти, когда работа с одним интерфейсом уже закончена, а с другим еще продолжается. Определить момент, когда компонент можно безопасно удалить, сложно еще и потому, что мы не знаем, указывают ли два указателя на интерфейсы одного и того же компонента. Единственный способ узнать это — запросить IUnknown через оба интерфейса и сравнить результаты. По мере того, как программа усложняется, становится все труднее определить, когда можно «отпускать» компонент. Проще всего загрузить его и не выгружать до завершения приложения. Но такое решение не слишком эффективно.

Итак, наша стратегия будет такова: вместо того, чтобы удалять компоненты напрямую, мы будем сообщать компоненту, что нам нужен интерфейс или что мы закончили с ним работать. Мы точно знаем, когда начинаем использовать интерфейс, и знаем (обычно), когда перестаем его использовать. Однако, как уже ясно, мы можем не знать, что закончили использовать компонент вообще. Поэтому имеет смысл ограничиться сообщением об окончании работы с данным интерфейсом — и пусть компонент сам отслеживает, когда мы перестанем пользоваться всеми интерфейсами.

Именно для реализации этой стратегии и предназначены еще две функции-члена IUnknown — AddRef и Release. В прошлой главе было дано определение интерфейса IUnknown, которое я любезно повторю:

interface IUnknown
{
	virtual HRESULT __stdcall OueryInterface(const IID& iid, 
	void** ppv) = 0;
	virtual ULONG __stdcall AddRef() = 0;
	virtual ULONG __stdcall Release() = 0;
};

В этой главе мы рассмотрим, как AddRef и Release дают компоненту возможность контролировать время своей жизни, а клиенту — возможность иметь дело только с интерфейсами. Мы начнем с обзора подсчета ссылок, а затем посмотрим, как клиент использует AddRef и Release. Познакомившись с использованием функций, мы реализуем их в компоненте. Наконец, мы обсудим, когда вызовы AddRef и Release можно опустить для повышения производительности и подытожим наше обсуждение набором правил.

Подсчет ссылок

AddRef и Release реализуют технику управления памятью, известную как подсчет ссылок (reference counting). Подсчет ссылок — простой и быстрый способ, позволяющий компонентам самим удалять себя. Компонент СОМ поддерживает счетчик ссылок. Когда клиент получает некоторый интерфейс, значение этого счетчика увеличивается на единицу. Когда клиент заканчивает работу с интерфейсом, значение на единицу уменьшается. Когда оно доходит до нуля, компонент удаляет себя из памяти. Клиент также увеличивает счетчик ссылок, когда создает новую ссылку на уже имеющийся у него интерфейс. Как Вы, вероятно, догадались, увеличивается счетчик вызовом AddRef, а уменьшается - вызовом Release.

Для того, чтобы пользоваться подсчетом ссылок, необходимо знать лишь три простых правила:

  1. Вызывайте AddRef перед возвратом. Функции, возвращающие интерфейсы, перед возвратом всегда должны вызывать AddRef для соответствующего указателя. Это также относится к QueryInterface и функции CreateInstance. Таким образом, Вам не нужно вызывать AddRef'в своей программе после получения (от функции) указателя на интерфейс.
  2. По завершении работы вызывайте Release. Когда Вы закончили работу с интерфейсом, следует вызвать для него Release.
  3. Вызывайте AddRef после присваивания. Когда бы Вы ни присваивали один указатель на интерфейс другому, вызывайте AddRef. Иными словами: следует увеличить счетчик ссылок всякий раз, когда создается новая ссылка на данный интерфейс.

Вот три простых правила подсчета ссылок. Теперь рассмотрим несколько примеров. Для начала — простой пример «на первые два правила». Приведенный ниже фрагмент кода создает компонент и получает указатель на интерфейс IX. Мы не вызываем AddRef, так как за нас это делают CreateInstance и (QueryInterface. Однако мы вызываем Release как для интерфейса IUnknown, возвращенного CreateInstance, так и для интерфейса IX, возвращенного QueryInterface.

// Создать компонент.
IUnknown* pIUnknown = CreateInstance();

// Получить интерфейс IX.
IX* рIХ = NULL;
HRESULT hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);
if (SUCCEEDED(hr))
{
	pIX->Fx();     		// Использовать интерфейс IX.
	pIX->Release();   		// Завершить работу с IX.
}
pIUnknown->Release();		// Завершить работу с IUnknown.

В приведенном выше примере мы фактически закончили работать с IUnknown сразу же после вызова QueryInterface, так что его можно освободить раньше:

// Создать компонент.
IUnknown* pIUnknown = CreateInstance();

// Получить интерфейс IX.
IX* рIХ = NULL;

HRESULT hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);

// Завершить работу с IUnknown.
pIUnknown->Release();

// Использовать IX, если он был получен успешно.
if (SUCCEEDED(hr))
{
	pIX->Fx();     		// Использовать интерфейс IX.
	pIX->Release();   		// Завершить работу с IX.
}

Легко забыть, что всякий раз, когда Вы копируете указатель на интерфейс, надо увеличить его счетчик ссылок. В приведенном далее фрагменте кода делается еще одна ссылка на интерфейс IX. В общем случае необходимо увеличивать счетчик ссылок всякий раз, когда создается копия указателя на интерфейс, о чем говорит приведенное выше правило 3.

IUnknown* pIUnknown = CreateInstance();
IX* pIX = NULL;
HRESULT hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);
pIUnknown->Release() ;
if (SUCCEEDED(hr))
{
	pIX->Fx();		// Использовать интерфейс IX.
	IX* рIХ2 = pIX;		// Создать копию pIX.
	pIX2->AddRef();		// Увеличить счетчик ссылок.
	pIX2->Fx();		// Что-то сделать при помощи рIХ2.
	pIX2->Release();		// Завершить работу с рIХ2.
	pIX->Release();		// Завершить работу с pIX.
}

Первая Ваша реакция на показанный выше код могла быть такой: «Обязательно ли нужно вызывать в этом примере AddRef и Release для pIX2» либо «Как я запомню, что всякий раз при копировании указателя нужно вызывать AddRef и Release?». У некоторых оба эти вопроса возникают одновременно. Ответ на первый вопрос — нет. В данном примере AddRefw. Release для рIХ2 вызывать необязательно. В простых случаях, вроде этого, легко заметить, что увеличение и уменьшение счетчика ссылок для рIХ2 излишне, поскольку время жизни рIХ2 совпадает со временем жизни pIX. Правила оптимизации подсчета ссылок будут рассмотрены ниже в этой главе. Однако в общем случае следует вызывать AddRef всякий раз, когда порождается новое имя для указателя на интерфейс. В реальных, не простых случаях гораздо сложнее понять, отсутствует ли вызов AddRef и Release по ошибке или вследствие оптимизации. Как человек, которому приходилось целыми днями искать, почему ссылки подсчитываются неправильно, могу Вас уверить, что решать такие проблемы нелегко.

Однако, как мы увидим в гл. 10, классы smart-указателей позволяют полностью инкапсулировать подсчет ссылок. Используя их, Вы можете практически совсем забыть о подсчете ссылок.

Еще раз: клиент сообщает компоненту о своем желании использовать интерфейс, когда вызывает QueryInterface. Как мы видели выше, QueryInterface вызывает AddRef для запрашиваемого интерфейса. Когда клиент заканчивает работу с интерфейсом, он вызывает для этого интерфейса Release. Компонент остается в памяти, ожидая, когда потребуется обрабатывать вызовы методов интерфейсов, до тех пор, пока счетчик ссылок не станет равен 0. Когда счетчик становится нулем, компонент сам себя удаляет.

Подсчет ссылок на отдельные интерфейсы

Я должен отметить одну тонкую деталь. С точки зрения клиента, подсчет ссылок ведется на уровне интерфейсов, а не на уровне компонентов. Помните пожарного с его амуницией? Клиент не может видеть все целиком, он видит только отдельные интерфейсы. Таким образом, клиент считает, что у каждого интерфейса — свой счетчик ссылок.

Итак, хотя с точки зрения клиентов подсчет ссылок осуществляется для интерфейсов, а не для компонентов (см. рис. 4-1), для реализации компонента это не имеет значения. Компонент может поддерживать отдельные счетчики для каждого из интерфейсов, а может и иметь один общий счетчик. Реализация не имеет значения до тех пор, пока клиент убежден в том что подсчет ссылок ведется для самих интерфейсов. Поскольку компонент может реализовывать подсчет для каждого интерфейса, постольку клиент не должен предполагать обратного.

РИС. 4-1. Программист компонента может использовать один счетчик ссылок для всего компонента либо отдельные счетчики для каждого интерфейса.

Что означает для клиента подсчет ссылок для каждого интерфейса по отдельности? Он означает, что клиент должен вызывать AddRef именно для того указателя, с которым собирается работать, а не какого-нибудь другого. Клиент также должен вызывать Release именно для того указателя, с которым закончил работу. Например, не делайте так:

IUnknown* pIUnknown = CreateInstance();

IX* pIX = NULL;
pIUnknown->QueryInterface(IID_IX, (void**)&pIX);
pIX->Fx();

IX* pIX2 = pIX;
pIUnknown->AddRer(); // Должно быть pIX2->AddRef();

pIX2->Fx();
pIX2->Release();
pIUnknown->Release() ; // Должно быть pIX->Release();
pIUnknown->Release() ;

В приведенном фрагменте предполагается, что можно вызывать AddRef и Release через указатель на IUnknown, как если бы это был указатель на IX. В зависимости от реализации такой код может создавать проблемы.

Зачем программисту компонента может потребоваться реализовывать подсчет ссылок для каждого интерфейса, а не для всего компонента в целом? По двум основным причинам: для упрощения отладки и для поддержки деления ресурсов по требованию.

Отладка

Предположим, что Вы забыли вызвать Release для некоторого из интерфейсов компонента; забыть это легко. Компонент никогда не освободится, так как delete вызывается, только когда счетчик ссылок становится равным нулю. Проблема в том, чтобы найти, где и когда надо было освободить интерфейс; это может оказаться очень трудно. Поиски ошибок еще более усложняются, если у компонента один общий счетчик ссылок. В этом случае придется проверять все случаи использования всех предоставляемых компонентом интерфейсов. Если же компонент поддерживает для каждого интерфейса отдельный счетчик ссылок, то поиск ограничивается местами использования нужного интерфейса. Иногда это экономит массу времени.

Выделение ресурсов по требованию

Реализация интерфейса может потребовать большого объема памяти или других ресурсов. Достаточно просто реализовать QueryInterface так, чтобы память выделялась в момент запроса интерфейса. Однако, если имеется только один счетчик ссылок на весь компонент, нельзя определить, когда можно безопасно освободить память, связанную с данным интерфейсом. Использование отдельных счетчиков упрощает задачу.

Другой, и в большинстве случаев лучший, вариант — реализовать «ресурсоемкий» интерфейс в отдельном компоненте и передавать клиенту интерфейс последнего. Эта техника, называемая агрегированием (aggregation), будет продемонстрирована в гл. 8.

Для того чтобы примеры в книге были проще, мы будем использовать в компонентах общий счетчик ссылок. Теперь давайте рассмотрим, как реализовать подсчет ссылок.

Реализация AddRef in Release

Реализация AddRef (и Release) относительно проста. В основном она сводится к операции увеличения(уменыпения) на единицу, как показано ниже.

ULONG __stdcall AddRef()
{	
	return ++m_cRef;
}
ULONG __stdcall Release()
{
	if(--m_cRef == 0)
	{
		delete this;
		return 0;
	}
	return m_cRef;
}
AddRef увеличивает значение переменной m_cRef, счетчика ссылок. Release уменьшает m_сRef и удаляет компонент, если значение переменной становится равным нулю. Во многих случаях Вы можете встретить реализацию AddRef и Release при помощи функций Win32 InterlockedIncrement и InterlockedDecrement. Эти функции гарантируют, что значение переменной изменяет в каждый момент времени только один поток управления. В зависимости от потоковой модели, используемой Вашим объектом СОМ, параллельные потоки могут создавать проблемы. Вопросы, связанные с потоками, будут рассмотрены в гл. 12.
ULONG __stdcall AddRef()
{
	return InterlockedIncrement(&m_cRef);
}
ULONG __stdcall Release()
{
	if(InterlockedDecrement(&m_cRef) == 0)
	{
		delete this;
		return 0;
	}
	return m_cRef;
}

Вам также следует знать, что возвращаемые AddRef и Release значения не имеют смысла и использовать их можно только для отладки. Клиент не должен полагаться на то, что эти значения как-то связаны с числом ссылок на компонент или его интерфейсы.

Если Вы внимательно читали код гл. 3, то заметили, что я уже использовал AddRef в двух местах — в QueryInterface и CreateInstance.

HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv)
{
	if (iid == IID_IUnknown)
	{
		*ppv = static_cast(this);
	}
	else if (iid == IID_IX)
	{
		*ppv = static_cast(this);
	}
	else if (iid == IID_IY)
	{
		*ppv = static_cast(this);
	}
	else
	{
		*ppv = NULL;
		return E_NOINTERFACE;
	}
	static_cast(*ppv)->AddRef(); // См. гл. 4.
	return S_OK ;
}
IUnknown* CreateInstance()
{
	IUnknown* pI = static_cast(new CA);
	pI->AddRef();
	return pI;
}

Всякий раз, создавая компонент. Вы создаете и ссылку на него. Таким образом, компонент в момент создания должен увеличивать счетчик ссылок, прежде чем возвратить клиенту указатель. Это освобождает программиста от необходимости помнить, что после CreateInstance и QueryInterface надо вызывать AddRef.

В некоторых случаях вызовы AddRefvi Release можно опустить. Однако, прежде чем мы избавимся от некоторых из них, давайте рассмотрим листинг 4-1, который показывает все изложенное выше на примере. Копию Кода и скомпилированную программу можно найти на прилагающемся книге компакт-диске.

// REFCOUNT.CPP
// RefCount.cpp
// Компиляция: cl RefCount.cpp UUID.lib
//
#include 
#include 

void trace(const char* msg) { cout << msg << endl;}

// Предварительные описания GUID

extern const IID IID_IX;
extern const IID IID_IY;
extern const IID IID_IZ;

// Интерфейсы
interface IX : IUnknown
{
	virtual void __stdcall Fx() = 0;
};

interface IY : IUnknown
{
	virtual void __stdcall Fy() = 0;
};

interface IZ : IUnknown
{
	virtual void __stdcall Fz() = 0;
};

//
// Компонент
//

class CA :	public IX,
			public IY
{

// Реализация IUnknown

virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv);
virtual ULONG __stdcall AddRef();
virtual ULONG __stdcall Release();

// Реализация интерфейса IX
virtual void __stdcall Fx() { cout << "Fx" << endl;}

// Реализация интерфейса IY
virtual void __stdcall Fy() { cout << "Fy" << endl;}

public:

// Конструктор
CA() : m_cRef(0) {}

// Деструктор
~CA() 
{ 
	trace("CA: Ликвидировать себя.");
}

private:

	long m_cRef;

};

HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv)
{

	if (iid == IID_IUnknown)

	{
		trace("CA QI:  Возвратить указатель на IUnknown.");
		*ppv = static_cast(this);
	}
	else if (iid == IID_IX)
	{
		trace("CA QI: Возвратить указатель на IX.");
		*ppv = static_cast(this);
	}
	else if (iid == IID_IY)
	{
		trace("CA QI: Возвратить указатель на IY.");
		*ppv = static_cast(this);
	}
	else
	{
		trace("CA QI:  Интерфейс не поддерживается.");
		*ppv = NULL;
		return E_NOINTERFACE;
	}
	reinterpret_cast(*ppv)->AddRef();
	return S_OK;
}

ULONG __stdcall CA::AddRef()
{
	cout << "CA:     AddRef = " << m_cRef+1 << '.' << endl;
	return InterlockedIncrement(&m_cRef);

}

ULONG __stdcall CA::Release()
{
	cout << "CA:     Release = " << m_cRef-1 << '.' << endl;
	if (InterlockedDecrement(&m_cRef) == 0)
	{
		delete this;
		return 0;
	}
	return m_cRef;
}
//	
// Функция создания
//

IUnknown* CreateInstance()
{
	IUnknown* pI = static_cast(new CA);
	pI->AddRef();
	return pI;
}

//
// II IID
//
// {32bb8320-b41b-11cf-a6bb-0080c7b2d682}

static const IID IID_IX =
{0x32bb8320, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82}};

// {32bb8321-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IY =
{0x32bb8321, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82}};

// {32bb8322-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IZ =
{0x32bb8322, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82}};

//
// Клиент
//

int main()
{

	HRESULT hr;

	trace("Клиент: Получить указатель на IUnknown.");
	IUnknown* pIUnknown = CreateInstance();

	trace("Клиент: Получить интерфейс IX.");

	IX* pIX = NULL;

	hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);

	if (SUCCEEDED(hr))
	{
		trace("Клиент: IX получен успешно.");

		pIX->Fx();          // Использовать интерфейс IX.
		pIX->Release();
	}

	trace("Клиент: Получить интерфейс IY.");

	IY* pIY = NULL;

	hr = pIUnknown->QueryInterface(IID_IY, (void**)&pIY);

	if (SUCCEEDED(hr))
	{
		trace("Клиент: IY получен успешно.");

		pIY->Fy();          // Использовать интерфейс IY
		pIY->Release();

	}

	trace("Клиент: Запросить неподдерживаемый интерфейс." );
	
	IZ* pIZ = NULL;

	hr = pIUnknown->QueryInterface(IID_IZ, (void**)&pIZ);

	if (SUCCEEDED(hr))
	{
		trace("Клиент: Интерфейс IZ получен успешно.");
		pIZ->Fz();
		pIZ->Release();

	}
	else
	{
		trace("Клиент: Не могу получить интерфейс IZ.");
	}
	
	trace("Клиент: Освободить интерфейс IUnknown.");
	pIUnknown->Release();
	
	return 0;
}
Эта программа выводит на экран следующее:
Клиент:  Получить указатель на IUnknown.
СА:   AddRef =1.
Клиент:   Получить интерфейс IX.
СА QI: Вернуть указатель на IX.
СА:   AddRef =2.
Клиент:   IX получен успешно.
Fx
СА:    Release =1.
Клиент:  Получить интерфейс IY.
СА QI: Вернуть указатель на IY.
СА:   AddRef = 2.
Клиент:   IY получен успешно.
Fy
СА:   Release =1.
Клиент:  Запросить неподдерживаемый интерфейс.
СА QI: Интерфейс не поддерживается.
Клиент:   Не могу получить интерфейс IZ.
Клиент:  Освободить интерфейс IUnknown.
СА:   Release = 0.
СА:   Ликвидировать себя.

Это та же программа, что и в примере гл. 3, но к ней добавлен подсчет ссылок. К компоненту добавлены реализации AddRefи Release. Единственное отличие в клиенте — добавлены вызовы Release, чтобы обозначить окончание работы с различными интерфейсами. Обратите также внимание, что клиент больше не использует оператор delete. В данном примере клиенту нет надобности в AddRef, так как эту функцию для соответствующих указателей на интерфейсы вызывают CreateInstance и QueryInterface.

Когда подсчитывать ссылки

Теперь пора разобраться с тем, когда нужно вести подсчет ссылок. Мы увидим, что иногда можно безопасно опустить пары вызовов AddRef/Release, тем самым оптимизируя код. Сочетая изложенные ранее принципы с новыми навыками оптимизации, мы определим некоторые общие правила подсчета ссылок.

Оптимизация подсчета ссылок

Не так давно мы задавали себе вопрос, нужно ли при копировании указателя интерфейса всякий раз увеличивать счетчик ссылок. Этот вопрос появился при рассмотрении фрагмента кода, похожего на приведенный ниже:

HRESULT hr;
IUnknown* pIUnknown = CreateInstance();
IX* pIX = NULL;

hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);
pIUnknown->Release();
if (SUCCEEDED(hr))
{
	IX* pIХ2 = pIX;    // Скопировать pIX.
		// Время жизни рIХ2 "вложено" во время существования pIX.
		pIX2->AddRef(); // Увеличить счетчик ссылок.
		pIX->Fx();      // Использовать интерфейс IX.
		pIX2->Fx();     // Сделать что-нибудь при помощи pIХ2.
		pIX2->Release();   // Конец работы с pIХ2.
	pIX->Release();    // Конец работы с IX.
		// А также конец работы с компонентом.
}

Представленный фрагмент не выгружает компонент до тех пор, пока клиент не освободит pIX. Клиент не освобождает pIX до тех пор, пока не закончит работу как с pIX, так и с рIХ2. Поскольку компонент не выгружается, пока не освобожден pIX, постольку он гарантированно остается в памяти на протяжении всей жизни рIХ2. Таким образом, нам и нет необходимости вызывать AddRef и Release для pIХ2, поэтому две строки кода, выделенные полужирным шрифтом, можно сократить.

Подсчет ссылок для pIX — это все, что необходимо для удерживания компонента в памяти. Принципиально то, что время жизни pIХ2 содержится внутри времени существования pIX. Чтобы подчеркнуть это, я отступы для строк, где используется pIХ2. На рис. 4-2 вложение времени существования pIХ и pIХ2 показано в виде графика. Здесь столбиками обозначены времена жизни различных интерфейсов и время жизи компонента. Ось времени направлена сверху вниз. Операции, оказывающие воздействие на продолжительности жизни, перечислены в левой части рисунка. Горизонтальные линии показывают, как эти операции начинают или завершают период существования интерфейсов. РИС. 4-2. Вложенность времен жизни указателей на интерфейсы. Ссылки для указателя со вложенным временем жизни подсчитывать не требуется. Из рис. 4-2 легко видеть, что жизнь pIХ2 начинается после начала жизни pIX и заканчивается до окончания жизни pIX. Таким образом, счетчик ссылок pIX будет сохранять компонент в памяти все время жизни pIХ2. Если бы жизнь pIХ2 не содержалась внутри жизни pIX, но перекрывалась с нею, то для pIХ2 потребовалось бы подсчитывать ссылки. Например, в следующем фрагменте кода жизни pIX2 и pIX перекрываются:

IDnknown* pIUnknown = CreateInstance();
IX* pIX = NULL;
HRESULT hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX) ;
pIUnknown->Release();
if (SUCCEEDED(hr))
{
	IX* pIX2 = pIX; // Скопировать pIX.
	pIX2->AddRef(); // Начало жизни pIX2.
	pIX->Fx();
	pIX->Release(); //Конец жизни pIX.
	pIX2->Fx();
	pIX2->Release();   // Конец жизни pIX2.
		// А также конец работы с компонентом.
}
В этом примере мы обязаны вызывать AddRef для pIX2, так как pIX2 освобождается после освобождения pIX. Графически это представлено на рис. 4-3.

РИС. 4-3. Перекрывание времен жизни указателей на интерфейсы. Здесь надо подсчитывать ссылки на оба интерфейса.

В таких простых примерах легко определить, нужно ли подсчитывать ссылки. Однако достаточно лишь немного приблизиться к реальности, как идентифицировать вложенность времен жизни затруднится. Тем не менее, иногда соотношение времен жизни по-прежнему очевидно. Один такой случай - функции. Для нижеследующего кода очевидно, что время работы foo содержится внутри времени жизни pIХ. Таким образом, нет необходимости вызывать AddRef и Release для передаваемых в функцию указателей на интерфейсы

void foo(IX* pIХ2)
{
	pIX2->Fx(); // Использование интерфейса IX.
}
void main()
{
	HRESULT hr ;
	IUnknown* pIUnknown = CreateInstanceO ;
	IX* pIX = NULL;
	hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX) ;
	pIUnknown->Release();
	if (SUCCEEDED(hr))
	{
		foo(pIX);       // Передать pIX процедуре.
		pIX->Release(); // Завершить работу с IX.
	// А также и с компонентом.
	}
}
Внутри функции незачем подсчитывать ссылки для указателей на интерфейсы, хранящихся в локальных переменных. Время жизни локальной переменной совпадает со временем работы функции, т. е. содержится внутри времени жизни вызывающей программы. Однако подсчет ссылок необходим при всяком копировании указателя в глобальную переменную или из нее — глобальная переменная может освободиться в любой момент и в любой функции.

Оптимизация подсчета ссылок основана на определении указателей на интерфейс, чьи времена жизни вложены во времена жизни других ссылок на тот же интерфейс. Искать такие вложения в непростом коде бывает сложно. Однако правила, представленные в следующем разделе, учитывают некоторые типичные случаи, когда пары AddRef/Release можно опустить без большой опасности внести в программу ошибку.

Правило подсчета ссылок

Эти правила объединяют идеи оптимизации из предыдущего раздела с правилами подсчета ссылок, приведенными в начале главы. Читая их, помните, что клиент должен работать с каждым интерфейсом так, как если бы у того был отдельный счетчик ссылок. Следовательно, клиент должен выполнять подсчет ссылок для разных указателей на интерфейсы, хотя бы их времена жизни и были вложенными.

Правило для выходных параметров

Выходной параметр (out parameter) — это параметр функции, в котором вызывающей программе возвращается некоторое значение. Функция устанавливает это значение; первоначальное, заданное вызывающей программой значение, не используется. Выходные параметры служат той же цели, что и возвращаемые значения функции. Пример выходного параметра — второй параметр функции QueryInterface.

HRESULT QueryInterface(const IID&, void**);

Любая функция, возвращающая указатель на интерфейс через выходной параметр или как свое собственное возвращаемое значение, должна вызывать AddRef для этого указателя. Это то же самое правило, что и «Вызывайте AddRef перед возвратом» из начала главы, но сформулировано оно по-другому. QueryInterface следует этому правилу; вызывая AddRef для возвращаемого ею указателя на интерфейс. Наша функция создания компонентов CreateInstance также следует ему.

Правило для входных параметров

Входной параметр (in parameter) — это параметр, через который функции передается некоторое значение. Функция использует это значение, но не изменяет его и ничего не возвращает в нем вызывающей программе. В C++ такие параметры представляются константами или передаваемыми по значению аргументами функции. Ниже указатель на интерфейс передается как входной параметр:

void foo(IX* pIX)
{
	pIX->Fx();
}

Указатель на интерфейс, переданный в функцию, не требует обращений к AddRef и Release, так как время жизни функции всегда вложено во время жизни вызывающей программы. Это правило легко запомнить, если попробовать мысленно подставить код функции в точку ее вызова. Возьмем в качестве примера следующий фрагмент:

IX* pIX = CreateInstance();		// Автоматический вызов AddRef
foo(pIX);
pIX->Release();
В варианте с «развернутым» кодом foo этот фрагмент имел бы вид:
IX* pIX = CreateInstance();		// Автоматический вызов AddRef
// foo(pIX);
pIX->Fx();			// Подстановка функции foo
pIX->Release();
После подстановки foo становится очевидно, что время ее жизни вложено во время жизни вызывающей программы.

Правило для параметров типа вход-выход

Параметр типа вход-выход (in-out parameter) может одновременно быть и входным, и выходным. Функция использует переданное ей значение такого параметра, затем изменяет его и возвращает вызывающей программе.

Функция обязана вызвать Release для указателя на интерфейс, переданного ей как произвольный параметр, прежде чем записать на его место новый указатель. Перед возвратом в вызывающую программу функция также должна вызывать AddRef для нового значения параметра.

void ExchangeForCachedPtr(int i, IX** ppIX)
{
	(*ppIX)->Fx();		// Делаем что-нибудь с входным параметром.
	(*ppIX)->Release();	// Освобождаем входной параметр.
	*рpIХ = g_Cache[i];	// Выбираем указатель из кэша.
	(*ppIX)->AddRef();		// Вызываем для него AddRef.
	(*ppIX)->Fx();		// Делаем что-нибудь с выходным параметром.
}

Правило для локальных переменных

Локальные копии указателей на интерфейсы, конечно, существуют только во время выполнения функции и не требуют пар AddRef/Release. Это правило непосредственно вытекает из правила для входных параметров. В приведенном далее примере pIХ2 гарантированно будет существовать только во время выполнения функции foo. Таким образом, его существование вложено во время жизни указателя pIX, переданного как входной параметр, — так что вызывать AddRef или Release для pIХ2 не нужно.

void foo(IX* pIX)
{
	IX* pIХ2 = pIX;
	pIX2->Fx();
}

Правило для глобальных переменных

Если указатель на интерфейс сохраняется в глобальной переменной, то прежде чем передавать управление другой функции, необходимо вызвать AddRef. Поскольку переменная является глобальной, любая функция может вызвать Release и закончить жизнь этого указателя. Указатели на интерфейсы, сохраняемые в переменных-членах, должны обрабатываться аналогично. Любая функция-член класса может изменить состояние такого указателя.

Правило для сомнительных случаев

Всякий раз, когда у Вас возникает сомнение, вставляйте пару AddRef/Release. Ее отсутствие редко дает значительный выигрыш в производительности или экономию памяти, зато легко может привести к созданию компонента, который никогда не удаляется из памяти. Кроме того, Вы можете потратить много времени на поиск ошибки, вызванной неправильным подсчетбм ссылок. Как обычно, с помощью профилировщика можно определить, дает ли оптимизация существенный выигрыш. Помимо этого, если Вы все же решили применить оптимизацию, обязательно пометьте указатель, для которого не выполняется подсчет ссылок, соответствующим комментарием. Другому программисту, модифицирующему Ваш код, очень легко запутаться во временах жизни и нарушить правильность оптимизированного подсчета ссылок.

Пропущенный вызов Release труднее обнаружить, чем отсутствие вызова AddRef. Программисты на C++ легко могут забыть вызвать Release или, еще хуже, попытаться использовать delete вместо Release. В гл. 10 показано, как smart-указатели могут полностью инкапсулировать подсчет ссылок.

Амуниция пожарного, резюме

Методы IUnknown дают нам полный контроль над интерфейсами. Как мы видели в предыдущей главе, указатели на интерфейсы, поддерживаемые компонентом, можно получить через QueryInterface. В этой главе мы видели, как AddRef и Release управляют временами жизни полученных интерфейсов. AddRef сообщает компоненту, что мы собираемся использовать Интерфейс. Release сообщает, что использование интерфейса закончено. Release также предоставляет компоненту некоторую способность управлять своим временем жизни. Клиент никогда не выгружает компонент напрямую; вместо этого Release сигнализирует, что клиент завершил работу с интерфейсом. Если ни один из интерфейсов никем не используется, компонент может удалить себя сам.

Хотя теперь мы имеем все возможности управления интерфейсами, у нас нет одной важной составляющей компонентной архитектуры — динамической компоновки. Компонент без динамической компоновки — это все равно что пожарный без каски и плаща. В следующей главе мы добавим к нашим компонентам динамическую компоновку.

[Назад] [Содержание] [Вперед]


Смотрите также:



Hosted by uCoz