Александр Шаргин - Делегаты на C++. Страница 4

class UseVoid {};

template‹class TRet TEMPLATE_PARAMS›

I_DELEGATE‹TRet TEMPLATE_ARGS›* NewDelegate(TRet (*pFunc)(PARAMS), UseVoid‹0›) {

 return new C_STATIC_DELEGATE‹TRet TEMPLATE_ARGS›(pFunc);

}

template‹class TRet TEMPLATE_PARAMS›

I_DELEGATE‹TRet TEMPLATE_ARGS›* NewDelegate(TRet (*pFunc)(PARAMS), UseVoid‹1›) {

 return new C_STATIC_DELEGATE_VOID‹TRet TEMPLATE_ARGS›(pFunc);

}

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

void f();

int g();

NewDelegate(f, UseVoid‹1›());

NewDelegate(g, UseVoid‹0›());

Чтобы избавиться от необходимости явно указывать параметр UseVoid, напишем третий вариант функции NewDelegate, который будет автоматически (причём на этапе компиляции) определять и вызывать нужную версию этой функции. Для реализации этой идеи нам потребуется механизм преобразования типа TRet в константу 1 (в случае TRet=void) или 0 (для всех остальных типов). Мы уже решали аналогичную задачу в классе DelegateRetVal, поэтому теперь решение записывается без труда:

template‹class T›

struct IsVoid {

 enum { Result = 0};

};

template‹› struct

IsVoid‹void› {

 enum {Result = 1};

};

Теперь воспользуемся классом IsVoid для выбора нужного варианта функции NewDelegate.

template‹class TRet TEMPLATE_PARAMS›

I_DELEGATE‹TRet TEMPLATE_ARGS›* NewDelegate(TRet (*pFunc)(PARAMS)) {

 return NewDelegate(pFunc, UseVoid‹IsVoid‹TRet›::Result›());

}

Аналогичным образом NewDelegate перегружается для случая создания объектов CMethodDelegate*:

I_DELEGATE‹TRet TEMPLATE_ARGS›* NewDelegate(TObj* pObj, TRet (TObj::*pMethod)(PARAMS), UseVoid‹0›) {

 return new C_METHOD_DELEGATE‹TObj, TRet TEMPLATE_ARGS› (pObj, pMethod);

}

template ‹class TObj, class TRet TEMPLATE_PARAMS›

I_DELEGATE‹TRet TEMPLATE_ARGS›* NewDelegate(TObj* pObj, TRet (TObj::*pMethod)(PARAMS), UseVoid‹1›) {

 return new C_METHOD_DELEGATE_VOID‹TObj, TRet TEMPLATE_ARGS› (pObj, pMethod);

}

template ‹class TObj, class TRet TEMPLATE_PARAMS›

I_DELEGATE‹TRet TEMPLATE_ARGS›* NewDelegate(TObj* pObj, TRet (TObj::*pMethod)(PARAMS)) {

 return NewDelegate(pObj, pMethod, UseVoid‹IsVoid‹TRet›::Result›());

}

Если вас успели утомить эти "хождения по мукам", у меня есть для вас хорошая новость. Проблема, которую мы только что решили, была последней. Осталось заменить возвращаемые значения методов Invoke и operator() в классе CDelegate на DelegateRetVal‹TRet›::Type, чтобы получить законченную реализацию делегатов для Visual C++ 6.0.

Полную версию реализации делегатов для Visual C++ 6.0 можно найти на сопровождающем компакт-диске.

Больше, лучше, быстрее

Реализация делегатов, которую мы рассмотрели выше, вполне работоспособна. Тем не менее, некоторые её особенности вызывают озабоченность. Во-первых, интенсивное использование шаблонов может привести к чрезмерному разбуханию кода. Во-вторых, объекты делегатов распределяются динамически (при помощи оператора new). Поскольку на создание объектов в куче тратится гораздо больше времени, чем на создание стековых объектов, это может привести к проблемам производительности. В этом разделе мы рассмотрим некоторые пути преодоления этих проблем.

С точки зрения разбухания кода наиболее неблагополучно выглядит класс CDelegateX. Его специализация генерируется для каждой сигнатуры, для которой будет использоваться делегат. Но методы Add, Remove и RemoveAll никак не используют информацию о сигнатуре. То есть для этих методов каждый раз будет генерироваться один и тот же код. Чтобы изменить ситуацию, можно вынести реализацию этих методов в отдельный нешаблонный класс CDelegateImpl. Тогда все специализации шаблона IDelegateX унаследуют эту реализацию, и она останется в программе в единственном экземпляре.

