Имитация делегатов C# в стандартном C++
ОГЛАВЛЕНИЕ
Введение
Язык C# имеет возможность, называемую делегаты, которые позволяют легко отделить инициатора события от конечного обработчика. По существу, они выполняют такую же роль, что и указатели функций в C и указатели на функции-члены в C++, но они намного более гибкие. В частности, они могут использоваться для указания на любую функцию любого объекта, если она имеет соответствующую сигнатуру.
Эта статья описывает наш подход к обеспечению такой же функциональности путем использования только стандартного C++. Есть много достойных альтернатив, которые можно легко найти через поиск в Google по ключевым словам "C++ делегаты". Нашей задачей было создание синтаксиса, аналогичного используемому в управляемом C++ и C#.
Предпосылки
Если вы уже знаете о делегатах все, что вам нужно, пропустите этот раздел.
Делегаты – это не новая идея. Продукты Borland Delphi и C++ Builder использовали их с самого начала, чтобы сопровождать Библиотеку визуальных компонентов, хотя они назывались ‘указатели методов’ в Delphi и ‘замыкания’ в Builder (это одинаковые вещи). По существу, замыкание – это указатель объектно-ориентированной функции. Внутри он просто хранит адрес вызываемой функции и адрес объекта, для которого она вызывается (то есть скрытый параметр 'this', передаваемый функции).
Важно следующее: сомнения относительно того, какой другой класс будет обрабатывать события, которые класс порождает, являются основной причиной создания визуальной среды разработки Delphi, основанной на использовании компонентов. Уменьшение связанности между классами – это хорошее дело.
Сейчас делегаты в стиле C# предоставляют такой же сервис и для языков .NET, но в стандартном C++ их нет. Указатели на члены-функции очень ограничены по сравнению с делегатами, хотя они использовались совместно с макроопределениями в таких библиотеках, как Borland OWL в прошлом, и с Trolltech's Qt сегодня.
С помощью делегатов .NET вы можете даже присоединить несколько обработчиков к одному событию. Они все будут вызваны (последовательно) при вызове делегата. Это значит, что вы можете безопасно присоединить ваш разработчик к событию без разрушения какой-либо другой связи. Мы еще не использовали эту возможность, но осознаем ее потенциал. Однако мы не знаем, может ли эта возможность быть эффективно реализована. Вызов замыканий Borland с одиночным приведением сокращает пару кодов операции, которые помещают 'this' в стек и вызывают функцию, поэтому они эффективны и малозатратны в использовании. [Жаль, что они не входят в стандарт.] Но как только вы начинаете поддерживать динамическую коллекцию целей, все становится намного сложнее. В идеале .NET должен иметь в наличии очень эффективные эффективные делегаты с одиночным приведением, а также реализовывать делегаты с многократным приведением в их – это лучшее, что можно было бы иметь под рукой.
NET различает 'делегаты' и 'события'. Делегат – это улучшенный указатель функции; делегат используется для событий – член класса, которому вы хотите присвоить адреса обработчиков, которые будут вызваны, когда класс вызовет делегат. Мы считаем это разграничение бесполезным – как еще могут использоваться делегаты? Мы иногда используем термины по очереди.
C# и управляемый C++ имеют довольно аккуратный синтаксис для присваивания обработчиков делегату/событию:
mnuPopup->Popup += new System::EventHandler(this, mnuPopup_Popup);
Когда контекстное меню mnuPopup отображается пользователем, оно вызывает его событие/делегат всплывающего меню. Затем будет вызван обработчик, реализованный в форме mnuPopup_Popup.
Понимание кода
Давайте поговорим о коде. Если вас не интересует, как он работает, пропустите этот раздел и переходите к Использованию кода.
Проект должен удовлетворять следующим ограничениям:
- Делегат может вызывать нестатические функции-члены объекта любого класса, включая виртуальные функции.
- Делегат может вызывать статические функции-члены любого класса и свободные функции.
- Делегат имеет многократное приведение.
- Делегат легко использовать (т.е. синтаксис близок к 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().
Использование кода
Чтобы испытать код, загрузите его демонстрационную версию. Все содержится в одном файле. Скомпилируйте его и запустите из командной строки. Этот код был разработан в g++ 3.2. Мы бы хотели знать, в каких других компиляторах он будет работать, а в каких нет, поэтому просим сообщать нам.
struct TShapes
{
virtual void Square(int i)
{ std::cout << "TShapes::Square: " << i << std::endl; }
void Triangle(int i)
{ std::cout << "TShapes::Triangle: " << i << std::endl; }
};
struct TDerivedShapes : TShapes
{
virtual void Square(int i)
{ std::cout << "TDerivedShapes::Square: " << i << std::endl; }
};
struct TThings
{
void Thing1(int i)
{ std::cout << "TThings::Thing1: " << i << std::endl; }
static void Thing2(int i)
{ std::cout << "TThings::Thing2: " << i << std::endl; }
};
void Free(int i)
{ std::cout << "Free: " << i << std::endl; }
int main()
{
// Как обычно, typedef облегчает использование шаблонов.
typedef Event<void, int> MyEvent;
MyEvent event;
TShapes shapes;
TDerivedShapes shapes2;
TThings things;
// Эти элементы заставят шар крутиться.
event += new MyEvent::T<TShapes>(&shapes, &TShapes::Square);
event += new MyEvent::T<TShapes>(&shapes, &TShapes::Triangle);
// Этот элемент показывает, что виртуальные функции обрабатываются правильно.
event += new MyEvent::T<TShapes>((TShapes*)&shapes2, &TShapes::Square);
// Этот элемент показывает, что унаследованные функции обрабатываются правильно.
event += new MyEvent::T<TDerivedShapes>(
&shapes2, &TDerivedShapes::Triangle);
// Этот элемент показывает, что объект События
// может хранить разнородную
// коллекцию целей.
event += new MyEvent::T<TThings>(&things, &TThings::Thing1);
// Этот элемент показывает, что статические функции обрабатываются правильно.
event += new MyEvent::S(&TThings::Thing2);
// Этот элемент показывает, что свободные функции обрабатываются правильно.
event += new MyEvent::S(&Free);
// Вызов события с многократным приведением
std::cout << "<multicast>" << std::endl;
event(100);
std::cout << "</multicast>" << std::endl;
return 0;
}
Вот что вы должны увидеть при запуске программы:
<multicast>
TShapes::Square: 100
TShapes::Triangle: 100
TDerivedShapes::Square: 100
TShapes::Triangle: 100
TThings::Thing1: 100
TThings::Thing2: 100
Free: 100
</multicast>