(*it);
m_DelegateList.clear();
}
TRet Invoke(PARAMS) {
DelegateList::const_iterator it;
for (it = m_DelegateList.begin(); it != --m_DelegateList.end(); ++it) (*it)-›Invoke(ARGS);
return m_DelegateList.back()-›Invoke(ARGS);
}
private:
DelegateList m_DelegateList;
};
Вынеся обобщённый таким образом делегат в отдельный файл delegate_impl.h, мы можем сгенерировать его специализацию для любого количества параметров. Например, специализация делегата для пяти параметров получается так: // 5 parameters… #define SUFFIX 5 #define TEMPLATE_PARAMS \ , class TP1, class TP2, class TP3, class TP4, class TP5 #define TEMPLATE_ARGS , TP1, TP2, TP3, TP4, TP5 #define PARAMS TP1 p1, TP2 p2, TP3 p3, TP4 p4, TP5 p5 #define ARGS p1, p2, p3, p4, p5
#include "delegate_impl.h"
#undef SUFFIX #undef TEMPLATE_PARAMS #undef TEMPLATE_ARGS #undef PARAMS #undef ARGS
Подобные фрагменты для наборов от 0 до 10 параметров можно включить в отдельный файл delegate.h, который и будут подключать пользователи делегатов. Вот пример использования библиотеки делегатов, которую мы только что получили. Обратите внимание, что он практически полностью соответствует примеру на языке C#, с которого началась эта статья. #include ‹iostream› #include ‹fstream› #include ‹string› using namespace std; #include "delegate.h"
class App { public: // Определяем делегат Callback, // который принимает 1 параметр и ничего не возвращает. typedef CDelegate1‹void, string› Callback;
// Это метод класса App. void OutputToConsole(string str) { cout ‹‹ str ‹‹ endl; }
// А это статический метод класса App. static void OutputToFile(string str) { ofstream fout("output.txt", ios::out | ios::app); fout ‹‹ str ‹‹ endl; fout.close(); } };
int main() { App app;
// Создаём делегат. App::Callback callback = NULL; if (!callback.IsNull()) callback("1");
// Добавляем ссылку на OutputToFile. // Вызываем её через делегата. callback += NewDelegate(App::OutputToFile); if (!callback.IsNull()) callback("2");
// Добавляем ссылку на OutputToConsole. // Вызывается вся цепочка: // сначала OutputToFile, потом OutputToConsole. callback += NewDelegate(&app, &App::OutputToConsole); if (!callback.IsNull()) callback("3");
// Убираем ссылку на OutputToFile. // Вызывается только OutputToConsole. callback -= NewDelegate(App::OutputToFile); if (!callback.IsNull()) callback("4");
// Убираем оставшуюся ссылку на OutputToConsole. callback -= NewDelegate(&app, &App::OutputToConsole); if (!callback.IsNull()) callback("5"); }
Законченный проект Visual Studio 7.0, содержащий этот пример, можно найти на сопровождающем компакт-диске.
Данная конструкция совершенно законна в соответствии с пунктом 6.6.3/3 Стандарта языка C++. Но VC6 об этом не знает. Поэтому нам придётся искать обходные пути. Чтобы обойти эту недоработку компилятора, необходимо отдельно реализовать все классы CDelegateX для случая TRet=void. Идеальным инструментом для этой цели служит частичная специализация шаблонов, но VC6 не поддерживает и эту возможность языка C++. В результате решение задачи на VC6 превращается в занимательную головоломку. Первой моей мыслью было воспользоваться техникой, описанной Павлом Кузнецовым в этом же номере журнала в статье "Симуляция частичной специализации". К сожалению, выяснилось, что эта методика неприменима для реализации делегатов на VC6 сразу по двум причинам. Первая причина состоит в том, что использование полиморфизма совместно с навороченными шаблонными конструкциями оказывается "не по зубам" VC6, и он отказывается компилировать классы CStaticDelegateX и CMethodDelegateX, переписанные с использованием частичной специализации. На самом деле, это ещё полбеды, так как эти классы являются внутренней деталью реализации, и применять к ним частичную специализацию не обязательно. Вторая причина носит более фундаментальный характер. Дело в том, что симуляция частичной специализации для класса CDelegate подразумевает создание двух базовых классов (например, CDelegate_void_ для случая TRet=void и CDelegate_ для всех остальных случаев). Затем, в зависимости от значения параметра TRet, класс CDelegate наследовался бы либо от общей, либо от частной реализации. И тут возникает проблема. Дело в том, что в языке C++ операторы не наследуются. Это означает, что operator() нам всё равно придётся реализовывать в классе CDelegate. А мы не сможем реализовать его из-за той самой ошибки VC6, с которой и начался этот раздел. Таким образом, мы заходим в тупик. Остаётся два пути. Первый путь - написать отдельную реализацию CDelegateVoidX, которая будет использоваться вместо CDelegateX в случае TRet=void. Этот путь плох, так как приводит к изменению внешнего интерфейса библиотеки делегатов. А это значит, что пользователям библиотеки придётся писать по две разных версии своих программ - для VC6 и для всех остальных компиляторов. Второй путь - изменить функции Invoke так, чтобы в случае TRet=void они возвращали не void, а какое-нибудь нейтральное значение (например, ноль). Конечно, это не совсем честное решение, но оно вполне работоспособно. Посмотрим, как его можно реализовать. В первую очередь нам нужен инструмент для преобразования типов, который на этапе компиляции превращал бы void в int, а остальные типы оставлял бы без изменений. В C++ такие преобразования типов осуществляются с использованием полной специализации шаблонов (к счастью, её VC6 поддерживает). В нашем случае реализация будет выглядеть так. template‹class T› struct DelegateRetVal { typedef T Type; };
template‹› struct DelegateRetVal‹void› { typedef int Type; };
Как видим, внутри класса DelegateRetVal определяется тип Type, который в общем случае совпадает с параметром шаблона T. Для случая T=void это поведение переопределяется с использованием специализации: в этом случае тип Type определяется как int. В результате, выражение DelegateRetVal‹TRet›::Type будет на этапе компиляции принимать нужный нам тип при любых значениях TRet.Следующий шаг - модификация классов CStaticDelegateX и CMethodDelegateX. Во-первых, нужно заменить значение, возвращаемое методом Invoke, на DelegateRetVal‹TRet›::Type. Во-вторых, нужно реализовать два дополнительных класса, CStaticDelegateVoidX и CMethodDelegateVoidX, для обработки случая TRet=void. Единственным их отличием от одноимённых классов без суффикса "Void" будет другая реализация метода Invoke: #define C_STATIC_DELEGATE_VOID COMBINE(CStaticDelegateVoid, SUFFIX) #define C_METHOD_DELEGATE_VOID COMBINE(CMethodDelegateVoid, SUFFIX) … template‹class TRet TEMPLATE_PARAMS› class C_STATIC_DELEGATE_VOID: public I_DELEGATE‹TRet TEMPLATE_ARGS› { … virtual DelegateRetVal‹TRet›::Type
Вынеся обобщённый таким образом делегат в отдельный файл delegate_impl.h, мы можем сгенерировать его специализацию для любого количества параметров. Например, специализация делегата для пяти параметров получается так: // 5 parameters… #define SUFFIX 5 #define TEMPLATE_PARAMS \ , class TP1, class TP2, class TP3, class TP4, class TP5 #define TEMPLATE_ARGS , TP1, TP2, TP3, TP4, TP5 #define PARAMS TP1 p1, TP2 p2, TP3 p3, TP4 p4, TP5 p5 #define ARGS p1, p2, p3, p4, p5
#include "delegate_impl.h"
#undef SUFFIX #undef TEMPLATE_PARAMS #undef TEMPLATE_ARGS #undef PARAMS #undef ARGS
Подобные фрагменты для наборов от 0 до 10 параметров можно включить в отдельный файл delegate.h, который и будут подключать пользователи делегатов. Вот пример использования библиотеки делегатов, которую мы только что получили. Обратите внимание, что он практически полностью соответствует примеру на языке C#, с которого началась эта статья. #include ‹iostream› #include ‹fstream› #include ‹string› using namespace std; #include "delegate.h"
class App { public: // Определяем делегат Callback, // который принимает 1 параметр и ничего не возвращает. typedef CDelegate1‹void, string› Callback;
// Это метод класса App. void OutputToConsole(string str) { cout ‹‹ str ‹‹ endl; }
// А это статический метод класса App. static void OutputToFile(string str) { ofstream fout("output.txt", ios::out | ios::app); fout ‹‹ str ‹‹ endl; fout.close(); } };
int main() { App app;
// Создаём делегат. App::Callback callback = NULL; if (!callback.IsNull()) callback("1");
// Добавляем ссылку на OutputToFile. // Вызываем её через делегата. callback += NewDelegate(App::OutputToFile); if (!callback.IsNull()) callback("2");
// Добавляем ссылку на OutputToConsole. // Вызывается вся цепочка: // сначала OutputToFile, потом OutputToConsole. callback += NewDelegate(&app, &App::OutputToConsole); if (!callback.IsNull()) callback("3");
// Убираем ссылку на OutputToFile. // Вызывается только OutputToConsole. callback -= NewDelegate(App::OutputToFile); if (!callback.IsNull()) callback("4");
// Убираем оставшуюся ссылку на OutputToConsole. callback -= NewDelegate(&app, &App::OutputToConsole); if (!callback.IsNull()) callback("5"); }
Законченный проект Visual Studio 7.0, содержащий этот пример, можно найти на сопровождающем компакт-диске.
Те же и Visual C++ 6.0
На этом можно было бы поставить точку, но остаётся ещё одна нерешённая проблема. Если вы попытаетесь скомпилировать приведённый пример в Visual C++ 6.0, у этого компилятора возникнут проблемы при задании параметра шаблона делегата TRet=void. Дело в том, что в этом случае VC6 не может корректно обработать конструкцию вида: virtual TRet Invoke(TP1 p1) { // VC6 полагает, что нельзя возвращать выражение типа void. return (m_pObj-›*m_pMethod)(p1); }Данная конструкция совершенно законна в соответствии с пунктом 6.6.3/3 Стандарта языка C++. Но VC6 об этом не знает. Поэтому нам придётся искать обходные пути. Чтобы обойти эту недоработку компилятора, необходимо отдельно реализовать все классы CDelegateX для случая TRet=void. Идеальным инструментом для этой цели служит частичная специализация шаблонов, но VC6 не поддерживает и эту возможность языка C++. В результате решение задачи на VC6 превращается в занимательную головоломку. Первой моей мыслью было воспользоваться техникой, описанной Павлом Кузнецовым в этом же номере журнала в статье "Симуляция частичной специализации". К сожалению, выяснилось, что эта методика неприменима для реализации делегатов на VC6 сразу по двум причинам. Первая причина состоит в том, что использование полиморфизма совместно с навороченными шаблонными конструкциями оказывается "не по зубам" VC6, и он отказывается компилировать классы CStaticDelegateX и CMethodDelegateX, переписанные с использованием частичной специализации. На самом деле, это ещё полбеды, так как эти классы являются внутренней деталью реализации, и применять к ним частичную специализацию не обязательно. Вторая причина носит более фундаментальный характер. Дело в том, что симуляция частичной специализации для класса CDelegate подразумевает создание двух базовых классов (например, CDelegate_void_ для случая TRet=void и CDelegate_ для всех остальных случаев). Затем, в зависимости от значения параметра TRet, класс CDelegate наследовался бы либо от общей, либо от частной реализации. И тут возникает проблема. Дело в том, что в языке C++ операторы не наследуются. Это означает, что operator() нам всё равно придётся реализовывать в классе CDelegate. А мы не сможем реализовать его из-за той самой ошибки VC6, с которой и начался этот раздел. Таким образом, мы заходим в тупик. Остаётся два пути. Первый путь - написать отдельную реализацию CDelegateVoidX, которая будет использоваться вместо CDelegateX в случае TRet=void. Этот путь плох, так как приводит к изменению внешнего интерфейса библиотеки делегатов. А это значит, что пользователям библиотеки придётся писать по две разных версии своих программ - для VC6 и для всех остальных компиляторов. Второй путь - изменить функции Invoke так, чтобы в случае TRet=void они возвращали не void, а какое-нибудь нейтральное значение (например, ноль). Конечно, это не совсем честное решение, но оно вполне работоспособно. Посмотрим, как его можно реализовать. В первую очередь нам нужен инструмент для преобразования типов, который на этапе компиляции превращал бы void в int, а остальные типы оставлял бы без изменений. В C++ такие преобразования типов осуществляются с использованием полной специализации шаблонов (к счастью, её VC6 поддерживает). В нашем случае реализация будет выглядеть так. template‹class T› struct DelegateRetVal { typedef T Type; };
template‹› struct DelegateRetVal‹void› { typedef int Type; };
Как видим, внутри класса DelegateRetVal определяется тип Type, который в общем случае совпадает с параметром шаблона T. Для случая T=void это поведение переопределяется с использованием специализации: в этом случае тип Type определяется как int. В результате, выражение DelegateRetVal‹TRet›::Type будет на этапе компиляции принимать нужный нам тип при любых значениях TRet.Следующий шаг - модификация классов CStaticDelegateX и CMethodDelegateX. Во-первых, нужно заменить значение, возвращаемое методом Invoke, на DelegateRetVal‹TRet›::Type. Во-вторых, нужно реализовать два дополнительных класса, CStaticDelegateVoidX и CMethodDelegateVoidX, для обработки случая TRet=void. Единственным их отличием от одноимённых классов без суффикса "Void" будет другая реализация метода Invoke: #define C_STATIC_DELEGATE_VOID COMBINE(CStaticDelegateVoid, SUFFIX) #define C_METHOD_DELEGATE_VOID COMBINE(CMethodDelegateVoid, SUFFIX) … template‹class TRet TEMPLATE_PARAMS› class C_STATIC_DELEGATE_VOID: public I_DELEGATE‹TRet TEMPLATE_ARGS› { … virtual DelegateRetVal‹TRet›::Type