Чтобы реализовать эту идею, для начала разобьём интерфейс IDelegateX на два интерфейса. Базовый, IComparableDelegate, будет "отвечать" за сравнение делегатов. Производный, уже знакомый нам IDelegateX, будет определять дополнительный метод Invoke.

class IComparableDelegate {

public:

 virtual ~IComparableDelegate() {}

 virtual bool Compare(IComparableDelegate* pDelegate) = 0;

};

template‹class TRet TEMPLATE_PARAMS›

class I_DELEGATE: public IComparableDelegate {

public:

 virtual TRet Invoke(PARAMS) = 0;

};

Обратите внимание, что в интерфейсе IComparableDelegate шаблоны не используются. Теперь в терминах этого интерфейса можно реализовать базовый класс CDelegateImpl, который будет отвечать за поддержку списка делегатов. Соответственно, в нём будут реализованы методы Add, Remove и Invoke.

class CDelegateImpl {

public:

 typedef std::list‹IComparableDelegate*› DelegateList;

 CDelegateImpl(IComparableDelegate* pDelegate = NULL) { Add(pDelegate); }

 ~CDelegateImpl() { RemoveAll(); }

 bool IsNull() { return (m_DelegateList.empty()); }

protected:

 void Add(IComparableDelegate* pDelegate) {

  if (pDelegate != NULL) m_DelegateList.push_back(pDelegate);

 }

 void Remove(IComparableDelegate* pDelegate) {

  DelegateList::iterator it;

  for (it = m_DelegateList.begin(); it != m_DelegateList.end(); ++it) {

   if ((*it)-›Compare(pDelegate)) {

    delete (*it);

    m_DelegateList.erase(it);

    break;

   }

  }

 }

 void RemoveAll() {

  DelegateList::iterator it;

  for (it = m_DelegateList.begin(); it != m_DelegateList.end(); ++it) delete (*it);

  m_DelegateList.clear();

 }

protected:

 DelegateList m_DelegateList;

};

Теперь реализация класса CDelegateX существенно упрощается. В нём останутся только операторы (для которых используется inline-подстановка) и метод Invoke. Только этот метод и будет сгенерирован отдельно для каждой специализации - хороший результат по сравнению с тем, что было раньше. Новая реализация класса CDelegateX будет выглядеть так:

template‹class TRet TEMPLATE_PARAMS›

class C_DELEGATE: public CDelegateImpl {

public:

 typedef I_DELEGATE‹TRet TEMPLATE_ARGS› IDelegate;

 C_DELEGATE(IDelegate* pDelegate = NULL): CDelegateImpl(pDelegate) {}

 C_DELEGATE‹TRet TEMPLATE_ARGS›& operator=(IDelegate* pDelegate) {

  RemoveAll();

  Add(pDelegate);

  return *this;

 }

 C_DELEGATE‹TRet TEMPLATE_ARGS›& operator+=(IDelegate* pDelegate) {

  Add(pDelegate);

  return *this;

 }

 C_DELEGATE‹TRet TEMPLATE_ARGS›& operator-=(IDelegate* pDelegate) {

  Remove(pDelegate);

  return *this;

 }

 TRet operator()(PARAMS) {

  return Invoke(ARGS);

 }

private:

 TRet Invoke(PARAMS) {

  DelegateList::const_iterator it;

  for (it = m_DelegateList.begin(); it!= --m_DelegateList.end(); ++it) static_cast‹IDelegate*› (*it)-›Invoke(ARGS);

  return static_cast‹IDelegate*› (m_DelegateList.back())-›Invoke(ARGS);

 }

};

Обратите внимание на появившиеся приведения типов. В данном случае они никак не сказываются на типобезопасности делегатов, так как в списке m_DelegateList могут храниться только указатели на объекты классов CStaticDelegateX и CMethodDelegateX, а эти указатели заведомо приводятся к указателю на IDelegate.

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

Заключение

Хочется отметить, что рассмотренный нами пример реализации делегатов может служить иллюстрацией как сильных, так и слабых сторон языка C++. Слабая сторона C++ - это его сложность. Особенно хорошо она заметна при реализации библиотек на базе шаблонов. Их код трудно читать и ещё труднее писать, так как в них семантическая сложность усугубляется сложностью синтаксической. Сильной же стороной C++ является совершенно невероятная гибкость этого языка. В рамках C++ можно реализовать и бесшовно интегрировать в язык самые разные возможности. Причём сделать это удаётся даже несмотря на грубейшие ошибки и недоработки разработчиков некоторых компиляторов.

Комментарии:

наследование операторов

›Дело в том, что в языке C++ операторы не наследуются.