Имитация делегатов C# в стандартном C++ - Понимание кода
ОГЛАВЛЕНИЕ
Понимание кода
Давайте поговорим о коде. Если вас не интересует, как он работает, пропустите этот раздел и переходите к Использованию кода.
Проект должен удовлетворять следующим ограничениям:
- Делегат может вызывать нестатические функции-члены объекта любого класса, включая виртуальные функции.
- Делегат может вызывать статические функции-члены любого класса и свободные функции.
- Делегат имеет многократное приведение.
- Делегат легко использовать (т.е. синтаксис близок к C#).
В нашем решении есть 4 части:
- Базовый класс, в сигнатуре которого нет ссылок на какой-либо реальный класс. Он имеет чисто виртуальную перегрузку operator(). Это закрытый вложенный класс.
- Внешний класс, реально используемый клиентами. Он вызывает operator() для набора производных классов от базового класса.
- Производные классы, реализующие перегруженный operator(), объявленный в базовом классе. Это открытый вложенный класс шаблона, позволяющий задавать тип целевого события.
- Производный класс, обрабатывающий статические и свободные функции. Это открытый вложенный класс.
Базовый класс выглядит примерно так (Return и Arg1 – это типы из внешнего класса):
class Base
{
public:
virtual ~Base() { }
virtual Return operator()(Arg1) = 0;
};
Производный класс для нестатических функций выглядит примерно так (Return и Arg1 – это типы из внешнего класса):
template <typename Class>
class T : public Base
{
// Сигнатура, применяемая к указателю на член для целевого класса.
typedef Return (Class::*Func)(Arg1);
private:
Class* mThis; // Указатель на объект, с которым связан делегат.
Func mFunc; // Адрес функции объекта делегата.
public:
T(Class* aThis, Func aFunc) : mThis(aThis), mFunc(aFunc) { }
virtual Return operator()(Arg1 arg1)
{
return (mThis->*mFunc)(arg1);
}
};
Производный класс для статических и свободных функций выглядит примерно так (Return и Arg1 – это типы из внешнего класса):
class S : public Base
{
typedef Return (*Func)(Arg1);
private:
Func mFunc;
public:
S(Func aFunc) : mFunc(aFunc) { }
virtual Return operator()(Arg1 arg1)
{
return mFunc(arg1);
}
};
Внешний класс выглядит примерно так (много подробностей пропущено):
template <typename Return, typename Arg1>
class Event
{
private:
std::vector<Base*> mPtrs;
class Base { ... };
public:
template <typename Class>
class T : public Base { ... }; // нестатический
class S : public Base { ... }; // статический
// Добавляем новую цель (вызываемую) в наш список.
Event& operator+=(Base* aPtr)
{
mPtrs.push_back(aPtr);
return *this;
}
// Вызываем все цели – код будет вести себя непредсказуемо,
// если вызываемый объект не существует.
Return operator()(Arg1 arg1)
{
// Здесь есть проблемы:
// 1. Какой результат должно возвращать многократное приведение?
// На данный момент возвращается последний вызванный элемент.
// 2. Нам нужно не сохранять временный результат, когда Return не имеет типа.
typename std::vector<Base*>::iterator end = mPtrs.end();
for (typename std::vector<Base*>::iterator i = mPtrs.begin();
i != end; ++i)
{
// Вероятно, специализация для Return == void была бы лучше.
if ((i + 1) == end)
return (*(*i))(arg1);
else
(*(*i))(arg1);
}
}
};
Есть еще некоторая работа. Нам нужно сделать копирование этих объектов безопасным, и нужно сделать что-то для делегатов с многократным приведением с сигнатурой, которая возвращает значение. Вероятно, нужно возвращать вектор результатов. Также нужно скопировать шаблон, чтобы справиться с двумя или более аргументами сигнатуры.
Несомненно, эта реализация очень медленная по сравнению с указателями функций, но обычно события применяются в работе графического интерфейса пользователя, поэтому скорость не так критична. Было бы интересно увидеть, как выглядит внутренняя реализация делегатов и событий .NET.
Похоже, что наш класс События был бы наиболее полезен как открытый член любого класса, планирующего представлять событие. Это нарушает инкапсуляцию, но в противном случае синтаксис добавления целей стал бы весьма неудобным. Нам нужно сделать так, чтобы клиенты класса, использующего Событие, не вызывали operator() для него. Возможно, сделать это мог бы простой адаптер – он должен быть открытым членом, передающим вызовы закрытому члену События, но не предоставляющим operator().